Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GWT Custom Field Serialization Issue

Consider an immutable class Foo (a POJO consisting of an ID and a name), that needs to be serialized in order for the data to be sent from the server to the client.

public final class Foo
{
    private final int m_id;
    private final String m_displayName;

    private Foo(final int id, final String displayName)
    {
        m_id = id;
        m_displayName = displayName;
    }

    public static Foo create(final int id, final String displayName)
    {
         // Some error checking occurs here.
         . . . 

         m_id = id;
         m_displayName = displayName;
    }

    // Getters etc.
    . . .
}

Instantiation of a Foo object happens through the static factory function and since the object's immutable there is no zero-args constructor.

Consider an immutable class Bar as well that contains a data member Foo and implements the Builder pattern for its instantiation (omitted from snippet as it is irrelevant to the problem).

public final class Bar
{
    private final Foo m_foo;
    . . .

    private Bar(final Builder builder)
    {
        . . .
    }

    public static Builder createBuilder()
    {
        return new Builder();
    }
}

After evaluating my choices regarding the way I should serialize this object without removing its immutability or adding any zero-args constructors for the sake of serialization I concluded to the fact that I needed to implement a CustomFieldSerializer (for both the client and the server).

I followed the guidelines written in the Server Communication article of GWT and implemented my own CustomFieldSerializer as depicted below.

// Contains the serialization logic of the class Bar.
public final class Bar_CustomFieldSerializerBase
{
    public static Bar instantiate(final SerializationStreamReader streamReader) throws SerializationException
    {
        return Bar.createBuilder().forFoo((Foo) streamReader.readObject()).build();
    }

    public static void serialize(final SerializationStreamWriter streamWriter, final Bar instance)
        throws SerializationException
    {
        // . . .
        streamWriter.writeObject(instance.getFoo());
    }

    public static void deserialize(final SerializationStreamReader streamReader, final Bar instance)
    {
        /*
         * Empty as everything is handled on instantiateInstance().
         */
    }
}

// The CustomFieldSerializer for class Bar. 
public class Bar_CustomFieldSerializer extends CustomFieldSerializer<Bar>
{
    public static void deserialize(final SerializationStreamReader streamReader, final Bar instance) throws SerializationException
    {
    Bar_CustomFieldSerializerBase.deserialize(streamReader, instance);
    }

    public static void serialize(final SerializationStreamWriter streamWriter, final Bar instance) throws SerializationException
    {
        Bar_CustomFieldSerializerBase.serialize(streamWriter, instance);
    }

    public static Bar instantiate(final SerializationStreamReader streamReader) throws SerializationException
    {
        return Bar_CustomFieldSerializerBase.instantiate(streamReader);
    }

    @Override
    public boolean hasCustomInstantiateInstance()
    {
        return true;
    }

    @Override
    public Bar instantiateInstance(final SerializationStreamReader streamReader) throws SerializationException
    {
        return instantiate(streamReader);
    }

    @Override
    public void deserializeInstance(final SerializationStreamReader streamReader, final Bar instance) throws SerializationException
    {
        deserialize(streamReader, instance);
    }

    @Override
    public void serializeInstance(final SerializationStreamWriter streamWriter, final Bar instance) throws SerializationException
    {
        serialize(streamWriter, instance);
    }

// Server side CustomFieldSerializer for class Bar.
public class Bar_ServerCustomFieldSerializer extends ServerCustomFieldSerializer<Bar>
{
    public static void deserialize(ServerSerializationStreamReader streamReader, Bar instance,
        Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes) throws SerializationException
    {
    /*
     * Empty as everything is handled on instantiateInstance().
     */
    }

    @Override
    public Bar instantiateInstance(ServerSerializationStreamReader streamReader) throws SerializationException
    {
        return Bar_CustomFieldSerializerBase.instantiate(streamReader);
    }

    @Override
    public void deserializeInstance(ServerSerializationStreamReader streamReader, Bar instance,
        Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes) throws SerializationException
    {
        deserialize(streamReader, instance, expectedParameterTypes, resolvedTypes);
    }

    @Override
    public void deserializeInstance(SerializationStreamReader streamReader, Bar instance) throws SerializationException
    {
        Bar_CustomFieldSerializerBase.deserialize(streamReader, instance);
    }

    @Override
    public void serializeInstance(SerializationStreamWriter streamWriter, Bar instance) throws SerializationException
    {
        Bar_CustomFieldSerializerBase.serialize(streamWriter, instance);
    }
}

Since the class bar contains a Foo object that needs to be serialized I went on and implemented another set of CustomFieldSerializers, this time for class Foo following the same pattern, for both the client and server.

The issue arises when serialization occurs for the class Bar and specifically at this point:

public static void serialize(final SerializationStreamWriter streamWriter, final Bar instance)
        throws SerializationException
{
   // . . .
   streamWriter.writeObject(instance.getFoo());
}

The exception message I get is the following:

[WARN] Exception while dispatching incoming RPC call com.google.gwt.user.client.rpc.SerializationException: Type 'ui.shared.models.fooItems.Foo' was not included in the set of types which can be serialized by this SerializationPolicy or its Class object could not be loaded. For security purposes, this type will not be serialized.

It seems that writeObject() cannot serialize the object of type Foo because the Foo class does not belong to the whitelisted items, even though custom serializers have been provided for both the client and the server.

I could always skip the writeObject() invocation and call writeInt() & writeString() for each Foo's data members (which is working well) but I would much rather get writeObject() to work. The solution that I proposed is highly prone to maintenance mistakes as any changes in the Foo class in the future have to be reflected on both the Foo's serializers (obvious) and Bar's serializers (not so obvious).

I have tried pretty much anything I could find on the net, from implementing the isSerializable interface on both Foo and Bar (didn't make any difference and it shouldn't make any difference since AFAIK classes that supply their own custom serializers do not need to adhere to this rule) and even supplying a private zero-args constructors (which should also not make any difference as the instantiation functions of the custom field serializer should take care of that through the static factories).

Why is the Foo class not whitelisted? Have I missed something obvious or have misinterpreted something?

Thank you for your time in advance.

like image 537
heaven_sent Avatar asked Feb 25 '13 15:02

heaven_sent


1 Answers

Well the problem here, is that most likely you never explicitly mention anywhere in your code, that Foo class is ever sent to server. E.g. you have only service methods like this:

interface MyService {

   Foo getFoo(Bar bar); 

}

To use writeObject you need to explicitly make a hint for GWT, to include Foo class into deserialization list, by adding a new service method, which takes Foo class as parameter:

interface MyService {

   Foo getFoo(Bar bar); 

   void setFoo(Foo foo);// letting GWT know that we might send Foo object over the wire, you don't have to ever call this method in your app, or implement it in some meaningful way, just let it be there

}

You don't have to ever call this method in your app, or provide any implementation to it. But otherwise it won't work. There are few other ways how to create this hint for GWT, but idea is the same, you will have explicitly expose Foo class for GWT-RPC. Also remember, that if you use Bar class in multiple services, you will have to add such method to each service which is using Bar class

Now, more details why this happens. On server side, GWT-RPC tracks a two lists: objects which it can serialize and objects which it can deserialize. This info is taken from RPC policy manifest. In my first example, I have only mentioned Bar object as something what can be send to the server. But since you have defined a custom field serializer on Bar class, GWT doesn't perform any analysis on Bar, therefore it has no idea that instance of Foo might be sent to the server, therefore it decides that no deserializer needed for Foo on server side. So when you try to send instance of Bar over the wire, server try to deserialize it, but since readObject is used inside the custom serializer, it tries to find deserializer for Foo as well, but it is not allowed and therefore the whole deserealization process fail. If you add extra service method, which sends only Foo object over the wire, GWT becomes aware that such object can be sent to the server as well, therefore it marks Foo as deserializable as well.

This very confusing, and personally I believe that classes with custom serialization should be added automatically to all whitelists, but it is what it is.

EDIT

Another (non hacky solution without stupid empty methods), will be to use specialized DTO layer for communication (e.g. only POJOs with default public constructors), but it might be difficult in cases when you need to send complex object graphs with a lot of cross references.

like image 67
jusio Avatar answered Oct 21 '22 03:10

jusio