EECOLOR
EECOLOR

Reputation: 11244

Starting a WebAppContext in isolation

I am trying to start a Jetty server with a web context. The web.xml that I am passing contains a custom class that is (for different reasons) on the classpath of the code executing the code below. I expect Jetty not to be able to find the class that is specified in the web.xml, but it does.

How would I construct a new Server with WebAppContext that will not load classes from the current context?

The code below is in Scala, but it should give you an idea what I have tried. If you answer with Java that is perfectly fine.

// A reference to the root class loader, this one should not know 
// about any of my custom classes
val rootLoader = {
  // Recursive method to find the loader without parent
  def parent(loader:ClassLoader):ClassLoader = {
    val p = loader.getParent
    if (p == null) loader else parent(p)
  }
  parent(getClass.getClassLoader)
}

// Check if the custom ClassFile is available from the rootLoader
val className = classOf[my.custom.servlet.ClassFile].getName
val classAvailable =
  try {
    rootLoader.loadClass(className)
    true
  } catch {
    case e:ClassNotFoundException => false
  }
// If I'm not insane no error is thrown
if (classAvailable) sys.error("Class should not be available")

// Create a WebAppContext using an empty WebAppClassLoader 
// based on that rootLoader
val context = new WebAppContext()
context.setParentLoaderPriority(false)
//context.setClassLoader(new WebAppClassLoader(rootLoader, context))
context.setClassLoader(rootLoader)
context.setContextPath("/")
context.setDescriptor(webXmlFile)
context.setResourceBase(resourceBase)

// Create an start the server with the context
val server = new Server(port)
server.setHandler(context)
server.start()

I expected the context not to find the class in question.

Upvotes: 2

Views: 1900

Answers (3)

Joakim Erdfelt
Joakim Erdfelt

Reputation: 49515

The question is not well stated, its hard to understand what your goal is.

Tip: In the future, ask how to reach a goal, then state the path you attempted

Lets assume you have a webapp called example-webapp.war

It has a simple WEB-INF/web.xml with a servlet entry as such ...

  <servlet>
    <servlet-name>test</servlet-name>
    <servlet-class>com.company.foo.TestServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>test</servlet-name>
    <url-pattern>/test</url-pattern>
  </servlet-mapping>

The example-webapp.war comes with its own com.company.foo.TestServlet (found in the webapp's WEB-INF/classes directory)

Now, you have a embedded-jetty instance, you want to launch example-webapp.war, but you want to use an implementation of com.company.foo.TestServlet that exists outside of the example-webapp.war (with different functionality).

You can accomplish this by first recognizing that the servlet spec has classloader isolation of a webapp from its server, and then configuring that classloader isolation layer to poke a small hole through this isolation.

This hole needs to:

  1. Make the webapp classloader capable of seeing the server class
  2. Force the webapp to use the server class version over the webapp class version

This is easy enough to accomplish in embedded-jetty.

Lets start with making a custom version of com.company.foo.TestServlet and making it available in the classpath for the embedded-jetty server.

Finally, configure your WebAppContext to configure the classloader isolation hole mentioned above. (Using the system classes and server classes configurations)

package demo;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class EmbedWithOverridenServlet
{
    public static void main(String[] args) throws Exception
    {
        int port = 8080;
        Server server = new Server(port);

        String wardir = "path/to/example-webapp.war";

        WebAppContext webapp = new WebAppContext();
        webapp.setWar(wardir);
        webapp.setContextPath("/");

        // don't hide server classes from webapps (hole #1 from answer)
        // (allow webapp to use ones from system classloader)
        webapp.addServerClass("-com.company.foo.");

        // webapp cannot change or replace these classes
        // force use of server classes (hole #2 from answer)
        webapp.addSystemClass("com.company.foo.");

        server.setHandler(webapp);

        try
        {
            server.start();
            server.dumpStdErr();
            makeWebRequest(new URI("http://localhost:8080/test"));
        }
        finally
        {
            server.stop();
        }
    }

    private static void makeWebRequest(URI uri) throws IOException
    {
        HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection();
        conn.setAllowUserInteraction(false);

        int status = conn.getResponseCode();
        System.out.printf("Response Status: %d%n", status);

        if(status != 200)
        {
            System.out.printf("ERROR: %s%n", conn.getResponseMessage());
            return;
        }

        try(InputStream in = conn.getInputStream();
            InputStreamReader reader = new InputStreamReader(in);
            BufferedReader buf = new BufferedReader(reader))
        {
            System.out.printf("Response Content Type: %s%n", conn.getHeaderField("Content-Type"));
            String line;
            while ((line = buf.readLine()) != null)
            {
                System.out.printf("[resp] %s%n",line);
            }
        }
    }
}

Update: 2014-Oct-16

A project demonstrating this is available at github.com/jetty-project/jetty-classloader-manipulation

The example-webapp is a pretty normal webapp with a single servlet of its own.

The example-embedded-servlet-override has a replacement for that servlet, along with the embedded jetty main class to deploy the example-webapp. It has a system property check against use.server.class to demonstrate both behaviors, normal (isolated), and overridden (hole punch).

The output looks like this:

Normal/Isolated Behavior: (no system prop. or using -Duse.server.class=false)

Response Status: 200
Response Content Type: text/plain; charset=ISO-8859-1
[resp] Hello Test Class: com.company.foo.TestServlet 
       from ClassLoader WebAppClassLoader=2113433461@7df86f75

Overridden/Hole Punch Behavior: (using -Duse.server.class=true)

Response Status: 200
Response Content Type: text/html; charset=ISO-8859-1
[resp] <html><body>
[resp] <h1>Hello Server Impl</h1>
[resp] <p>Class: com.company.foo.TestServlet 
       from ClassLoader sun.misc.Launcher$AppClassLoader@63e68a2b</p>
[resp] </body></html>

Hope this helps you understand the nature of what you are working with.

Upvotes: 1

Jan
Jan

Reputation: 1297

OK EEColor, I've had a dig around in the code and I think we have 2 issues for which I've opened these bugs:

Now, bug #1 is the reason why even if you set a completely isolated classloader the webapp is magically able to still load the class. Bug #2 shouldn't really affect you, unless you start to play around with Joakim's suggestion of setting system and server classes, and even then you're probably ok, because the class exists in both the container classloader parent hierarchy and the webapp classloader and the bug only occurs when a system class is missing from the container classloader but present in the webapp classloader.

So, you can try using Joakim's suggestion of adding the class as a system class and that should work. Alternatively, you should also be able to simply set WebAppContext.setParentLoaderPriority(true) and that will also try the container classloader hierarchy first. Although be aware that setParentPriority(true) will apply to all classloading for the webapp, whilst Joakim's suggestion limits the different loading behaviour to just the class in question.

Jan

Upvotes: 2

Jan
Jan

Reputation: 1297

I can only assume that the class in question resides in WEB-INF/lib or WEB-INF/classes. No idea why you'd want to ensure that the webapp can't find this class, but try using a classloader that isn't an instance of WebAppClassLoader, like a new URLClassLoader with an empty list of urls and the rootLoader as its parent.

Jan

Upvotes: 0

Related Questions