Java 9, compatability issue with ClassLoader.getSystemClassLoader

Solution 1:

You've run into the fact that the system class loader is no longer a URLClassLoader. As indicated by ClassLoader::getSystemClassLoader's return type, this was an implementation detail, albeit one that a non-negligible amount of code relied upon.

Judging by the comments, you are looking for a way to dynamically load classes at run time. As Alan Bateman points out, this can not be done in Java 9 by appending to the class path.

You should instead consider creating a new class loader for that. This has the added advantage that you'll be able to get rid of the new classes as they are not loaded into the application class loader. If you're compiling against Java 9, you should read up on layers - they give you a clean abstraction for loading an entirely new module graph.

Solution 2:

I have stumbled over this issue a while ago. As many, I had used a method similar to that in the question

private static int AddtoBuildPath(File f)

to dynamically add paths to the classpath at runtime. The code in the question is probably bad style in multiple aspects: 1) assuming that ClassLoader.getSystemClassLoader() returns an URLClassLoader is an undocumented implementation detail and 2) using reflection to make addURL public is maybe another one.

Cleaner way to dynamically add classpaths

In case that you need to use the additional classpath URLs for class loading through „Class.forName“, a clean, elegant and compatible (Java 8 to 10) solution is the following:

1) Write your own class loader by extending URL classloader, having a public addURL method

public class MyClassloader extends URLClassLoader {

    public MyClassloader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public void addURL(URL url) {
        super.addURL(url);
    }
}

2) Declare a (singleton/app wide) object of your classloader

private final MyClassloader classLoader;

and instanciate it via

classLoader = new MyClassloader(new URL[0], this.getClass().getClassLoader());

Note: The system class loader is the parent. Classes loaded though classLoader know those who can be loaded through this.getClass().getClassLoader() but not the other way around.

3) Add additional classpaths whenever needed (dynamically):

File file = new File(path);
if(file.exists()) {
    URL url = file.toURI().toURL();
    classLoader.addURL(url);
}

4) Instanciate objects or your app though your singleton classloader via

cls = Class.forName(name, true, classLoader);

Note: Since class loaders try a delegation to the parent class loader prior loading a class (and the parent to its parent), you have to make sure that the class to load is not visible to the parent class loader to make sure that it is loaded through the given class loader. To make this clearer: if you have ClassPathB on your system class path and later add ClassPathB and some ClassPathA to your custom classLoader, then classes under ClassPathB will be loaded through the system classloader and classes under ClassPathA are not known to them. However, if you remove ClassPathB from you system class path, such classes will be loaded through your custom classLoader, and then classes under ClassPathA are known to those under ClassPathB.

5) You may consider passing your class loader to a thread via

setContextClassLoader(classLoader)

in case that thread uses getContextClassLoader.

Solution 3:

If you're just looking to read the current classpath, for example because you want to spin up another JVM with the same classpath as the current one, you can do the following:

object ClassloaderHelper {
  def getURLs(classloader: ClassLoader) = {
    // jdk9+ need to use reflection
    val clazz = classloader.getClass

    val field = clazz.getDeclaredField("ucp")
    field.setAccessible(true)
    val value = field.get(classloader)

    value.asInstanceOf[URLClassPath].getURLs
  }
}

val classpath =
  (
    // jdk8
    // ClassLoader.getSystemClassLoader.asInstanceOf[URLClassLoader].getURLs ++
    // getClass.getClassLoader.asInstanceOf[URLClassLoader].getURLs

    // jdk9+
    ClassloaderHelper.getURLs(ClassLoader.getSystemClassLoader) ++
    ClassloaderHelper.getURLs(getClass.getClassLoader)
  )

By default the final fields in the $AppClassLoader class cannot be accesed via reflection, an extra flag needs to be passed to the JVM:

--add-opens java.base/jdk.internal.loader=ALL-UNNAMED

Solution 4:

I was given a spring boot application that runs in Java 8. I had the task to upgrade it to Java 11 version.

Issue faced:

Caused by: java.lang.ClassCastException: jdk.internal.loader.ClassLoaders$AppClassLoader (in module: java.base) cannot be cast to java.net.URLClassLoader (in module: java.base)

Way around used:

Create a class:

import java.net.URL;

/**
 * This class has been created to make the code compatible after migration to Java 11
 * From the JDK 9 release notes: "The application class loader is no longer an instance of
 * java.net.URLClassLoader (an implementation detail that was never specified in previous releases).
 * Code that assumes that ClassLoader.getSytemClassLoader() returns a URLClassLoader object will
 * need to be updated. Note that Java SE and the JDK do not provide an API for applications or
 * libraries to dynamically augment the class path at run-time."
 */

public class ClassLoaderConfig {

    private final MockClassLoader classLoader;

    ClassLoaderConfig() {
        this.classLoader = new MockClassLoader(new URL[0], this.getClass().getClassLoader());
    }

    public MockClassLoader getClassLoader() {
        return this.classLoader;
    }
}

Create Another class:

import java.net.URL;
import java.net.URLClassLoader;

public class MockClassLoader extends URLClassLoader {

    public MockClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public void addURL(URL url) {
        super.addURL(url);
    }
}

Now set it in the current thread from your main class (Right at the beginning of your application)

Thread.currentThread().setContextClassLoader(new ClassLoaderConfig().getClassLoader());

Hope this solution works for your!!!

Solution 5:

Shadov pointed to a thread at the oracle community. There is the correct answer:

Class.forName("nameofclass", true, new URLClassLoader(urlarrayofextrajarsordirs));

The caveats mentioned there are also important:

Caveats:

java.util.ServiceLoader uses the thread's ClassLoader context Thread.currentThread().setContextClassLoader(specialloader);

java.sql.DriverManager does honors the calling class' ClassLoader, -not- the Thread's ClassLoader. Create Driver directly using Class.forName("drivername", true, new URLClassLoader(urlarrayofextrajarsordirs).newInstance();

javax.activation uses the thread's ClassLoader context (important for javax.mail).