//
you're reading...
Java/Cloud

Serialization and Classloaders

Serialization in java allows POJOs to be converted into a “savable” format that can be easily saved into any persistence or used to transmit across network and loaded back to create new objects in a different or same JVM. There are a number of ways in which serialization can be adapted to do the things we want as detailed here and many other sites. But all of these talk about objects as homogeneous objects., homogeneous in terms of the classloader used to load these objects. What do I mean by this? 
Let’s take two simple objects to understand this:
public class TestObject1 implements java.io.Serializable
{
    private String _value1;
    public TestObject1()
    {
        _value1 = “using classloader:” + this.getClass().getClassLoader();
    }
}
and another object as below:
public class TestObject2 implements java.io.Serializable
{
    private String _value1;
    //NOTE I have defined this as object so I can use different loaders
    private Object _tst1; 
    public TestObject2()
    {
        _value1 = “using classloader:” + this.getClass().getClassLoader();
    }
    public void setTst1(Object t) { _tst1 = t; }
    public Object getTst1() { return _tst1; }
}
A sample instantiation of the TestObject2 is as below:
     TestObject2 obj = new TestObject2();
     TestObject1 obj1 = new TestObject1();
     obj.setTst1(obj1);
When the object is created as above, the default serialization and deserialization of “obj” will work correctly, since both “obj” and “obj1” are created using the same classloader. So a code as below will not have RuntimeException:
public void serializeDeserialize(Object obj)
    throws Exception
{
      ByteArrayOutputStream ostr = new ByteArrayOutputStream();
      ObjectOutputStream str = new ObjectOutputStream(ostr);
      str.writeObject(obj);
      str.close();
      byte[] serial = ostr.toByteArray();
      ostr.close();
      ByteArrayInputStream istr = new ByteArrayInputStream(serial);
      ObjectInputStream des = new ObjectInputStream(istr);
      Object o = des.readObject();
      des.close();
      istr.close();
}
But let’s change the instantiation a little bit using different classloaders. Say we have something as below:

        TestObject2 obj = new TestObject2();
        ClassLoader ldr = new MyLoader(
                  new URL[] { new 
                   URL(“file:///home/rsankar/research/serial/tstcl/test.jar”) }
        Class cls = ldr.loadClass(“TestObject1”);
        Object t = cls.newInstance();
        obj.setTst1(t);
Calling serializeDeserialize function on this “obj” object will throw a RuntimeException, assuming that the TestObject1 is not in the classpath of TestObject2 classloader. The problem is that TestObject1 has to be resolved by our MyLoader classloader while TestObject2 needs to be resolved by the default classloader. So, how can we get over this problem?
One of the approaches is to create your own deserializer that knows which object is loaded using which classloader. So in the above a Deserializer can be created as below that will help:
public class TestCLInputStream extends ObjectInputStream
{
        private ClassLoader _ldr;
        TestCLInputStream(InputStream str, ClassLoader ldr)
            throws IOException
        {
            super(str);
        }
        protected ClassresolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException
        {
            if (desc.getName().contains(“TestObject2”))
                return _ldr.loadClass(desc.getName());
            return super.resolveClass(desc);
        }
}
Thus the serialize/Deserialize changes as below:
public void serializeDeserialize(Object obj, ClassLoader ldr)
    throws Exception
{
      ByteArrayOutputStream ostr = new ByteArrayOutputStream();
      ObjectOutputStream str = new ObjectOutputStream(ostr);
      str.writeObject(obj);
      str.close();
      byte[] serial = ostr.toByteArray();
      ostr.close();
      ByteArrayInputStream istr = new ByteArrayInputStream(serial);

      TestCLInputStream des = new TestCLInputStream(istr, ldr);

      Object o = des.readObject();
      des.close();
      istr.close();
}
But this has it’s own very obvious extension problems:
  • What if I had a whole jar with 100 classes in it which I want loaded using MyLoader.
  • What if I had a test1.jar with classes which is loaded using MyLoader, while I have another test2.jar which is loaded using MyLoader2?
  • What if I had a class TestObject2 loaded using different loaders, based on the context of execution?
A better approach would have been if the serialized bytes somehow gave me an indication of which classloader was used during serialization so that during deserialization I can map the same classloader and deserialize this? So, can we do this using java serialization framework? Yes. 
The ObjectOutputStream and ObjectInputStream has the functions writeClassDescriptor and readClassDescriptor functions respectively that writes a description of the class being serialized. This information is used to read back the data. This can then be used to write some id of the classloader used to load the object during serialization and can be used to look up the classloader during deserialization.
For now I have created classloaders that can be recognized using unique names. To create this: 
    public class MyLoader extends URLClassLoader
    {
        private String _name;
        public MyLoader(URL[] urls, String name)
        {
            super(urls);
            _name = name;
        }
        public String toString() { return _name; }
    }
Create a ObjectOutputStream that overrides writeClassDescriptor and stores the classloader name also:
    public class TestCLOutputStream extends ObjectOutputStream
    {
        TestCLOutputStream(OutputStream str)
            throws IOException
        {
            super(str);
        }
        @Override
        protected void writeClassDescriptor(ObjectStreamClass desc)
             throws IOException
        {
            super.writeClassDescriptor(desc);
            //Write the classloader name
            String ldr = desc.forClass().getClassLoader().toString();
            writeObject(ldr);
        }
    }
Create a ObjectInputStream that overrides the readClassDescriptor:
    public class TestCLInputStream extends ObjectInputStream
    {
        private Map _loaders;
        TestCLInputStream(InputStream str)
            throws IOException
        {
            super(str);
            _loaders = new HashMap();
        }
        @Override
        protected ObjectStreamClass readClassDescriptor()
             throws IOException, ClassNotFoundException
        {
            ObjectStreamClass cls = super.readClassDescriptor();
            String ldr = (String)readObject();
            _loaders.put(cls, ldr);
            return cls;
        }
        protected ClassresolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException
        {
            //use look up functionality to 
            ClassLoader ldr = _loader.get(_loaders.get(desc));
            if (ldr != null)
                return ldr.loadClass(desc.getName());
            return super.resolveClass(desc);
        }
    }
Now create the serializeDeserialize function as below and run to get a heterogeneous object serialized and deserialized correctly:
public void serializeDeserialize(Object obj, ClassLoader ldr)
    throws Exception
{
      ByteArrayOutputStream ostr = new ByteArrayOutputStream();

      TestCLOutputStream str = new TestCLOutputStream(ostr);

      str.writeObject(obj);
      str.close();
      byte[] serial = ostr.toByteArray();
      ostr.close();
      ByteArrayInputStream istr = new ByteArrayInputStream(serial);

      TestCLInputStream des = new TestCLInputStream(istr, ldr);

      Object o = des.readObject();
      des.close();
      istr.close();
}
In this methodology objects can contain other objects loaded using any number of classloaders. The deserialized objects will also follow the same classloader hierarchy with the mapped classloaders. 

Advertisements

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: