Equinox Jetty: ClassNotFound when trying use JDBCSessionManager

759 views Asked by At

I am getting a ClassNotFoundException from Jetty (Equninox embedded) when trying to use JDBCSessionManager and JDBCSessionIdManager.

Exception:

2017-01-06 10:37:02.620:WARN:oejss.JDBCSessionManager:qtp1215746443-29: Unable to load session 192168178229yf02ln7ut25phh97b49003w
java.lang.ClassNotFoundException: org.eclipse.equinox.http.servlet.internal.servlet.HttpSessionAdaptor$ParentSessionListener cannot be found by org.eclipse.jetty.util_9.3.9.v20160517
    at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:439)
    at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:352)
    at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:344)
    at org.eclipse.osgi.internal.loader.ModuleClassLoader.loadClass(ModuleClassLoader.java:160)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:628)
    at org.eclipse.jetty.util.ClassLoadingObjectInputStream.resolveClass(ClassLoadingObjectInputStream.java:59)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1620)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
    at java.util.HashMap.readObject(HashMap.java:1404)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058)
    at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1909)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1808)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
    at org.eclipse.jetty.server.session.JDBCSessionManager$1.run(JDBCSessionManager.java:970)
    at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1262)
    at org.eclipse.jetty.server.session.JDBCSessionManager.loadSession(JDBCSessionManager.java:992)
    at org.eclipse.jetty.server.session.JDBCSessionManager.getSession(JDBCSessionManager.java:502)
    at org.eclipse.jetty.server.session.JDBCSessionManager.getSession(JDBCSessionManager.java:75)
    at org.eclipse.jetty.server.session.AbstractSessionManager.getHttpSession(AbstractSessionManager.java:331)
    at org.eclipse.jetty.server.session.SessionHandler.checkRequestedSessionId(SessionHandler.java:275)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:151)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1106)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
    at org.eclipse.jetty.server.Server.handle(Server.java:524)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:319)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:253)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:273)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:95)
    at org.eclipse.jetty.io.SelectChannelEndPoint$2.run(SelectChannelEndPoint.java:93)
    at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.executeProduceConsume(ExecuteProduceConsume.java:303)
    at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.produceConsume(ExecuteProduceConsume.java:148)
    at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.run(ExecuteProduceConsume.java:136)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:671)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:589)
    at java.lang.Thread.run(Thread.java:745)

I am using a JettyCustomizer to hook into the Jetty startup to change the default HashSessionManager with the JDBCSessionManager. The JettyCustomizer is located in a Fragment Bundle which belongs to

Fragment-Host: org.eclipse.equinox.http.jetty

I got this idea from https://wiki.eclipse.org/RAP/FAQ#How_can_I_use_Jetty_basic_authentication_in_my_application.3F

This setup works ok, and JDBCSessionManager places a session in the Database. The session is Serialized to a Byte-BLOB and stored in the DB. I can see it there.

But it seems the serialization is done by org.equinox.http and it places class reference like org.eclipse.equinox.http.servlet.internal.servlet.HttpSessionAdaptor$ParentSessionListener into the BLOB.

Note, that internal.servlet.HttpSessionAdaptor is an internal class which is not exported to other bundles.

Now when the session information is read again from the database (e.g. when I access the webpage again later with the same sessionCookie) I run into this problem when org.eclipse.jetty.util.ClassLoadingObjectInputStream.resolveClass(ClassLoadingObjectInputStream.java:59) tries to load the classHttpSessionAdaptor$ParentSessionListener but cannot see it (because it is a) internal and / or b) in another bundle.

org.eclipse.jetty.util.ClassLoadingObjectInputStream lives in bundle org.eclipse.jetty.util but org.eclipse.equinox.http.servlet.internal.servlet.HttpSessionAdaptor$ParentSessionListener lives in bundle org.eclipse.equinox.http.servlet.

org.eclipse.jetty.util.ClassLoadingObjectInputStream seems to do the following:

@Override
    public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException
    {
        try
        {
            return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader());
        }
        catch (ClassNotFoundException e)
        {
            return super.resolveClass(cl);
        }
    }

Is there anybody from the OSGI experts with an ideas?

I would describe the problem as that Session byte-BLOB contains Class References to internal classes which cannot be seen by org.eclipse.jetty.util.ClassLoadingObjectInputStream.resolveClass

Does that seem like a bug? Or is the approach with the FragmentBundle with the the wrong approach? (IMO it is the only way I found to exchange the SessionManager)

1

There are 1 answers

4
Gunnar On BEST ANSWER

The issue is probably because ClassLoadingObjectInputStream is using the TCCL for class resolution, which - in Equinox - by default is the org.eclipse.osgi.internal.framework.ContextFinder. It is finding the first bundle on the call stack. This is likely the Jetty bundle, which does not see any of the Equinox classes.

As far as the Equinox HTTP Service is concerned, the fragment approach is the right one for hooking into Jetty. If I'm reading the code path right, you could try the following things.

(1) Set class loader on ContextHandler

In your JettyCustomizer.customizeContext you should inspect the context. It should be a ServletContextHandler. Use its setClassLoader method to give it a class loader that knows about the Equinox classes (which any fragment of org.eclipse.equinox.http.jetty should know anyway) and any other classes of your own custom code.

(2) Fork/patch JDBCSessionManager

If approach 1 does not work then you likely need to create your own fork of JDBCSessionManager. Extending might not work because of visibility issues (some methods are private). You need to override/patch/reimplement the JDBCSessionManager.loadSession method to use the correct class loader for loading. In the original implementation you can see why approach 1 should work (in theory). The code of your implementation can be much simpler, though.

If your fragment also imports the packages of your code, then simple use your fragment class loader. Otherwise you can create a custom one that delegates to the correct bundles for resolution.