Say we have an interface 'Foo' (defining a getter and setter for
property 'bar' and 'id') and an implementing class 'FooImpl' which is
a simple bean with the properties 'bar' and 'id'. Further we have
something like 'FooManager' with methods 'Foo FooManager.getFoo(long
id)' and 'void FooManager.modifyFoo(Foo foo)'
As long as I retrieve some Foo (more precisely the JSON representation
thereof) from the server, modify it and pass it back to modifyFoo,
everything works fine as long as the class hinting is enabled. The
JSON representation will have a class hint with the runtime class of
our Foo, namely FooImpl. When passing the modified JSON object back to
the server, jabsorb looks at the method signature of the FooManager
instance registered with the bridge and the class hint on the JSON
object, and looks for a serializer that can handle the type found in
the class hint, in this case, FooImpl. In our example, let's assume
the serialzer is BeanSerializer. BeanSerializer checks that the class
in the hint (FooImpl) is compatible with the interface in the method
signature (Foo), if so, it unmarshalls the JSON by instantiating an
instance of FooImpl and setting some properties based on the JSON
object.
The tricky part comes if we use the same scenario, but the Foo passed
from the browser to the server has no class hint. JSON looks like '{
bar: "some value" }'. How could this happen? Maybe we have chosen to
disable class hinting, or maybe we have class hinting enabled, but
just created the JSON object on the fly instead of retrieving it from
the server - some javascript like 'jsonrpc.fooManager.modifyFoo({ id:
1, bar: "new value"})'. What happens in this case? In the absence of a
class hint, based on the method signature only, jabsorb looks for a
serializer that can handle type Foo (the interface). None of the
default serializers will take the job, they just do not know how they
should create an instance of - or better said: implementing -
interface Foo. An exception is thrown.
As I see there are 3 alternatives to solve this issue:
1. The quick and dirty one is to always set a class hint on the client
side. Why do I say dirty? Well, the javascript code has nothing to do
with, and should not know about what runtime classes (implementing the
public interfaces) the server side java code uses. It would just
pollute the client code and guarantee some head scratching once the
implementation behind the interfaces changes. We have interfaces in
java to enable loosely coupled, modular and extensible application
design. The javascript client should really not have to know which
concrete java class to set in the class hint.
2. Let's create a custom serializer for Foo, that knows how to
marshall and unmarshall this type, and is aware of which implementing
class to use while unmarshalling. This is straightforward, code is
kept on the server side. Though it requires a lot of boilerplate code
if you have many interfaces... Still not satisfied.
3. The default serializers are great, especially BeanSerializer can
free us from writing dozens of custom serializers. I thought it would
be great if we could somehow register interfaces with their
implementing classes, and, for example tell jabsorb to, whenever it
has to unmarshall interface Foo, just unmarshall class FooImpl instead
with the registered serializers. This approach is generic, we only had
to define a mapping from interfaces to concrete classes instead of
developing many-many custom serializers. And based on this
interface-to-class mapping, unmarshalling of interfaces could be
'redirected' to the serializer that can handle the implementing class.
I have implemented option 3 and tested it with the 1.3 branch. It can
unmarshall to the configured implementing classes of the interfaces
without any further hinting. You can use it as illustrated below:
Map<Class<?>,Class<?>> interfaceMap = new HashMap<Class<?>,Class<?>>();
interfaceMap.put(org.example.Foo.class, org.example.impl.FooImpl.class);
interfaceMap.put(org.example.Bar.class, org.example.impl.BarImpl.class);
interfaceMap.put(org.example.Frenzy.class, org.example.impl.FrenzyImpl.class);
try {
bridge.registerSerializer(new InterfaceDeserializer(interfaceMap));
} catch (Exception e) {
throw new RuntimeException("Could not register InterfaceDeserializer!", e);
}
This piece of code is distributed in the hope that it will be useful,
but without any warranty... you know the story.
I hope I did not reinvent the wheel... Is there some trivial/simpler
solution for the problem I have missed? What do you think about the
approach?
Any comment is welcome!