在本文中,我们探讨了 Java 编译器 API (Java Compiler API)及其在程序化代码编译中的作用。我们学习了如何编译内存中的源代码、捕获诊断信息以及动态执行编译。
通过利用 Compiler API,我们可以:
- 在 CI/CD 管道、教育平台和低代码环境中自动化编译工作流程
- 在应用程序内动态验证并执行用户定义的代码
- 通过捕获详细的诊断来改进调试和错误处理
无论我们构建的是自动评分系统、插件系统还是动态 Java 执行工具,Java 编译器 API (Java Compiler API)都提供了强大而灵活的解决方案。
在 Java 开发中,编译是防止语法错误、类型不匹配和其他可能导致项目失败的问题的第一道防线。 传统的工作流程依赖于手动编译,而现代应用程序则需要动态编译检查。 例如
- 实时验证学生代码提交的教育平台
- 在部署之前编译生成的代码片段的 CI/CD 管道
- 动态编译用户定义逻辑的低代码工具
- 热代码重载系统可立即重新加载开发人员的更改
- 创建Java 插件
Java 编译器 API (Java Compiler API)允许在 Java 应用程序中以编程方式编译代码,从而实现了这些应用场景。
LeetCode 或 Codecademy 等平台可即时验证用户提交的代码。 当用户点击 "运行 "时,后台会使用编译器 API (Java Compiler API)等工具对代码段进行编译,检查错误并在沙盒环境中执行。 程序化编译为这一即时反馈循环提供了动力。
什么是Java编译器API(Java Compiler API)
Java 编译器 API 位于javax.tools包中,提供对 Java 编译器的编程访问。此 API 对于需要在运行时验证或执行代码的动态编译任务至关重要。
编译器 API ( Compiler API)的关键组件包括:
- JavaCompiler:启动编译任务的主编译器实例
- JavaFileObject:表示 Java 源文件或类文件,位于内存中或基于文件
- StandardJavaFileManager:管理编译过程中的输入和输出文件
- DiagnosticCollector:捕获编译诊断信息,例如错误和警告
这些组件协同工作,实现 Java 应用程序内灵活、高效的动态编译。
编译器 API ( Compiler API)在 JDK 环境中默认可用,无需任何外部依赖项。现在,让我们看看如何从.java文件编译内存中的 Java 代码。
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,使我们能够将源代码直接传递给编译器,而无需物理文件。
2. 编译 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()调用接受六个参数:null表示写入器(默认为System.err),一个用于处理源文件的标准文件管理器,一个用于编译消息的诊断收集器,一个 null表示编译器选项(使用默认值而非自定义标志),一个 null表示注解处理类(因为没有特定类型需要处理),以及一个包含待编译源文件的编译单元。执行task.call()后,该方法会记录所有诊断消息并返回一个布尔值,指示编译成功。
3. 从内存字符串编译
为了使编译更容易在客户端代码或测试用例中使用,让我们引入一个直接从String编译 Java 代码的包装方法:
public boolean compileFromString(String className, String sourceCode) { JavaFileObject sourceObject = new InMemoryJavaFile(className, sourceCode); return compile(Collections.singletonList(sourceObject)); }
|
在这里,我们创建之前的InMemoryJavaFile类的一个实例,并将其包装在单例列表中以传递给实际的compile()方法。
4. 测试编译器
现在我们已经实现了动态编译 Java 代码的方法,让我们使用有效和无效的代码片段对其进行测试。这将确认 API 能够正确识别语法错误并返回相应的诊断信息:
@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"); // Check if the class file was created 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文件,确认错误已被正确捕获。