2. 概述
本教程将学习如何将包含Java源代码的字符串编译成可执行类并运行它。运行时编译代码有诸多应用场景:
- 动态代码生成 - 基于运行时才能获取或频繁变化的信息生成代码
- 热替换 - 无需重启应用即可替换代码逻辑
- 代码存储/注入 - 将业务逻辑存储在数据库中,按需检索执行。需谨慎处理,不用时可卸载自定义类
虽然编译类的方法有多种,但本文重点介绍JavaCompiler API。
3. 工具与策略
javax.tools
包包含了我们编译字符串所需的核心抽象。先了解关键组件及整体流程:
- 将代码传入JavaCompiler API
- FileManager提取源代码供编译器使用
- JavaCompiler编译并返回字节码
- 自定义ClassLoader将类加载到内存
本文不关注如何生成字符串格式的源码,直接使用硬编码示例:
final static String sourceCode =
"package com.baeldung.inmemorycompilation;\n"
+ "public class TestClass {\n"
+ "@Override\n"
+ " public void runCode() {\n"
+ " System.out.println(\"code is running...\");\n"
+ " }\n"
+ "}\n";
4. 表示我们的代码(源码和编译后)
首先需要将代码转换为FileManager可识别的格式。
Java源文件和类文件的顶层抽象是FileObject
。虽然没有现成的完整实现,但我们可以利用SimpleJavaFileObject
这个部分实现,只需重写关键方法。
4.1. 源码表示
对于源码,必须定义FileManager如何读取它。需重写getCharContent()
方法,该方法期望返回CharSequence
。由于代码已是字符串,直接返回即可:
public class JavaSourceFromString extends SimpleJavaFileObject {
private String sourceCode;
public JavaSourceFromString(String name, String sourceCode) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension),
Kind.SOURCE);
this.sourceCode = requireNonNull(sourceCode, "sourceCode must not be null");
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return sourceCode;
}
}
4.2. 编译后代码表示
对于编译后的代码,操作相反。需定义FileManager如何写入对象,即重写openOutputStream()
并提供OutputStream
实现。
使用ByteArrayOutputStream
存储字节码,并添加便捷方法供类加载时使用:
public class JavaClassAsBytes extends SimpleJavaFileObject {
protected ByteArrayOutputStream bos =
new ByteArrayOutputStream();
public JavaClassAsBytes(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/')
+ kind.extension), kind);
}
public byte[] getBytes() {
return bos.toByteArray();
}
@Override
public OutputStream openOutputStream() {
return bos;
}
}
4.3. 顶层接口
虽非必需,但为内存编译创建顶层接口很有用。主要好处:
- 明确ClassLoader返回的对象类型,便于安全转换
- 解决跨类加载器的对象相等性问题。不同类加载器加载的相同类可能无法相等,共享接口可弥合此鸿沟
Java预定义的函数式接口(如Function
、Runnable
、Callable
)很适合此模式。本文自定义接口:
public interface InMemoryClass {
void runCode();
}
调整源码实现该接口:
static String sourceCode =
"package com.baeldung.inmemorycompilation;\n"
+ "public class TestClass implements InMemoryClass {\n"
+ "@Override\n"
+ " public void runCode() {\n"
+ " System.out.println(\"code is running...\");\n"
+ " }\n"
+ "}\n";
5. 管理内存中的代码
代码格式已就绪,现在需要能操作它的FileManager
。标准FileManager
不适用,且JavaCompiler API未提供默认实现。
幸运的是,tools
包包含ForwardingJavaFileManager
,它将所有调用转发给底层FileManager
。可扩展此类并重写特定行为,类似之前处理SimpleJavaFileObject
的方式。
首先重写getJavaFileForOutput()
。JavaCompiler
通过此方法获取编译字节码的JavaFileObject
,需返回自定义的JavaClassAsBytes
实例:
public class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
// 标准构造函数
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
FileObject sibling) {
return new JavaClassAsBytes(className, kind);
}
}
还需存储编译后的类供后续ClassLoader
使用。使用Map
存储并提供访问方法:
public class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private Map<String, JavaClassAsBytes> compiledClasses;
public InMemoryFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
this.compiledClasses = new Hashtable<>();
}
@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, Kind kind, FileObject sibling) {
JavaClassAsBytes classAsBytes = new JavaClassAsBytes(className, kind);
compiledClasses.put(className, classAsBytes);
return classAsBytes;
}
public Map<String, JavaClassAsBytes> getBytesMap() {
return compiledClasses;
}
}
6. 加载内存中的代码
最后一步是创建类加载器。我们将为InMemoryFileManager
构建配套的ClassLoader
。
类加载本身是个复杂话题,超出本文范围。简单来说,我们将自定义ClassLoader
挂载到现有委托层次底部,直接从FileManager
加载类:
首先创建继承ClassLoader
的类,修改构造函数接收InMemoryFileManager
:
public class InMemoryClassLoader extends ClassLoader {
private InMemoryFileManager manager;
public InMemoryClassLoader(ClassLoader parent, InMemoryFileManager manager) {
super(parent);
this.manager = requireNonNull(manager, "manager must not be null");
}
}
然后重写findClass()
定义类查找位置。只需检查InMemoryFileManager
中的map:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Map<String, JavaClassAsBytes> compiledClasses = manager.getBytesMap();
if (compiledClasses.containsKey(name)) {
byte[] bytes = compiledClasses.get(name).getBytes();
return defineClass(name, bytes, 0, bytes.length);
} else {
throw new ClassNotFoundException();
}
}
⚠️ **注意:若找不到类则抛出ClassNotFoundException
**。因我们位于层次结构底部,此时找不到意味着全局找不到。
完成InMemoryClassLoader
后,需修改InMemoryFileManager
建立双向关系。先添加ClassLoader
成员并修改构造函数:
private ClassLoader loader;
public InMemoryFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
this.compiledClasses = new Hashtable<>();
this.loader = new InMemoryClassLoader(this.getClass().getClassLoader(), this);
}
然后重写getClassLoader()
返回InMemoryClassLoader
实例:
@Override
public ClassLoader getClassLoader(Location location) {
return loader;
}
现在可重用同一组FileManager
和ClassLoader
进行多次内存编译。
7. 整合所有部分
最后将各组件整合。通过单元测试展示:
@Test
public void whenStringIsCompiled_ThenCodeShouldExecute() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
InMemoryFileManager manager = new InMemoryFileManager(compiler.getStandardFileManager(null, null, null));
List<JavaFileObject> sourceFiles = Collections.singletonList(new JavaSourceFromString(qualifiedClassName, sourceCode));
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, diagnostics, null, null, sourceFiles);
boolean result = task.call();
if (!result) {
diagnostics.getDiagnostics()
.forEach(d -> LOGGER.error(String.valueOf(d)));
} else {
ClassLoader classLoader = manager.getClassLoader(null);
Class<?> clazz = classLoader.loadClass(qualifiedClassName);
InMemoryClass instanceOfClass = (InMemoryClass) clazz.newInstance();
Assertions.assertInstanceOf(InMemoryClass.class, instanceOfClass);
instanceOfClass.runCode();
}
}
执行测试后控制台输出:
code is running...
字符串源码中的方法成功执行!
8. 结论
本文学习了如何将Java源码字符串编译为可执行类并运行。
⚠️ 警告:操作类加载器需格外谨慎。Class
和ClassLoader
的双向关系使自定义类加载易引发内存泄漏,尤其使用第三方库时,它们可能在后台持有类引用。
本文源码可在GitHub获取。