OSGi: jumping through classoading hoops
Join the DZone community and get the full member experience.
Join For FreeTo set the context of this post, let's have a look at the original use case leading to the problem that caused the solution.
The use caseI wanted to use the Rome library to consume/produce RSS/Atom feeds. I have used it before on traditional JEE projects, but never in an OSGi container.
The environementI am using Fuse ESB 4.1, Progress' commercial distribution of Apache ServiceMix 4 OSGi runtime, based on Apache Felix. The JAX-RS implementation used is Apache CXF v2.2. All of this running on my macbook pro, under Java 1.6.
I deployed Rome 1.0 jar in Fuse ESB 4. This jar contains the necessary metadata in it's manifest to be deployed in an OSGi container.
The codeTo simplify things, let's assume a JAX-RS resource class publishing a feed:
SyndFeed feed = new SyndFeedImpl();
feed.setFeedType("rss_2.0");
feed.setTitle("Sample Feed (created with ROME)");
feed.setLink("http://rome.dev.java.net");
feed.setDescription("This feed has been created using ROME (Java syndication utilities");
// ... add entries
StringWriter sw = new StringWriter();
SyndFeedOutput output = new SyndFeedOutput();
output.output(feed, sw);
return Response.ok(sw.toString()).build();
This code is deployed as an OSGi bundle, depending on CXF, JAX-RS spec, and Rome (among other things).
The problemWhen I run this code and call the URL for this resource, I get a strange exception, referring to the SyndFeedImpl class not initializing correctly. What? I only called the default constructor, what's wrong with that??
org.apache.cxf.interceptor.Fault: Could not initialize class com.sun.syndication.feed.synd.SyndFeedImpl
at org.apache.cxf.service.invoker.AbstractInvoker.createFault(AbstractInvoker.java:148)
at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:114)
at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:122)
at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:70)
... [snip]
Digging through Rome source code, I found that all the plugins (I/O components + various helper implementations) are specified in a properties file somewhere on the classpath (some poor man's DI). Rome reads this file so it can instantiate these classes. But to do so, it needs to ask a classloader to load them first. Let's have a look at how this is implemented:
PropertiesLoader loader = (PropertiesLoader)
clMap.get(Thread.currentThread().getContextClassLoader());
if (loader == null) {
try {
loader = new PropertiesLoader(MASTER_PLUGIN_FILE, EXTRA_PLUGIN_FILE);
clMap.put(Thread.currentThread().getContextClassLoader(), loader);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
return loader;
So here, we can clearly see that while reading the plugins configuration, it stores these properties alongside the classloader to be used to load the classes. The classloader used is Thread.currentThread().getContextClassLoader().
BAM!At runtime, when inspecting what this classloader resolves to, we find that it is set to:
System.out.println(Thread.currentThread().getContextClassLoader().toString());
$ [Apache ServiceMix CXF Transport for OSGi (org.apache.servicemix.cxf.transport.osgi)]
Which is the classloader for an entirely different bundle. However, to achieve true modularity, OSGi's architecture tells us (section 3.4 second par.) that for each bundle there is one classloader. So Rome cannot load and instantiate its plugins (dynamically in this case).
Solution 1Procrastinate, and by doing so, bill your client even more.
Solution 2Open up Rome's maven project in Netbeans, and fix the code. But isn't classloading stuff a complicated matter? it depends. One easy way to fix it, is to figure out how OSGi is working. When it is installing a bundle, even before resolving this bundle's dependencies, the framework creates a classloader, a private & cozy place where the bundle's classes will not get disturbed/shadowed by others (read: jar hell). This classloader is the only one who can load classes from this bundle, since it's the only one seeing them (not entirely true, but anyways).
So, it's easy to ask a class for it's classloader, by simply doing something like:
ClassLoader cl = this.getClass().getClassLoader();
So now, If I change the code to use this classloader, and I inspect what it resolves to, I get:
System.out.println(this.getClass().getClassLoader().toString());
$ [211.0]
Which represents Rome bundle ID inside of Fuse ESB 4.
ConclusionI opted for solution 2 ;) Which solution forced me to recompile a special Rome version and store it in our internal Maven repo.
I learned that sometimes, there's more to osgifiying a jar than just providing a nice manifest. Enterprise systems developers must be aware of that sort of stuff before just adding a jar to a project.
Maybe other solutions would have been viable: Equinox buddy visibility manifest headers? Dynamic-imports? (althought I read somewhere that this must be used as a last resort solution as it breaks OSGi modularity principles).
I'd be happy to hear other point of views.
JS.Opinions expressed by DZone contributors are their own.
Comments