SudeepShakya
SudeepShakya

Reputation: 621

Dynamically load jars using classloader in Java 17(migrating from Java 8 to Java 17)

I have classsloading code which is working fine in Java 8(it's a bit old code). Now we are trying to migrate from Java 8 to Java 17. While migrating to Java 17, we got an error which loads JARs at runtime. The message of the exception is :

java.lang.ClassCastException: class jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to class java.net.URLClassLoader (jdk.internal.loader.ClassLoaders$AppClassLoader and java.net.URLClassLoader are in module java.base of loader 'bootstrap')

So I found from documentation that :

New Class Loader Implementations JDK 9 and later releases maintain the hierarchy of class loaders that existed since the 1.2 release. However, the following changes have been made to implement the module system:

The application class loader is no longer an instance of URLClassLoader but, rather, of an internal class. It is the default loader for classes in modules that are neither Java SE nor JDK modules.

The extension class loader has been renamed; it is now the platform class loader. All classes in the Java SE Platform are guaranteed to be visible through the platform class loader.

Just because a class is visible through the platform class loader does not mean the class is actually defined by the platform class loader. Some classes in the Java SE Platform are defined by the platform class loader while others are defined by the bootstrap class loader. Applications should not depend on which class loader defines which platform class.

The changes that were implemented in JDK 9 may impact code that creates class loaders with null (that is, the bootstrap class loader) as the parent class loader and assumes that all platform classes are visible to the parent. Such code may need to be changed to use the platform class loader as the parent (see ClassLoader.getPlatformClassLoader).

The platform class loader is not an instance of URLClassLoader, but, rather, of an internal class.

The bootstrap class loader is still built-in to the Java Virtual Machine and represented by null in the ClassLoader API. It defines the classes in a handful of critical modules, such as java.base. As a result, it defines far fewer classes than in JDK 8, so applications that are deployed with -Xbootclasspath/a or that create class loaders with null as the parent may need to change as described previously.

Went through different articles like https://cgjennings.ca/articles/java-9-dynamic-jar-loading/, Java 9, compatability issue with ClassLoader.getSystemClassLoader and other examples but couldn't find exact solution.

My existing code in Java 8 for loading JARs at runtime is :

import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import com.immunesecurity.shared.lib.Log;
import com.immunesecurity.shared.lib.LogDetail;
import org.apache.commons.lang3.exception.ExceptionUtils;

public class ClasspathUtils {

    // Parameters
    private static final Class[] parameters = new Class[] { URL.class };

    /**
     * Adds the jars in the given directory to classpath
     * 
     * @param directory
     * @throws IOException
     */
    public static void addDirToClasspath(File directory) throws IOException {
        if (directory.exists()) {
            File[] files = directory.listFiles();
            for (int i = 0; i < files.length; i++) {
                File file = files[i];
                addURL(file.toURI().toURL());
            }
        } else {
            Log.warn.log(new LogDetail().toBuilder().fileName(MethodHandles.lookup().lookupClass().getName())
                    .methodName(Thread.currentThread().getStackTrace()[1].getMethodName())
                    .lineNumber(Thread.currentThread().getStackTrace()[1].getLineNumber())
                    .threadName(Thread.currentThread().getName())
                    .message("The directory \"" + directory + "\" does not exist!").build().buildLog());
        }
    }

    /**
     * Add URL to CLASSPATH
     * 
     * @param u
     *            URL
     * @throws IOException
     *             IOException
     */
    public static void addURL(URL u) throws IOException {

        URLClassLoader sysLoader = (URLClassLoader) ClassLoader
                .getSystemClassLoader();
        URL urls[] = sysLoader.getURLs();
        for (int i = 0; i < urls.length; i++) {
            if (urls[i].toString().equalsIgnoreCase(u.toString())) {
                Log.info.log(new LogDetail().toBuilder().fileName(MethodHandles.lookup().lookupClass().getName())
                        .message("URL " + u + " is already in the CLASSPATH").build().buildLog());
                return;
            }
        }
        Class sysclass = URLClassLoader.class;
        try {
            Method method = sysclass.getDeclaredMethod("addURL", parameters);
            method.setAccessible(true);
            method.invoke(sysLoader, new Object[] { u });
        } catch (Throwable t) {
            throw new IOException(
                    "Error, could not add URL to system classloader");
        }
    }
}

We use addDirToClasspath method which calls method addURL from the given directory to load all the JARs.

I need suggestions on how to make it work on Java 17 or re-write the classloading logic to be compatible with Java 17.

Upvotes: 2

Views: 3216

Answers (0)

Related Questions