Piotr Tomiak Architect and lead developer of the MyEclipse and Webclipse products. Having worked for more than 5 years in Genuitec knows Eclipse inside out and is dedicated to provide the best tools for developers. He believes that it is never too late for some refactoring. Follow @MyEclipseIDE for the latest.

The Challenge

Dealing with classloaders can be tricky, especially if some of them provide the same classes. For instance, this can happen if you need to include several versions of the same library. OSGi is a nice way to deal with such problems, but sometimes there is no other way than to isolate the classloader.

For example, when we were creating a connector for the Websphere server in MyEclipse we had to load WebSphere Admin Client jars. Since WebSphere is based on Eclipse, those jars include a lot of Eclipse classes, sometimes in different versions than MyEclipse. When we tried to use the client we ran into method declarations incompatibility and LinkageError: loader constraint violation errors when some of the classes were loaded from Admin Client and some from MyEclipse.

We had no other option but to isolate the Admin Client jars classloader.



Isolating the classloader

Cross-Boundary Communications

Managing the WebSphere server is not trivial and we needed to easily “communicate” with the Admin Client part. That’s where dynamic proxies came into the picture.

Let’s say we’ve implemented an admin client which implements the following interface:

1 2 3 4 5 6 7 8 9 public interface IWebSphereAdminClient { public void stopServer ( ) throws Exception ; public String deployApplication ( Map < String , Object > configuration , IProgressCallback callback ) throws Exception ; } public interface IProgressCallback { void beginTask ( String name ) ; void message ( String message ) ; }

In our connector plugin we would like to use that interface to easily manage WebSphere. However, IWebSphereAdminClient is not visible to the code since it’s classloader is isolated. We need to copy the interface class (and IProgressCallback ) to the connector plugin, so that we have the same interface definitions in both places. They are going to be a bridge between both classloaders.



Bridging between two classloaders

Dynamic Proxies to the Rescue

Once we have such a setup, we need to create a dynamic proxy on IWebSphereAdminClient from connector plugin, which delegates method invocation to WebSphereAdminClientImpl from Admin Client jar. It sounds straightforward, let’s have a look at the code!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //Load the implementation class using admin client jar classloader Class < ? > clazz = proxyClassLoader . loadClass ( "com.genuitec.websphere.proxy.WebSphereAdminClientImpl" ) ; //$NON-NLS-1$ //Create dynamic proxy of IWebSphereAdminClient interface and //delegate method invocation to PassThroughProxyHandler return ( IWebSphereAdminClient ) Proxy . newProxyInstance ( IWebSphereAdminClient . class . getClassLoader ( ) , new Class [ ] { IWebSphereAdminClient . class } , //PassThroughProxyHandler will delegate method invocation to //a particular object passed to it's constructor new PassThroughProxyHandler ( //Create WebSphereAdminClientImpl object by passing //configuration map to it's only constructor. clazz . getConstructors ( ) [ 0 ] . newInstance ( configuration ) , //Pass the admin client jar classloader //for nested proxying proxyClassLoader ) ) ;

All of the magic happens in the PassThroughProxyHandler class. It implements only a single method:

1 public Object invoke ( Object proxy , Method method , Object [ ] args ) throws Throwable ;

and has two fields:

1 2 private final Object delegate ; private final ClassLoader delegateClassLoader ;

delegate is the object to which the method call is delegated and delegateClassLoader is the class loader which PassThroughProxyHandler uses when loading classes visible to the object. We could retrieve class loader from the delegate object, but having a separate field for class loader makes the code more transparent and flexible (if we want to inject a different one at some point).

Finding the method which should be invoked looks to be simple:

1 2 Class < ? > [ ] argTypes = method . getParameterTypes ( ) ; Method delegateMethod = delegate . getClass ( ) . getMethod ( method . getName ( ) , argTypes ) ;

Nested Proxies to Combat Visibility

But what to do, if one of the method arguments, let’s say, the i-th one, has a class not visible to admin client jar class loader, like IProgressCallback (the delegate method will not be found using the code above since classes come from different class loaders)? We need to convert the argument to class, which is visible by the admin client jar class loader, by doing a “nested” proxy:

1 2 3 4 5 6 7 8 9 newArgType [ i ] = delegateClassLoader . loadClass ( argTypes [ i ] . getCanonicalName ( ) ) ; newArg [ i ] = Proxy . newProxyInstance ( delegateClassLoader , new Class [ ] { newArgTypes [ i ] } , new PassThroughProxyHandler ( args [ i ] , argTypes [ i ] . getClassLoader ( ) ) ) ;

Note that this code works only if the argType[i] is an interface, as only interface-type classes can be used with dynamic proxies. Fortunately, we need to convert only those interfaces/classes, which are not visible by both class loaders, that means any arguments of classes (even non-interfaces) from java.* package (and others included in bootstrap classloader), like String , Map , do not need conversion.

Now, having all required interfaces translated, we can find the delegate method:

1 Method delegateMethod = delegate . getClass ( ) . getMethod ( method . getName ( ) , newArgTypes ) ;

And invoke the method with translated arguments:

1 return delegateMethod . invoke ( delegate , newArgs ) ;

Viola!

A complete example application is attached to this article. It contains two eclipse projects: connector , which mimics our connector plugin, and admin-client , which is loaded using a separated class-loader by connector. Remember to run admin-client/build.xml each time you modify anything in the admin-client project so that the changes are picked up by the connector project. To launch the example, make sure that both projects reside in the same directory and launch com.genuitec.websphere.Main#main() method.

Attachments

dynproxies.zip – Sample projects showing code in action