Java 9,与ClassLoader.getSystemClassLoader兼容性问题

19

以下代码将jar文件添加到构建路径,它在Java 8中运行良好。但是,在Java 9中,它会抛出与URLClassLoader转换相关的异常。有什么想法可以解决这个问题?最佳解决方案将对其进行编辑,以使其同时适用于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;
}

2
请查看此链接:https://community.oracle.com/thread/4011800 - Shadov
9
根据JDK 9版本的发布说明:“应用程序类加载器不再是java.net.URLClassLoader的实例(这是以前版本中从未规定的实现细节)。假设ClassLoader.getSystemClassLoader()返回一个URLClassLoader对象的代码需要更新。请注意,Java SE和JDK不提供API,供应用程序或库在运行时动态增加类路径。” 因此,我认为您应该解释您真正需要做什么,以便可以建议替代方案。 - Alan Bateman
2
你有一个接口和许多潜在的实现 - 这听起来像是服务和ServiceLoader的好选择,无需动态调整类路径即可完成。 - Alan Bateman
2
另外,我注意到使用ServiceLoader需要提供者添加特定配置的Meta-INF,但在我的情况下,由于我无法控制实现提供者,这是不可能的。 - Mostafa abdo
2
没有必要通过系统类加载器来提供动态加载的类,因此,您可以创建一个新的URLClassLoader。这甚至有一个优点,即当服务类不再需要时,它们可以被卸载。 - Holger
显示剩余4条评论
8个回答

11

您遇到了一个问题,即系统类加载器不再是URLClassLoader,请参阅Java 9迁移指南。正如ClassLoader::getSystemClassLoader的返回类型所示,这是一个实现细节,尽管有很多代码依赖它。

从评论中可以看出,您想要在运行时动态加载类。正如Alan Bateman指出的,在Java 9中无法通过附加到类路径来实现。

相反,您应该考虑为此创建一个新的类加载器。这样做的额外好处是,由于新类没有加载到应用程序类加载器中,因此您可以将其清除。如果您使用Java 9进行编译,则应该详细了解层次结构——它们为加载完全新的模块图提供了一个干净的抽象。


Nicolai,如果我使用现有的系统类加载器作为父类来创建一个自定义类加载器,那么这个JAR文件会被添加到模块图的哪个位置呢?会被添加到--module-path(显式或自动模块)还是--class-path(未命名模块)? - undefined

10

我之前曾遇到过这个问题。和许多人一样,我使用了类似于问题中的方法。

private static int AddtoBuildPath(File f)

在运行时动态添加路径到类路径中的更好方法。这个问题中的代码可能存在多个不好的实现方式:1)假设 ClassLoader.getSystemClassLoader() 返回的是一个 URLClassLoader,这是未记录的实现细节;2)使用反射将 addURL 设置为 public 也可能是另一个。

更干净的动态添加类路径的方法

如果您需要通过“Class.forName”加载类时使用额外的类路径 URL,则以下方法是一种干净、优雅且兼容的解决方案(适用于 Java 8 到 10):

1)通过扩展 URL 类加载器编写自己的类加载器,并具有公共的 addURL 方法。

public class MyClassloader extends URLClassLoader {

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

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

2)声明一个(单例/全局应用)对象,使用您的类加载器

private final MyClassloader classLoader;

通过实例化它

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

注意:系统类加载器是父级加载器。通过classLoader加载的类知道可以通过this.getClass().getClassLoader()加载哪些类,但反过来不行。

3)在需要时动态添加额外的类路径:

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

4) 通过你的单例类加载器实例化对象或你的应用程序:

cls = Class.forName(name, true, classLoader);
注意:由于类加载器在加载类之前会尝试委托给父类加载器(以及父类的父类),因此您必须确保要加载的类对父类加载器不可见,以确保它通过给定的类加载器加载。为了使这更清晰:如果您在系统类路径上有 ClassPathB ,然后稍后将 ClassPathB 和一些 ClassPathA 添加到自定义 classLoader 中,则 ClassPathB 下的类将通过系统类加载器加载,而 ClassPathA 下的类则对它们未知。但是,如果您从系统类路径中移除 ClassPathB,这些类将通过您的自定义 classLoader 加载,然后 ClassPathA 下的类对于那些在 ClassPathB 下的类是已知的。

5) 您可以考虑通过使用方法

setContextClassLoader(classLoader)

如果线程使用 getContextClassLoader,则会发生以下情况。


1
Java 11在运行时添加依赖JAR的问题 - 有什么解决方案吗? - Valsaraj Viswanathan

4

如果你只想读取当前的类路径,例如因为你想要使用与当前JVM相同的类路径启动另一个JVM,那么你可以执行以下操作:

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

默认情况下,$AppClassLoader类中的最终字段无法通过反射访问,需要向JVM传递额外的标志:

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

这是Scala吗?我知道它适用于响应,但考虑到这是一个Java问题,也许将其转换为Java语法会更好? - searchengine27
1
如果你只想获取当前类路径,为什么不直接这样说:return System.getProperty("java.class.path"); - colin

4

我收到了一个运行在Java 8中的Spring Boot应用程序,并被委派将其升级到Java 11版本。

遇到的问题:

导致的问题:java.lang.ClassCastException:jdk.internal.loader.ClassLoaders $ AppClassLoader(在模块:java.base中)无法转换为java.net.URLClassLoader(在模块:java.base中)

采用的解决方法:

创建一个类:

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;
    }
}

创建另一个类:

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);
    }
}

现在将其设置在当前线程中的主类中(就在应用程序的开头)。
Thread.currentThread().setContextClassLoader(new ClassLoaderConfig().getClassLoader());

希望这个解决方案对你有用!

MockClassLoader解决了类转换异常,但是加载的jar包中的类未找到。mockClassLoader.loadClass可以工作,但当类路径中的实用程序jar调用此运行时加载的jar类时,依赖关系似乎在运行时不可用。有什么想法吗? - Valsaraj Viswanathan
https://stackoverflow.com/questions/68380968/java-11-issue-with-adding-dependency-jars-at-runtime - Valsaraj Viswanathan

2

Shadov提到了一个帖子,它在Oracle社区上。这里有正确的答案:

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

那里提到的注意事项也很重要:
警告: java.util.ServiceLoader使用线程的ClassLoader上下文Thread.currentThread().setContextClassLoader(specialloader); java.sql.DriverManager遵守调用类的ClassLoader,而不是线程的ClassLoader。可以直接使用Class.forName("drivername", true, new URLClassLoader(urlarrayofextrajarsordirs).newInstance()创建Driver。 javax.activation使用线程的ClassLoader上下文(对于javax.mail很重要)。

0

还有这位的文章帮了我很大的忙。 虽然找不到那篇文章,但是在这里:https://github.com/CGJennings/jar-loader

在这里的指南中有一部分内容,里面有一个可以阅读他的指南并进行设置的jar文件。

我亲自试过了,下载了包含类文件的jar文件。

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;
}

我只是自己尝试将这些代码打包成一个包,然后使用 -javaagent:plugin......jar 选项启动应用程序类,然后调用这个函数。它不会改变我的类路径。我可能在这里漏掉了一些细节。

希望你能让它正常工作。


0

参考 Edi 的解决方案,这对我有用:

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);
        }

    }
}

在Eclipse中运行,您需要将VM参数设置为JUnit Launch/Debug Configuration。 通过命令行使用maven运行有两个选项:

选项1
将以下行添加到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>

选项2

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


-1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接