Tuesday, July 15, 2008

ClassLoader hell the OSGi way

Recently I have been trying to write a proxy service in OSGi. The reasons for wanting to do this are varied, but it is mostly about imports. In OSGi (and Spring DM) you have to import packages that you use. This is all well and good until you start needing to import packages that you didn't know you used - proxies in particular. Spring uses proxies to augment bean functionality usually using AOP and Spring DM uses proxies to represent client services. All well and good - I don't want to describe here the benefits of proxies. The issue is that in my code, or Spring context file, I specify that I am creating a FooBean so clearly I need to import FooBean's package. However when it gets proxied I need to also start importing interfaces exposed by the proxy. In Spring's case this is org.springframework.aop.SpringProxy and org.springframework.aop.framework.Advised.

Fine, I will import those two packages. FooBean is an impl so I am using cglib to proxy it. cglib adds net.cglib.proxy.Factory to the proxied interface set so I need to add that import as well. I can just about live with those, although explaining them to users will be a pain. Next, I want to hide all of the proxy implementation classes inside another bundle, so I write a service that provides approximately the same set of functionality as org.springframework.aop.framework.ProxyFactory. Easy as pie.

You would think.

The use of Factory is the first problem. When I run my little service I start getting NoClassDefFoundErrors for net.sf.cglib.core.Signature. This package is imported by my proxy service provider bundle, and the provider bundle's classloader is the one I am passing to cglib's Enhancer for proxy generation. So why can't the bundle classloader for Enhancer see Signature?
Well the bundle classloader for Enhancer turns out to be the client bundle because Enhancer and Factory are in the same package. So the fact that the client needs to see Factory also means that it imports Enhancer (rather than my provider bundle) and the whole thing unravels from there.

Spring has the same problem. The cglib entry point is defined by org.springframework.aop.framework.Cglib2AopProxy. Does that package look familiar somehow? Right, its the same package as Advised - needed by the proxy. Doh!

So now my options are:
  1. Change Spring and cglib - bit of a backwards compatibility problem there.
  2. Start massaging classloaders for individual classes - not exactly the OSGi way
  3. Chain the required packages into the client classloader - again subverting the OSGi class space protection model
  4. Create some entirely new classloader for the proxy - not clear to me that this will actually work
  5. Try using Enhancer.setUseFactory()
  6. Import the packages into my client bundle, *sigh*
The moral of the story: keep interfaces in separate packages when you are targetting OSGi.

Update

I have actually now used a modified version of (3) above, and it works quite nicely. The theory goes like this - the client bundle knows about classes that need augmenting, and contains the appropriate imports for these. The provider bundle knows about proxying and contains the appropriate imports for the mechanics of this. So what we actually need is a classloader that synthesizes these two. Constructing a simple delegating classloader which first tries the client classloader and then the provider ends up working really nicely. Even better I would argue that this does not subvert the OSGi model, the reasoning is that this delegate classloader is only used for proxying - no client classes are magically loaded by it - and the class space represented by the union of the two classloaders involved is still bounded by their respective imports. No need to use dynamic imports or other ugly hacks. Furthermore the class space boundary is neatly delineated by the service definition. Only classes on the client-side need to be imported and no details of the service implementation are exposed by the required imports.

No comments: