如何使用自定义的类加载器进行后续的类加载?

Run*_*ekn 1 java classloader urlclassloader classnotfoundexception java-17

我有一个主要方法,用于创建自定义类加载器并用它实例化一个名为 Test 的类。

public class App {
    public static void main(String[] args) throws Exception {
        try {
            Class.forName("com.mycompany.app2.Test2"); // We ensure that Test2 is not part of current classpath
            System.err.println("Should have thrown ClassNotFound");
            System.exit(1);
        } catch (ClassNotFoundException e) {
            // ignore
        }

        String jar = "C:\\experiments\\classloader-test2\\target\\classloader-test2-1.0-SNAPSHOT.jar"; // Contains Test2
        URL[] classPaths = new URL[] { new File(jar).toURI().toURL() };
        ClassLoader classLoader = new URLClassLoader(classPaths, App.class.getClassLoader());

        Thread.currentThread().setContextClassLoader(classLoader);
        Class.forName("com.mycompany.app2.Test2", true, classLoader); // Check that custom class loader can find the wanted class
        Test test = (Test) Class.forName("com.mycompany.app.Test", true, classLoader).getDeclaredConstructor().newInstance();
        test.ex(); // This throws ClassNotFound for Test2
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,该类本身实例化另一个类,该类不是原始类路径的一部分,而是自定义类路径的一部分。

public class Test {
    public void ex() {
        new Test2().test();
    }
}
Run Code Online (Sandbox Code Playgroud)

根据我对类加载器的理解,由于测试是使用自定义类加载器创建的,所以其中的任何类加载都应该使用相同的加载器完成。但事实似乎并非如此。

Exception in thread "main" java.lang.NoClassDefFoundError: com/mycompany/app2/Test2
        at com.mycompany.app.Test.ex(Test.java:7)
        at com.mycompany.app.App.main(App.java:28)

Run Code Online (Sandbox Code Playgroud)

我需要在 main 方法中做什么才能使 Test#ex 工作而不更改 Test?我正在使用 Java 17。

Hol*_*ger 5

您创建URLClassLoaderusingApp.class.getClassLoader()作为父类加载器。因此,通过自定义类加载器加载的请求Test是通过父加载器解析的,最终得到与您在 main 方法中使用的\xe2\x80\x99d 完全相同的类Test.class

\n

您可以传递不同的父加载器,例如null表示引导加载器,以禁止Test通过父加载器解析类,但这会导致两种无益的情况之一

\n
    \n
  1. com.mycompany.app.Test如果自定义类加载器本身没有类,则加载尝试将会失败。

    \n
  2. \n
  3. 如果自定义类加载器有一个com.mycompany.app.Test类,即 inside classloader-test2-1.0-SNAPSHOT.jar,那么它将是Test与应用程序类加载器加载的 main 方法中引用的类不同的类。在这种情况下,类型转换(Test)将会失败。

    \n
  4. \n
\n

换句话说,Test你的 main 方法引用的类根本不会受到另一个不相关的类加载器的影响。

\n
\n

有一种完全不同的方法可能在某些情况下有效。当您只想注入一个新类时,不要创建新的类加载器。

\n
byte[] code;\ntry(var is = new URL("jar:file:C:\\\\experiments\\\\classloader-test2\\\\target\\\\" +\n    "classloader-test2-1.0-SNAPSHOT.jar!/com/mycompany/app2/Test2.class").openStream())\n{\n  code = is.readAllBytes();\n}\n\nMethodHandles.lookup().defineClass(code);\n\nTest test = new Test();\ntest.ex();\n
Run Code Online (Sandbox Code Playgroud)\n

这会将类添加到当前类加载上下文中,因此后续链接将会成功。有以下收获:

\n
    \n
  • 到目前为止,之前尚未尝试过链接此类
  • \n
  • 它仅适用于不依赖于其他缺失类的类(因为 \xe2\x80\x99s 没有类加载器将这些类解析为类路径之外的 jar 文件)。\n在某些情况下,当依赖关系是非循环或已解析时懒惰地,define如果您知道需要哪些类以及按什么顺序,则可以通过多次调用添加所有类。
  • \n
  • 该类必须位于同一个包中,否则,您\xe2\x80\x99d 必须将查找上下文移动到正确的包,并具有记录的限制
  • \n
\n
\n

将类添加到现有环境的一种完全不同的方法是通过 Java 代理,因为它们可以将 jar 文件添加到类路径

\n

  • @JohannesKuhn 实际上,甚至在 Java 9 之前就遇到了这个问题,因为有扩展加载器。如果没有访问器,您必须使用“ClassLoader.getSystemClassLoader().getParent()”来获取它。如今,这恰好对平台类加载器进行评估...... (3认同)