May 28, 2011

Injecting context in Java serialization

Tags: Java, Technical

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);
  }
}