【问题标题】:Class.forName(name, instantiation, classLoader) doesn't add class to classpathClass.forName(name, instantiation, classLoader) 不会将类添加到类路径
【发布时间】:2019-06-06 02:31:28
【问题描述】:

我在运行时生成.java 类文件,需要立即在代码中使用这些类。 所以我使用 Compiler API 编译 .java 类来制作 .class 文件:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

StandardJavaFileManager manager = compiler.getStandardFileManager(diagnostics, null, null);

File file = new File("path to file");

Iterable<? extends JavaFileObject> sources = manager.getJavaFileObjectsFromFiles(Arrays.asList(file));

CompilationTask task = compiler.getTask(null, manager, diagnostics, null, null, sources);

task.call();

manager.close();

然后我需要使用Class.forName() 获取对那些编译类的引用,但是如果我只调用Class.forName("com.foo.Bar") 它会抛出ClassNotFoundException,假设这是因为新的.class 文件没有添加到classpath 我寻找在运行时向classpath 添加类的方法。我遇到了一些与此概念相关的歧义:

1.这种方法(首先编译.java文件,使用编译器API,然后在第二步将其添加到类加载器)是否正确?能够立即使用代码中的类。

2. AFAIK,有两种方法可以在运行时将类动态加载到类路径中:一种是使用这样的自定义 ClassLoader:(我在编译时出错,因为它抱怨 BuiltinClassLoader没有addURL 方法):

    // Get the ClassLoader class
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    Class<?> clazz = cl.getClass();

    // Get the protected addURL method from the parent URLClassLoader class
    Method method = clazz.getSuperclass().getDeclaredMethod("addURL", new Class[] { URL.class });

    // Run projected addURL method to add JAR to classpath
    method.setAccessible(true);
    method.invoke(cl, new Object[] { cls });

另一种方法是使用Class.forName(name, instantiation, classLoader) 将一个类添加到类路径中(同时也提供类引用)。如上所述,由于出现编译器错误(Java 11),我无法应用第一种方法。 关于第二种方法,如果我们这样调用默认类加载器,Class.forName(name, instantiation, classLoader) 会将新类附加到classpath 吗? :

Class.forName("com.foo.Bar",true, ClassLoader.getSystemClassLoader());
// or:
Class.forName("com.foo.Bar",true, ApiHandler.class.getClassLoader());

这对我不起作用。上述 classLoader 参数的哪个变体 是正确的,为什么这些不起作用?是否必须创建自定义类加载器并将其传递给Class.forName()

3. 我在 Eclipse 项目的 src 文件夹中的 com.foo 包中制作 .java 文件。他们编译的.class 文件也在同一个文件夹中生成(使用编译器API)。当我使用 Eclipse 刷新项目时(右键单击项目 -> 刷新),相关的 .class 文件将在 target/classes 文件夹中生成,此时可以通过代码访问类(例如使用 Class.forName("com.foo.Bar).如果我在target/classes 文件夹中生成.class 文件(通过编译器API),是否可以识别这些类而无需将它们引入类路径?


更新:

我能够在我的代码中使用已编译的类,方法是将受人尊敬的.class 文件保存在项目的target/classes 文件夹中,如上面第三个问题中提到的)。 (通过在编译器的getTask() 方法中添加-d 选项:

Iterable<String> options = Arrays.asList( new String[] { "-d", System.getProperty("user.dir") + "/target/classes/"} );
.
.
.

CompilationTask task = compiler.getTask(null, manager, diagnostics, options, null, sources);

这样,似乎甚至不需要使用 classLoader 将类添加到类路径中;因为可以使用简单的Class.forName() 访问该类。 你如何解释这个?

Class<?> cls1 = Class.forName("com.foo.Bar");

当然还有通过ClassLoader的方式:

ClassLoader classLoader = ClassLoader.getSystemClassLoader(); 

Class<?> cls = classLoader.loadClass("com.foo.Bar");

【问题讨论】:

  • URIClassLoader?
  • @dan1st 如果你的意思是代码 sn-p 来制作自定义 classLoader,我从 here 得到代码,这里也是利用 URLClassLoader 制作自定义 ClassLoader 的表示:stackoverflow.com/a/1011126/190929
  • 我认为它可以帮助你。
  • @dan1st 谢谢,我不知道如何以不同于我之前评论中上述链接的方式使用它。
  • 您在“one is using a custom ClassLoader”之后的代码 sn-p 使用自定义类加载器,而是试图侵入系统类加载器。 @dan1st 建议使用自定义类加载器,创建一个新的URLClassLoader 并在其上调用loadClass 或使用Class.forName("com.foo.Bar", irrelevant, theNewlyCreatedLoader)。当您告诉编译器写入类路径中的目录时,该类可能变得可用,但前提是 a) 之前没有尝试加载它并且 a) 类路径上有一个目录(而不是 jar文件或模块存储)。

标签: java classloader dynamic-class-loaders


【解决方案1】:

最安全的解决方案是创建一个新的ClassLoader 实现并通过新的加载器加载生成的类,如this answer 所示。

但是从 Java 9 开始,如果尚未定义/加载具有该名称的类,则可以在您自己的上下文中定义类,即在同一个包中。如前所述,这样的类定义甚至可以取代类路径上的定义,只要它还没有被加载。因此,不仅后续的 Class.forName(String) 调用将解析为此类定义,甚至非反射引用。

这可以用下面的程序来演示。

class Dummy { // to make the compiler happy
    static native void extensionMethod();
}
public class CompileExtension {
    public static void main(String[] args) throws IOException, IllegalAccessException {
        // customize these, if you want, null triggers default behavior
        DiagnosticListener<JavaFileObject> diagnosticListener = null;
        Locale locale = null;

        // the actual class implementation, to be present at runtime only
        String class1 =
            "class Dummy {\n"
          + "    static void extensionMethod() {\n"
          + "        System.out.println(\"hello from dynamically compiled code\");\n"
          + "    }\n"
          + "}";
        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fm
          = c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
        // define where to store compiled class files - use a temporary directory
        fm.setLocation(StandardLocation.CLASS_OUTPUT,
            Set.of(Files.createTempDirectory("compile-test").toFile()));
        JavaCompiler.CompilationTask task = c.getTask(null, fm,
            diagnosticListener, Set.of(), Set.of(),
            Set.of(new SimpleJavaFileObject(
                URI.create("string:///Class1.java"), JavaFileObject.Kind.SOURCE) {
                    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                        return class1;
                    }
                }));

        if(task.call()) {
            FileObject fo = fm.getJavaFileForInput(
                StandardLocation.CLASS_OUTPUT, "Dummy", JavaFileObject.Kind.CLASS);
            // these are the class bytes of the first class
            byte[] classBytes = Files.readAllBytes(Paths.get(fo.toUri()));
            MethodHandles.lookup().defineClass(classBytes);

            Dummy.extensionMethod();
        }
    }
}

Dummy 定义的存在只是为了能够在编译时插入对所需方法的调用,而在运行时,动态定义的类会在方法被调用之前取而代之。

但要小心处理。如前所述,自定义类加载器是最安全的解决方案。通常,您应该通过始终存在且仅动态加载实现的接口创建对扩展的编译时引用,该接口可以在运行时强制转换为接口,然后通过接口定义的 API 使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-04-03
    • 2012-09-05
    • 2015-03-02
    • 2012-04-22
    • 2013-03-15
    • 1970-01-01
    • 2021-07-21
    相关资源
    最近更新 更多