Serialization is a useful
feature built into Java. It converts objects into a bytestream that can
be saved to a file or transmitted over a network and then reconstructed
later into the same object (perhaps in a different Java
VM). The basics are
detailed
here,
with some more advanced features
here
and the complete API is
here.
Despite all this documentation, I recently came a across a problem not
described. What if an object being deserialized requires a reference to
some non-serializable object in the
application. That is how can an object be reconstructed when it contains
part of the application’s context that can’t be serialized. For
instance, what if the object has a network connection as a member
variable. The network connection can not be meaningfully serialized, so
when the object is deserialized it has somehow reconnect itself to the
network, which requires some reference to the network context. This may
sound like a contrived example, but it occurs in the Red Dwarf
Server (previously known as Sun’s
Darkstar project) where network connections are transparently moved
around a server cluster.
The best way to solve this problem is to ensure that the serializable objects are self-contained and do not need any outside context when deserialized. Let’s assume that is not possible and move on. Next best is to hook the object to its required context immediately after the deserialization. However, this may not be practicable because the deserialized object is not easily accessible (for example, if it is deep in an object graph - which is the case in Red Dwarf and my code).
Red Dwarf solves the problem by defining a static accessor on the context class and then on the serializable object adding a serialization override method like the below:
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
this.contextObject = Context.getContextObject();
}
This solution works, but I don’t like it. Creating a static context makes it hard to write tests and add a second context if required (I have been caught out by this before). It could improved by making the static context use ThreadLocal storage, but there are other ways.
Checking out the Java serialization source
code,
I thought there may be some way of extending the deserialization
process. The main extension points seem to be the
readObject(ObjectInputStream in)
, writeObject(ObjectOutputStream in)
and readResolve()
methods added to the serializable object. These are
found by reflection on their signature in a private method and can not
be changed. There is a method readObjectOverride()
on
ObjectInputStream,
which allows a bespoke deserialization process to be defined. However,
so many of the methods on ObjectInputStream are private that this would
be like writing a new process from scratch.
There are two other options. The ObjectInputStream can be subclassed and
contain a reference to the context object. This new class can detected
within readObject(ObjectInputStream in)
with instanceof
and the
context dereferenced. An alternative is to keep the subclass of
ObjectInputStream and set enableResolveObject(true)
in the constructor
(this requires the program to have security permission). This means that
after deserialization the resolveObject(Object obj)
method on the
stream subclass will be called and the return value passed as the final
result of the deserialization. Thus this method can perform extra
initialisation or even replace the given object with a different one.
The code below shows both these techniques - you probably only want to
use one at a time.
// imports skipped for brevity
public class SerializationTest {
public static class A implements Serializable {
public int a;
public A(int x) {a = x;}
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
if (in instanceof MyObjectInputStream) {
System.out.println("Found context=" + ((MyObjectInputStream) in).context);
}
}
}
public static class MyObjectInputStream extends ObjectInputStream {
private final int context;
public MyObjectInputStream(InputStream in, int context) throws Exception {
super(in);
enableResolveObject(true); // requires permissions
this.context = context;
}
protected Object resolveObject(Object obj) throws IOException {
return new A(3);
}
}
public static void main(String[] args) throws Exception {
A test = new A(2);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(test);
out.close();
byte[] buf = bos.toByteArray();
MyObjectInputStream in = new MyObjectInputStream(new ByteArrayInputStream(buf), 9);
A test2 = (A) in.readObject();
System.out.println("Serialized object=" + test2.a);
}
}