在Java代码内编译Java的方法

在本文中,我们探讨了 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文件,确认错误已被正确捕获。