Sunday, January 27, 2013

OSGi class loading issue

One of the difficulty when using OSGi is that if there are more than one version (or copies) of the same class available in different bundle, it is often difficult to tell what went wrong.

java.lang.LinkageError: loader constraint violation in interface itable initialization: when resolving method "org.eclipse.jetty.server.Request.getServletContext()Ljavax/servlet/ServletContext;" the class loader (instance of org/jboss/osgi/framework/internal/HostBundleClassLoader) of the current class, org/eclipse/jetty/server/Request, and the class loader (instance of org/jboss/modules/ModuleClassLoader) for interface javax/servlet/ServletRequest have different Class objects for the type javax/servlet/ServletContext used in the signature
22:44:51,428 ERROR [stderr] (qtp1026914784-119 Selector0)     at org.eclipse.jetty.server.AbstractHttpConnection.<init>(AbstractHttpConnection.java:151)

org.springframework.aop.AopInvocationException: AOP configuration seems to be invalid: tried calling method [public abstract void org.ops4j.pax.web.service.WebContainer.registerServlet(javax.servlet.Servlet,java.lang.String,java.lang.String[],java.util.Dictionary,org.osgi.service.http.HttpContext) throws javax.servlet.ServletException] on target [org.ops4j.pax.web.service.internal.HttpServiceProxy@28a2f76f]; nested exception is java.lang.IllegalArgumentException: argument type mismatch
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:326)

These are two of the error I got recently because there are actually two copies of ServletContext.  The first exception happens when I tried to call the rest interface (resteasy with pax web jetty bundle), the framework threw a LinkageError which has a very good error message showing both are loaded with a different classloader.  The 2nd error is when my code tried to register a servlet (which get the ServletContext from HostBundleClassLoader) to resteasy (which gets it from ModuleClassLoader). 

I've developed a little code to print out more details about where the class is loaded.
private Map<String,Map<String,Object>> findClass(BundleContext context, String name) {
        Map<String,Map<String,Object>> result = new HashMap<String,Map<String,Object>>();
        for (Bundle b : context.getBundles()) {
            try {
                Class<?> c = b.loadClass(name);
                Dictionary<String,String> dict = b.getHeaders();
                Map<String,Object> value = new HashMap<String, Object>();
                Enumeration<String> keys = dict.keys();
                while( keys.hasMoreElements() ) {
                    String key = keys.nextElement();
                    value.put(key,dict.get(key));
                }
                value.put("class", c );
                value.put("classLoader", c.getClassLoader() );
                value.put("location", c.getClassLoader().getResource(name.replace('.','/')+".class"));
                result.put(b.getSymbolicName() + " " + c, value );
            } catch (ClassNotFoundException e) {
                // No problem, this bundle doesn't have the class
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

The idea is simple.  For each bundle, call findClass (which use the classloader of the bundle) and if it's found, get the headers, class loader & the location of the class itself and put everything in a hashmap.

Note that it's running on JBoss, the header dictionary it returned is unmutable.

Turns out the problem is because "bundles" from jboss' module directory (/standalone/modules) are loaded by ModuleClassLoader.

<subsystem xmlns="urn:jboss:domain:osgi:1.2" activation="eager">
...
<capabilities>
  <capability name="org.jboss.resteasy.resteasy-jaxrs"/>
...

and it will automatically loaded all dependencies defined in path\to\jboss\modules\org\jboss\resteasy\resteasy-jaxrs\main\module.xml which includes javax.servlet.api.

However, unless you specifically declares that as one of the capabilities
<capability name="javax.servlet.api"/>
it won't be available to OSGi.  Adding the javax.servlet.api to jboss' osgi capabilities does completely solve the classloading issue as another copy of ServletContext is available in the pax web jetty bundle.  Since both are exporting javax.servlet;version=3.0.0, we have an intermitten LinkageError problem.


Two options here:

1. do not use the resteasy in the jboss modules.  instead deploy it with the rest of the bundles so it won't load the servlet api with the module classloader.  thus, only the one loaded from pax web jetty bundle will be used.  Because resteasy in the central maven repo is not osgi ready, i'll have to wrap it in a bundle.
2.  instead of using pax web jetty bundle, use the individual pax web jetty, pax web runtime...  I did a simple test, looks like pax web runtime (2.1.2) has problem importing the javax.servlet;version=3.0.0 from jboss.

not sure which options is better, i'll update the post once i'm done with it.

2 comments:

  1. pax web only supports servlet 2.5, not 3.0. For OSGi, one should use JBoss 7.2 snapshots, 7.1 seems to be quite crippled osgiwise...

    ReplyDelete
  2. in fact, starting pax web 2.x, it supports servlet 3.0. and yes, osgi in 7.1.1.Final is bad but our management had a hard time letting us to use anything you need to download the source to build. :(

    ReplyDelete