Search code examples
javajarclassloader

Unable to load classes which are not in the jars of the uber jar


I am trying to load classes from jar in jar (uber jar) and I have followed the way of such class loading from org.eclipse.jdt.internal.jarinjarloader. I have checked other resources and SO thread and I came up with the following URLStreamHandlerFactory implementation(it mostly the JBoss's implementation).

import java.lang.reflect.Constructor;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

public class JarInJarURLStreamHandlerFactory implements URLStreamHandlerFactory {

    private ClassLoader classLoader;

    public JarInJarURLStreamHandlerFactory(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    private static final String PACKAGE_PREFIX = "com.mycompany.project.classloader";
    private static final String PROTOCOL_PATH_PROPERTIES = "java.protocol.handler.pkgs";
    private static final String JDK_PACKAGE_PREFIX =  "sun.net.www.protocol";   

    private static Map<String, URLStreamHandler> handlerMap = Collections.synchronizedMap(new HashMap<String, URLStreamHandler>());
    private static ThreadLocal<String> createURLStreamHandlerProtocol = new ThreadLocal<String>();

    private String lastHandlerPackages = PACKAGE_PREFIX;
    private String[] handlerPackages = { PACKAGE_PREFIX, JDK_PACKAGE_PREFIX };      


    public URLStreamHandler createURLStreamHandler(final String protocol) {     
        URLStreamHandler handler = handlerMap.get(protocol);

        if (handler != null) {
            return handler;
        }

        String prevProtocol = createURLStreamHandlerProtocol.get();

        if (prevProtocol != null && prevProtocol.equals(protocol)) {
            return null;
        }

        createURLStreamHandlerProtocol.set(protocol);
        checkHandlerPackages();

        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

        for (int p = 0; p < handlerPackages.length; p++) {
            try {
                String classname = handlerPackages[p] + "." + protocol + ".Handler";
                Class<?> type = null;

                try {
                    type = contextClassLoader.loadClass(classname);
                } catch (ClassNotFoundException ignore1) {
                    try {
                        type = Class.forName(classname);
                    } catch (Exception ignore2) {
                    }                   
                }

                if (type != null) {
                    if (handlerPackages[p].equals(PACKAGE_PREFIX)) {
                        Constructor<?> constructor = type.getConstructor(ClassLoader.class);
                        handler = (URLStreamHandler) constructor.newInstance(classLoader);
                    } else {
                        handler = (URLStreamHandler) type.newInstance();
                    }

                    handlerMap.put(protocol, handler);

                }
            } catch (Throwable ignore) {
            }
        }

        createURLStreamHandlerProtocol.set(null);
        return handler;
    }

    private synchronized void checkHandlerPackages() {
        String packagePrefixList = AccessController.doPrivileged(new PrivilegedAction<String>() {

            @Override
            public String run() {
                return System.getProperty(PROTOCOL_PATH_PROPERTIES);
            }
        });

        if (packagePrefixList != null && !packagePrefixList.equals(lastHandlerPackages)) {
            StringTokenizer tokeninzer = new StringTokenizer(packagePrefixList, "|");
            Set<String> packageList = new HashSet<String>();

            while (tokeninzer.hasMoreTokens()) {
                String pkg = tokeninzer.nextToken().intern();
                packageList.add(pkg);

            }

            if (!packageList.contains(PACKAGE_PREFIX)) {
                packageList.add(PACKAGE_PREFIX);
            }

            handlerPackages = new String[packageList.size()];
            packageList.toArray(handlerPackages);
            lastHandlerPackages = packagePrefixList;
        }
    }
}

This is the Handler class:

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

import com.mycompany.project.classloader.connection.JarInJarURLConnection;
import com.mycompany.project.classloader.constants.JarInJarConstants;

public class Handler extends URLStreamHandler {

    private ClassLoader classLoader;

    public Handler(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    @Override
    protected URLConnection openConnection(URL url) throws IOException {
        return new JarInJarURLConnection(url, classLoader);
    }

    @Override
    protected void parseURL(URL url, String spec, int start, int limit) {       
        if (spec.startsWith(JarInJarConstants.INTERNAL_URL_PROTOCOL_WITH_COLON))  {
            String file = spec.substring(JarInJarConstants.INTERNAL_URL_PROTOCOL_WITH_COLON.length());
            setURL(url, JarInJarConstants.INTERNAL_URL_PROTOCOL, "", -1, null, null, file, null, null);
            return;
        }

        super.parseURL(url, spec, start, limit);
    }
}

I have modified the Handler to call the super.parseURL as said in the above mentioned SO thread.

The implementation of the URLConnection class is:

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;

import com.mycompany.project.classloader.constants.JarInJarConstants;

public class JarInJarURLConnection extends URLConnection {

    private ClassLoader classLoader;

    public JarInJarURLConnection(URL url, ClassLoader classLoader) {
        super(url);
        this.classLoader = classLoader;
    }

    @Override
    public void connect() throws IOException {

    };

    @Override
    public InputStream getInputStream() throws IOException {
        String fileName = URLDecoder.decode(getURL().getFile(), JarInJarConstants.UTF8_ENCODING);
        InputStream stream = classLoader.getResourceAsStream(fileName);
        return stream;
    }
}

The constants are:

public final class JarInJarConstants {

    private JarInJarConstants() {

    }

    public static final String LIBRARY_NAME = "Lib";
    public static final String LAUNCHER_CLASS = "Launcher-Class";
    public static final String MAIN_METHOD_NAME = "main";
    public static final String PATH_TO_MANIFEST = "META-INF/MANIFEST.MF";
    public static final String JAR_WITH_COLON = "jar:";
    public static final String JAR_EXTENSION = "jar";
    public static final String INTERNAL_URL_PROTOCOL_WITH_COLON = "jarinjar:";
    public static final String INTERNAL_URL_PROTOCOL = "jarinjar";
    public static final String PATH_SEPARATOR = "/";
    public static final String EXCLAMATION = "!";
    public static final String UTF8_ENCODING = "UTF-8";
}

And the Init class is:

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import com.mycompany.project.classloader.JarInJarClassLoader;
import com.mycompany.project.classloader.constants.JarInJarConstants;
import com.mycompany.project.classloader.factory.JarInJarURLStreamHandlerFactory;

public class Init {

    public static void main(String[] args) throws IOException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();    
        ManifestEntry manifestEntry = getManifestEntry(contextClassLoader);;
        String jarDirectoryName = manifestEntry.jarDirectoryName;
        String launcherClassName = manifestEntry.launcherClassName;

        List<String> jarFiles = getJarFiles(contextClassLoader, jarDirectoryName);

        ClassLoader parentClassLoader = Init.class.getClassLoader();
        URLStreamHandlerFactory urlStreamHandlerFactory = new JarInJarURLStreamHandlerFactory(parentClassLoader);
        URL.setURLStreamHandlerFactory(urlStreamHandlerFactory);

        URL[] urls = new URL[jarFiles.size()];
        int idx = 0;

        for(String jarFile : jarFiles) {
            urls[idx++] = new URL(JarInJarConstants.INTERNAL_URL_PROTOCOL_WITH_COLON + jarDirectoryName + JarInJarConstants.PATH_SEPARATOR + jarFile);
        }               

        ClassLoader jarInJarClassLoader = new JarInJarClassLoader(urls, null, urlStreamHandlerFactory);
        Thread.currentThread().setContextClassLoader(jarInJarClassLoader);

        Class<?> clazz = Class.forName(launcherClassName, true, jarInJarClassLoader);
        Method main = clazz.getMethod(JarInJarConstants.MAIN_METHOD_NAME, new Class[]{args.getClass()});
        main.invoke(null, new Object[]{args});
    }

    private static ManifestEntry getManifestEntry(ClassLoader contextClassLoader) throws IOException {
        URL url = contextClassLoader.getResource(JarInJarConstants.PATH_TO_MANIFEST);
        InputStream stream = url.openStream();
        Manifest manifest = new Manifest(stream);
        Attributes mainAttributes = manifest.getMainAttributes();
        String jarDirectoryName = mainAttributes.getValue(JarInJarConstants.LIBRARY_NAME);
        String launcherClassName = mainAttributes.getValue(JarInJarConstants.LAUNCHER_CLASS);
        return new ManifestEntry(jarDirectoryName, launcherClassName);
    }

    private static List<String> getJarFiles(ClassLoader contextClassLoader, String jarDirectoryName) throws UnsupportedEncodingException, IOException {
        URL dirURL = contextClassLoader.getResource(jarDirectoryName);
        String jarPath = dirURL.getPath().substring(JarInJarConstants.JAR_WITH_COLON.length() + 1, dirURL.getPath().indexOf(JarInJarConstants.EXCLAMATION));
        JarFile jar = new JarFile(URLDecoder.decode(jarPath, JarInJarConstants.UTF8_ENCODING));
        Enumeration<JarEntry> entries = jar.entries();
        List<String> jarFiles = new ArrayList<String>();

        while (entries.hasMoreElements()) {
            String name = entries.nextElement().getName();

            if (name.startsWith(jarDirectoryName) & name.endsWith(JarInJarConstants.JAR_EXTENSION)) {
                jarFiles.add(name.substring((jarDirectoryName + JarInJarConstants.PATH_SEPARATOR).length()));
            }
        }

        return jarFiles;
    }

    private static class ManifestEntry {
        String jarDirectoryName;
        String launcherClassName;

        public ManifestEntry(String jarDirectoryName, String launcherClassName) {
            this.jarDirectoryName = jarDirectoryName;
            this.launcherClassName = launcherClassName;
        }               
    }
}

Now the jar structure is:

enter image description here

The 3rd party libraries are bundled in the lib folder and the classes of my project is in the com folder.

I am getting java.lang.ClassNotFoundException to load the main class, but this class is present in the com folder. If I package my classes of my project as jar and placed inside the lib then it works. But I want to have only the 3rd party jars in the lib.

I was wondering if I load all the java handlers of sun.net.www.protocol package why I am getting this error?

Also the Init class is in the same package where my main class is, but Init is loaded by the ClassLoader but why the main class is not. Any suggestion would be very helpful.


Solution

  • You try to load the main class with your JarInJarClassLoaderwhich is an UrlClassloader
    but your classpath is not inside the array of URLs which you pass to your loader.
    That is the reason that all jars could be loaded, but not your main class,if the main class is not inside the jar .
    Your Init class could be loaded, because it is loaded by the current threads class loader or its parents.

    So you have two choices
    Add your classfolder to the URL array

    URL[] urls = new URL[jarFiles.size() + 1];
    int idx = 0;
    for(String jarFile : jarFiles) {
        urls[idx++] = new URL(JarInJarConstants.INTERNAL_URL_PROTOCOL_WITH_COLON + jarDirectoryName + JarInJarConstants.PATH_SEPARATOR + jarFile);
    }        
    urls[urls.length-1] = new URL("file:///C:/Users/TapasB/<INSERT CORRECT PATH>/com/");
    

    Or use another classloader for loading the main class
    e.g the one used to load the Init class

    Class<?> clazz = Class.forName(launcherClassName, true, Init.class.getClassLoader());