使用 Java Compiler API 编译 Java 代码
1、简介
在 Java 开发中,编译是防止语法错误、类型不匹配和其他可能导致项目失败的问题的第一道防线。传统的工作流程依赖于手动编译,而现代应用程序则需要动态编译检查。例如:
- 实时验证学生提交的代码的教育平台
- 在部署前对生成的代码片段进行编译的 CI/CD 流水线
- 动态编译用户自定义逻辑的低代码工具
- 热代码重载系统可即时重载开发人员所做的更改
- 创建 Java 插件
Java Compiler
API 允许在 Java 应用程序中以编程式编译代码,从而实现上述应用场景。像 LeetCode 或 Codecademy 等平台可即时验证用户提交的代码。当用户点击 “运行” 时,后台会使用编译器 API 等工具编译代码段、检查错误并在沙盒环境中执行。程序化编译为这一即时反馈循环提供了动力。
本文将带你了解 Java Compiler
这一强大的工具。
2、Java Compiler API 概览
Java Compiler
API 位于 javax.tools
包内,提供对 Java 编译器的编程式访问。该 API 对于需要在运行时验证或执行代码的动态编译任务至关重要。
Compiler
API 的主要组件包括:
- JavaCompiler:启动编译任务的主编译器实例
- JavaFileObject:代表 Java 源文件或类文件,可以是内存文件,也可以是基于文件的文件
- StandardJavaFileManager:编译过程中管理输入和输出文件
- DiagnosticCollector:捕获错误和警告等编译诊断信息
这些组件协同工作,可在 Java 应用程序中实现灵活高效的动态编译。
3、实现编译
JDK 环境默认提供 Compiler
API,无需任何外部依赖。
现在,来看看如何编译内存中的 Java 代码。
3.1、在内存中创建源码
首先在内存中创建一个 Java 源码类:
// 继承自 SimpleJavaFileObject
public class InMemoryJavaFile extends SimpleJavaFileObject {
private final String code;
protected InMemoryJavaFile(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension),
Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
该类将 Java 代码表示为内存中的 JavaFileObject
,使我们能够直接将源码传递给编译器,而无需物理文件。
3.2、Compile API 的用法
创建一个工具方法来编译 Java 代码并捕获诊断信息:
private boolean compile(Iterable<? extends JavaFileObject> compilationUnits) {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JavaCompiler.CompilationTask task = compiler.getTask(
null,
standardFileManager,
diagnostics,
null,
null,
compilationUnits
);
boolean success = task.call();
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
System.out.println(diagnostic.getMessage(null));
}
return success;
}
compile()
方法通过 Compiler
API 处理 Java 源码编译。
首先使用 DiagnosticCollector
捕捉编译信息。
compiler.getTask() 调用接受六个参数:writer
为 null
(默认为 System.err
)、用于处理源文件的标准文件管理器、编译消息诊断收集器、编译器选项为 null
(使用默认值而非自定义选项)、注解处理类为 null
(不需要处理特定类型),以及所提供的包含要编译源文件的编译单元。执行 task.call()
后,该方法会记录所有诊断信息,并返回一个表示编译成功的布尔值。
3.3、编译字符串形式的代码
封装一个方法,直接从字符串编译 Java 代码:
public boolean compileFromString(String className, String sourceCode) {
JavaFileObject sourceObject = new InMemoryJavaFile(className, sourceCode);
return compile(Collections.singletonList(sourceObject));
}
如上,根据传递的 Java 代码创建 InMemoryJavaFile
类的实例,并将其封装在一个 List
中,以传递给 compile()
方法。
3.4、测试编译器
使用有效和无效代码段来进行测试:
@Test
void givenSimpleHelloWorldClass_whenCompiledFromString_thenCompilationSucceeds() {
String className = "HelloWorld";
String sourceCode = "public class HelloWorld {\n" +
" public static void main(String[] args) {\n" +
" System.out.println(\"Hello, World!\");\n" +
" }\n" +
"}";
boolean result = compilerUtil.compileFromString(className, sourceCode);
assertTrue(result, "Compilation should succeed");
// 检查是否创建了 class 文件
Path classFile = compilerUtil.getOutputDirectory().resolve(className + ".class");
assertTrue(Files.exists(classFile), "Class file should be created");
}
如上,该测试确认编译器是否处理并编译了有效的 Java 源码,并在预期输出目录中生成了可执行类文件。
接着,测试语法错误的代码:
@Test
void givenClassWithSyntaxError_whenCompiledFromString_thenCompilationFails() {
String className = "ErrorClass";
String sourceCode = "public class ErrorClass {\n" +
" public static void main(String[] args) {\n" +
" System.out.println(\"This has an error\")\n" +
" }\n" +
"}";
boolean result = compilerUtil.compileFromString(className, sourceCode);
assertFalse(result, "Compilation should fail due to syntax error");
Path classFile = compilerUtil.getOutputDirectory().resolve(className + ".class");
assertFalse(Files.exists(classFile), "No class file should be created for failed compilation");
}
由于编译失败,因此不会创建 .class
文件。
4、总结
本文介绍了 Java Compiler
API 及其用法,还介绍了如何编译内存中的源码、捕获诊断信息以及动态执行编译。
利用 Compiler
API,我们可以:
- 在 CI/CD 管道中实现编译工作流程自动化。
- 在应用中动态验证和执行用户定义的代码。
- 获取详细诊断信息,改进调试和错误处理。
无论是构建自动分级系统、插件系统还是动态 Java 执行工具,Java Compiler
API 都能提供强大而灵活的解决方案。
Ref:https://www.baeldung.com/java-compilation-compiler-api