The Pitfalls of Dynamic Proxy Serialization

Recently, we ran into problems de-serializing proxy objects. The first pitfall involved a bug in Weblogic that prevented us from de-serializing a java.lang.reflect.Proxy instance. The second pitfall occurred as a result of using a Cglib proxy. In our efforts to deal with these pitfalls we deepened our understanding of how these libraries create dynamic proxies.

Pitfall #1: Classloader Problems

We wanted to use the java.lang.reflect.Proxy class to make instances of one (very simple) class assignable to different types of interfaces. Everything was going just swimmingly and we were close to checking in our code. When it came time to run our integration tests, Weblogic began throwing a ClassNotFoundException whenever it attempted to de-serialize a proxy instance in a MessageDrivenBean.

Once we confirmed that the interfaces were indeed on the Classpath, we fired up the debugger and dug a little deeper (but not until we consumed a a copious amount of Coca-Cola and Marlboros… thanks for the tip Mike). We discovered that – in the process of de-serializing our proxy object – the java.io.ObjectInputStream was trying to load our interfaces with the wrong classloader.

It was using the parent of the Weblogic application classloader which had no knowledge of the classpath in our EAR file. We were were pretty certain that the reason was due to a bug in the way Weblogic was managing classloaders in its JMS implementation (of course, Weblogic is not open source so we can’t be 100% certain of this).

We couldn’t de-serialize the object ourselves inside the message bean and we couldn’t modify the way Weblogic managed classloaders, so we tried using the Cglib library instead. The Cglib proxy worked. The reason is because the ObjectInputStream de-serializes Cglib instances differently than it does a java.lang.reflect.Proxy instance. Why? Because of a key difference between the two types of proxy objects;

  • When you create an instance using the JDK Proxy, it generates a subclass of the java.lang.reflect.Proxy class for you (the subclass is assignable to any interfaces you provide). Furthermore, the class generated by the java.lang.reflect.Proxy will have a different class descriptor (in fact, a Proxy class descriptor).
  • On the other hand, the Cglib library generates a subclass of an arbitrary class that you provide (as with the JDK proxy, the Cglib generated subclass will also be assignable to any interfaces you choose to provide). The class generated by the Cglib library will have a regular class descriptor.

Whenever the ObjectInputStream de-serializes an object, it peeks at the Class descriptor. If it has a Proxy class descriptor, it invokes the ObjectInputStream.resolveProxyClass() method which creates a new version of the proxy subclass in the JVM (if it didn’t already exist) based on the declared interfaces. Because the Cglib class has a normal class descriptor, the ObjectInputStream.resolveClass() method is invoked instead. For reasons we’re not sure of, the classloader that is retrieved in the ObjectInputStream.resolveClass() method is the correct one and can resolve our interfaces/classes.

Pitfall #2: Generated Subclasses

Now our Cglib proxy was being de-serialized correctly when sent to a JMS MessageDrivenBean. But we we were also sending these proxies to a rich client, and the Cglib proxies were not being de-serialized there. We were stunned, but only temporarily. This result was to be expected. The Cglib generated objects were instances of a subclass generated dynamically on a different JVM. Our rich client JVM didn’t recognize the class and was quite justified in throwing a ClassNotFoundException.

It turns out there is a pretty simple solution to this problem. Basically, you need to implement the writeReplace() and readResolve() methods on the object that your proxy delegates methods to. The delegate uses these methods to de-proxy and re-proxy itself depending on whether it is being serialized or de-serialized. In our case, our delegate object is the super-class of our Proxy instance, so we implemented the writeReplace() and readResolve() methods on it. Here is an example using a class called DataHandler;

public static class DataHandler implements Serializable {
		private static final long serialVersionUID = 1L;
		private final String someData;

		public DataHandler(String someData){
			this.someData = someData;
		}
		public String getSomeData() {
			return someData;
		}

		public boolean equals(Object obj) {
			return obj instanceof DataHandler ?
				((DataHandler)obj).getSomeData().equals(this.someData)
				: false;
		}

		public Object writeReplace() throws ObjectStreamException{
			return new DataHandler(this.someData);
		}
		public Object readResolve() throws ObjectStreamException{
			return ProxyFactory.createCglibProxy(DataHandler.class, new Class[]{IFoo.class}, new Object[] {this.someData});
		}
}

public interface IFoo extends java.io.Serializable{
}

We create a proxy with the Cglib library like so;

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(DataHandler.class);
enhancer.setInterfaces(new Class[]{IFoo.class});
enhancer.setCallback(new NoOpCallback());//a NoOp callback. All methods will be invoked on the superclass

//generates a subclass of DataHandler that is assignable to the IFoo interface.
IFoo foo = (IFoo) enhancer.create(new Class[]{String.class}, new Object[] {"someData"});

When the foo instance is serialized, the super-class (DataHandler) method writeReplace() is invoked. The writeReplace method returns a new instance of the DataHandler class, thereby discarding the Proxy wrapper subclass. It’s this new instance of DataHandler that gets serialized, not the original subclass.

When the DataHandler instance is eventually de-serialized, the readResolve() method is invoked on it. The readResolve() will generate and return a new proxy subclass equivalent to the original. We learned this little trick by studying the code in the Hibernate project which uses Cglib for proxies and is open-source.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply