如何通过编程方式编译和实例化Java类?

78

我在一个属性文件中存储了一个类名。我知道这些类会实现IDynamicLoad接口。如何动态地实例化这个类?

目前我的代码是:

     Properties foo = new Properties();
    foo.load(new FileInputStream(new File("ClassName.properties")));
    String class_name = foo.getProperty("class","DefaultClass");
    //IDynamicLoad newClass = Class.forName(class_name).newInstance();

newInstance方法只能加载已编译的.class文件吗?我如何加载一个未编译的Java类?


你可以做到,但需要使用JDK而不是JRE。https://medium.com/@davutgrbz/the-need-history-c91c9d38ec9 - Davut Gürbüz
3个回答

157

如何加载未编译的Java类?

您需要先进行编译。可以使用javax.tools API进行程序化操作。这仅需要在本地机器上安装JDK,并在JRE之上运行。

以下是一个基本的启动示例(忽略明显的异常处理):

// Prepare source somehow.
String source = "package test; public class Test { static { System.out.println(\"hello\"); } public Test() { System.out.println(\"world\"); } }";

// Save source in .java file.
File root = Files.createTempDirectory("java").toFile();
File sourceFile = new File(root, "test/Test.java");
sourceFile.getParentFile().mkdirs();
Files.write(sourceFile.toPath(), source.getBytes(StandardCharsets.UTF_8));

// Compile source file.
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
compiler.run(null, null, null, sourceFile.getPath());

// Load and instantiate compiled class.
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { root.toURI().toURL() });
Class<?> cls = Class.forName("test.Test", true, classLoader); // Should print "hello".
Object instance = cls.getDeclaredConstructor().newInstance(); // Should print "world".
System.out.println(instance); // Should print "test.Test@hashcode".

产生的结果如下

hello
world
test.Test@cafebabe

如果这些类实现已经在类路径中的某个接口,那么进一步使用会更加容易。

SomeInterface instance = (SomeInterface) cls.getDeclaredConstructor().newInstance();

否则,您需要使用反射 API来访问和调用(未知的)方法/字段。

话虽如此,与实际问题无关:

properties.load(new FileInputStream(new File("ClassName.properties")));

java.io.File 依赖于当前工作目录会导致可移植性问题。不要这样做。将该文件放在类路径中,并使用 ClassLoader#getResourceAsStream() 与类路径相关的路径。
properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("ClassName.properties"));

只是一个离题的问题。我按照你的方式加载属性时得到了null,但是当我使用Foo.class.getResourceAsStream()时,我得到了属性。你能帮我理解一下你的代码吗?谢谢。 - unj2
那个属性文件显然放在与Foo类相同的包中。正如所说,您需要指定一个类路径相关的路径,例如com/example/filename.properties。但是,如果您可以保证属性文件始终位于与Foo类相同的包中,则Class#getResourceAsStream()也可以。您只会错过将属性文件外部化到应用程序之外以便可以在不修改/重新打包应用程序的情况下进行修改的能力。 - BalusC
1
@BalusC:不好意思如果我有所冗余,但是我可以在编译Java文件之前指定classpath吗?如果可以的话,我需要您的指导。 - log N
2
是否有可能将类编译到内存中,获取生成的字节码作为结果字节数组并将其存储在其他地方(例如网络或数据库)以便自定义类加载器可以检索它?(而不是编写本地文件)。 - mydoghasworms

6
在与BalusC的答案相同的基础上,这里有一段代码来自我的kilim发布版,它更加自动化地包装了一些内容。https://github.com/kilim/kilim/blob/master/src/kilim/tools/Javac.java 它接受一个包含Java源代码的字符串列表, 提取包名和公共类/接口名称,并在tmp目录中创建相应的目录/文件层次结构。然后运行Java编译器并返回名称和类文件对(ClassInfo结构)的列表。
请随意使用这段代码。该软件采用MIT许可证。

如果编译失败会发生什么? - khatchad

5

如果您知道该类具有公共无参构造函数,则您的注释代码是正确的。您只需要将结果强制转换,因为编译器无法知道该类实际上会实现 IDynamicLoad。因此:

   IDynamicLoad newClass = (IDynamicLoad) Class.forName(class_name).newInstance();

当然,为了让这个类起作用,它必须被编译并存在于类路径中。

如果你想要从源代码动态编译一个类,那就是完全不同的事情了。


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