19

The following code adds jar file to the build path, it works fine with Java 8. However, it throws exception with Java 9, the exception is related to the cast to URLClassLoader. Any ideas how this can be solved? an optimal solution will edit it to work with both Java 8 & 9.

private static int AddtoBuildPath(File f) {
    try {
        URI u = f.toURI();
        URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Class<URLClassLoader> urlClass = URLClassLoader.class;
        Method method = urlClass.getDeclaredMethod("addURL", URL.class);
        method.setAccessible(true);
        method.invoke(urlClassLoader, u.toURL());
    } catch (NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException | MalformedURLException | IllegalAccessException ex) {
        return 1;
    }

    return 0;
}
Naman
  • 27,789
  • 26
  • 218
  • 353
Mostafa abdo
  • 616
  • 1
  • 8
  • 21
  • 2
    Check this link: https://community.oracle.com/thread/4011800 – Shadov Oct 11 '17 at 17:58
  • 9
    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.". So I think you should explain what you really need to do so that alternatives can be suggested. – Alan Bateman Oct 11 '17 at 18:06
  • @AlanBateman I am using it with what is called "HLA distributed simulation", the idea is you build your app according to IEEE standard interface and at runtime you specify a specific implementation of this interface. Please let me know if you have any other suggesting to achieve this. – Mostafa abdo Oct 11 '17 at 18:13
  • 2
    You have an interface and many potential implementations - this sounds like a good candidate for services and ServiceLoader, no need to dynamically adjust the class path to do that. – Alan Bateman Oct 11 '17 at 18:49
  • 1
    @AlanBateman But if I want to allow the user to select the implementation at runtime, then I still have to add it dynamically to classpath using URLClassLoader, right? – Mostafa abdo Oct 11 '17 at 19:11
  • 2
    Also, I noticed that using ServiceLoader requires that the providers add Meta-INF with specific configuration, this is not possible in my scenarios as I don't have a control over the implementation providers. – Mostafa abdo Oct 11 '17 at 19:52
  • 1
    There is no need for dynamically loaded classes to be available through the system class loader, hence, you may just create a new `URLClassLoader`. This has even the advantage that the service classes could get unloaded when they are not needed anymore. – Holger Oct 12 '17 at 08:39
  • 1
    @Holger is correct. It would be useful if the question were expanded to show how the implementation class is used. I assume there isn't anything else on the class path with static references to these implementation classes and so they must be loaded and used with reflection. – Alan Bateman Oct 12 '17 at 12:15
  • Is this issue resolved? – Valsaraj Viswanathan Sep 15 '21 at 12:11

8 Answers8

11

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.

Christian Fries
  • 16,175
  • 10
  • 56
  • 67
Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255
10

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.

Christian Fries
  • 16,175
  • 10
  • 56
  • 67
4

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
Edi
  • 621
  • 6
  • 17
  • Is this Scala? I understand it's appropriate for a response, but considering this is a Java question, maybe convert it to Java syntax? – searchengine27 Aug 16 '21 at 15:08
  • If you just want to get the current classpath, why not just say: `return System.getProperty("java.class.path");` – colin Jan 11 '22 at 17:26
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!!!

saptarshi
  • 151
  • 2
  • 7
  • MockClassLoader resolved the class cast exception but the classes in the loaded jars not found. mockClassLoader.loadClass works but the dependency seems not available at runtime when a utility jar in classpath invoked this runtime loaded jar class. Any thoughts? – Valsaraj Viswanathan Sep 15 '21 at 12:17
  • https://stackoverflow.com/questions/68380968/java-11-issue-with-adding-dependency-jars-at-runtime – Valsaraj Viswanathan Sep 15 '21 at 13:30
2

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).

PaL
  • 136
  • 2
  • 10
0

Referring to Edi's Solution this worked for me:

public final class IndependentClassLoader extends URLClassLoader {

    private static final ClassLoader INSTANCE = new IndependentClassLoader();

    /**
     * @return instance
     */
    public static ClassLoader getInstance() {

        return INSTANCE;
    }

    private IndependentClassLoader() {

        super(getAppClassLoaderUrls(), null);
    }

    private static URL[] getAppClassLoaderUrls() {

        return getURLs(IndependentClassLoader.class.getClassLoader());
    }

    private static URL[] getURLs(ClassLoader classLoader) {

        Class<?> clazz = classLoader.getClass();

        try {
            Field field = null;
            field = clazz.getDeclaredField("ucp");
            field.setAccessible(true);

            Object urlClassPath = field.get(classLoader);

            Method method = urlClassPath.getClass().getDeclaredMethod("getURLs", new Class[] {});
            method.setAccessible(true);
            URL[] urls = (URL[]) method.invoke(urlClassPath, new Object[] {});

            return urls;

        } catch (Exception e) {
            throw new NestableRuntimeException(e);
        }

    }
}

Running within Eclipse, you need to set VM Arguments to JUnit Launch/Debug Configuration. Running with maven via command line you have two options:

Option 1
Add following lines to pom.xml :

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.16</version>
                <configuration>
                    <argLine>--add-opens java.base/jdk.internal.loader=ALL-UNNAMED</argLine>
                </configuration>
            </plugin>

Option 2

run mvn test -DargLine="-Dsystem.test.property=--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"

Splioo
  • 370
  • 3
  • 11
0

There's also this guys article that helped me. I could not find the article but... here: https://github.com/CGJennings/jar-loader

Here's a part of guide inside there there's a jar at release you could read his guide & setup it up.

I just tried it myself download the jar file which include the class file

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;

public final class classname{

    public static void premain(String agentArgs, Instrumentation instrumentation) {
        loadedViaPreMain = true;
        agentmain(agentArgs,instrumentation);
    }

    public final static void addToClassPath(File jarfile)throws IOException{inst.appendToSystemClassLoaderSearch(new JarFile(jarfile));}
    public final static void agentmain(String agentArgs, Instrumentation instrumentation) {
      if (instrumentation == null){throw new NullPointerException("instrumentation");}
      if (inst == null) {inst = instrumentation;}
    }
    private static Instrumentation inst;
    private static boolean loadedViaPreMain = false;
}

I just try it out myself package these code as a package then start the application class with -javaagent:plugin......jar option then call this function.It doesn't change my classpath.I am probably missing some details here.

Hope you can make it work though.

-1

i found this, and worked for me.

String pathSeparator = Syste .getProperty("path.separator"); String[] classPathEntries = System.getProperty("java.class.path") .split(pathSeparator);

from the web site https://blog.codefx.org/java/java-11-migration-guide/#Casting-To-URL-Class-Loader