Search code examples
javajax-rsjettyclassloaderresteasy

Jetty ServletContextHandler setClassLoader not working on every request thread


I tried to develop multiple webservices using RESTEasy and Jetty. Im planning to make each of the webservice to have its own set of JAR files which will be loaded from certain directory.

What I have done is I create custom ClassLoader like this

public class AppClassLoader extends ClassLoader{
static Logger log = Logger.getLogger(AppClassLoader.class.getName());

String libPath = "";

public AppClassLoader(String libPath) {
    this.libPath = libPath;
}

@Override
public Class loadClass(String name) throws ClassNotFoundException {
    Class clazz = findLoadedClass(name);
    
    if(clazz == null) {
        try {
            
            clazz = ClassLoader.getSystemClassLoader().loadClass(name);
            
            if(clazz == null) {
                clazz = getClass(name);
                
                if(clazz == null) {
                    throw new ClassNotFoundException();
                }
            }
            
            return clazz;
        }catch (ClassNotFoundException e) {
            // TODO: handle exception
            throw new ClassNotFoundException();
        }
    }else {
        return getSystemClassLoader().loadClass(name);
    }
}

private Class<?> getClass(String name) throws ClassNotFoundException {
    try {
        File dir = new File(this.libPath);
        
        if(dir.isDirectory()) {
            for(File jar : dir.listFiles()) {
                JarFile jarFile = new JarFile(jar.getPath());
                Enumeration<JarEntry> e = jarFile.entries();

                URL[] urls = { new URL("jar:file:" + jar.getPath()+"!/") };
                URLClassLoader cl = URLClassLoader.newInstance(urls);

                while (e.hasMoreElements()) {
                    JarEntry je = e.nextElement();
                    if(je.isDirectory() || !je.getName().endsWith(".class")){
                        continue;
                    }
                    
                    String className = je.getName().substring(0,je.getName().length()-6);
                    className = className.replace('/', '.');
                    
                    if(className.equals(name)) {
                        return cl.loadClass(className);
                    }
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
    
    return null;
}

And then what I did is assign this custom class loader into ServletContextHandler when I initialized Jetty and RestEasy to start the server, like below

 final int port = 8080;
    final Server server = new Server(port);

    // Setup the basic Application "context" at "/".
    // This is also known as the handler tree (in Jetty speak).
    final ServletContextHandler context = new ServletContextHandler(server, CONTEXT_ROOT);

    AppClassLoader  classLoader = new AppClassLoader("../apps/dummy/lib");    

    context.setClassLoader(classLoader);

    // Setup RESTEasy's HttpServletDispatcher at "/api/*".
    final ServletHolder restEasyServlet = new ServletHolder(new HttpServletDispatcher());
    
    restEasyServlet.setInitParameter("resteasy.servlet.mapping.prefix",APPLICATION_PATH);
    restEasyServlet.setInitParameter("javax.ws.rs.Application",App.class.getCanonicalName());
    
    final ServletHolder servlet = new ServletHolder(new HttpServletDispatcher());
    context.addServlet(restEasyServlet, APPLICATION_PATH + "/*");
    
    server.start();
    server.join();

And then in the jax-rs endpoint I have this code

 @Path("/")
 public class Dummy {
 Logger log = Logger.getLogger(Dummy.class.getName());

 @GET
 @Path("dummy")
 @Produces(MediaType.TEXT_PLAIN)
 public String test() {
     HikariConfig src = new HikariConfig();
     JwtMap jw = new JwtMap();
     
     return "This is DUMMY service : "+src.getClass().getName().toString()+" ### "+jw.getClass().getName();
 }}

I managed to start the server just fine, but when I tried to call the webservice, it return

java.lang.NoClassDefFoundError: com/zaxxer/hikari/HikariConfig

And then I see the classLoader used in the thread is not my custom class loader but the java standard class loader.

Which part that I did wrong here? Im so new to this classs loading stuff and I am not sure I literally understand how to used it.

Regards


Solution

  • By Default, ClassLoaders works on Parent first strategy. That means that Classes are searched and loaded through the sequence

    Bootstrap Class Loader -> Ext Class Loader -> System Class Loader -> Custom Class Loader

    So, with that approach Dummy Class is loaded using System Class Loader. Now, classes loaded through a ClassLoader only has the visibility to the classes from Parent ClassLoader and not vice versa. So, HikariConfig class is not visible to the Dummy Class. Hence, the exception.

    But, you should be able to load a class this way using the ServletContext Classloader which in your case is the Custom ClassLoader. Inject the Servlet Context in your Dummy class and then

    servletContext.getClassLoader().loadClass("com.zaxxer.hikari.HikariConfig");