Java深入解析篇六十一之编译器API(javax.tools)详解

Java编译器API(javax.tools)详解

一、概述与历史

1.1 什么是Java编译器API

Java编译器API(javax.tools包)是Java平台提供的以编程方式访问Java编译器的标准化接口体系。它允许开发者在应用程序内部:

  • 动态编译Java源码(源码可以来自文件、字符串、数据库或网络)
  • 收集和分析编译诊断信息(错误、警告)
  • 自定义编译过程中的文件管理(如将输出重定向到内存)
  • 与注解处理器协作进行代码生成和检查

1.2 历史背景

版本 时间 事件
JDK 5及以前 - 只能通过Runtime.exec("javac ...")调用外部进程,或使用非公开的com.sun.tools.javac
JDK 6 2006 JSR-199 正式引入 javax.tools 包,标准化编译器访问
JDK 9 2017 模块化(JPMS)支持,javax.tools归入jdk.compiler模块
JDK 10+ 2018+ 持续完善诊断信息,支持新语言特性
JDK 17 2021 密封类、模式匹配等新语法的编译支持

1.3 设计动机

在JSR-199之前,需要编程访问编译器的开发者面临两个困境:

  1. 进程调用方式Runtime.exec("javac ...")效率低、无法精细控制、诊断信息难以解析
  2. 内部API方式com.sun.tools.javac是非公开API,随版本变化不保证兼容

javax.tools提供了稳定的公共API,同时保持足够的灵活性支持高级场景。


二、核心概念

2.1 ToolProvider - 入口工厂

ToolProvider是获取编译器实例的静态工厂类:

java 复制代码
import javax.tools.*;

public class ToolProviderDemo {
    public static void main(String[] args) {
        // 获取系统Java编译器(需要JDK环境,JRE中返回null)
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            System.out.println("当前环境没有Java编译器(可能是JRE)");
            return;
        }
        System.out.println("编译器: " + compiler.getClass().getName());
        System.out.println("支持的源码版本: " + compiler.getSourceVersions());
    }
}

注意getSystemJavaCompiler() 在JRE环境中返回null。JDK 9+模块化环境下需确保jdk.compiler模块可访问。

2.2 JavaCompiler - 编译器核心接口

JavaCompiler接口提供两种编译方式:

java 复制代码
import javax.tools.*;
import java.io.*;
import java.nio.file.*;

public class SimpleCompileDemo {
    public static void main(String[] args) throws Exception {
        // 准备测试源文件
        Path sourceDir = Path.of("dynamic-src");
        Files.createDirectories(sourceDir);
        Files.writeString(sourceDir.resolve("Hello.java"),
            "public class Hello { public String greet() { return \"Hello!\"; } }");

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        // 方式1: 简单编译(类似命令行)
        // 参数: 输入流、输出流、错误流、编译参数
        int result = compiler.run(null, null, null,
            sourceDir.resolve("Hello.java").toString());
        System.out.println("编译结果: " + (result == 0 ? "成功" : "失败"));

        // 方式2: 高级编译(使用CompilationTask)
        // 见下节
    }
}

2.3 CompilationTask - 编译任务

CompilationTask是编译任务的核心接口,继承Callable<Boolean>,支持精细配置:

java 复制代码
import javax.tools.*;
import java.io.*;
import java.nio.file.*;
import java.util.*;

public class CompilationTaskDemo {
    public static void main(String[] args) throws Exception {
        // 创建测试源文件
        Path srcDir = Path.of("task-demo-src");
        Files.createDirectories(srcDir);
        Files.writeString(srcDir.resolve("Demo.java"),
            "public class Demo { public int add(int a, int b) { return a + b; } }");

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        StandardJavaFileManager fileManager =
            compiler.getStandardFileManager(diagnostics, null, null);

        // 获取源文件对象
        Iterable<? extends JavaFileObject> sources =
            fileManager.getJavaFileObjectsFromStrings(
                List.of(srcDir.resolve("Demo.java").toString()));

        // 创建编译任务
        // 参数: 输出流, 文件管理器, 诊断监听器, 选项, 注解处理类名, 源文件
        JavaCompiler.CompilationTask task = compiler.getTask(
            null,               // out: null表示使用标准输出
            fileManager,        // 文件管理器
            diagnostics,        // 诊断信息收集器
            List.of("-d", "task-demo-classes"),  // 编译选项:输出目录
            null,               // 注解处理要处理的类(null=自动发现)
            sources             // 源文件列表
        );

        // 执行编译
        Boolean success = task.call();
        System.out.println("编译" + (success ? "成功" : "失败"));

        // 输出诊断信息
        for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) {
            System.out.printf("[%s] %s:%d:%d - %s%n",
                d.getKind(),
                d.getSource() != null ? d.getSource().getName() : "unknown",
                d.getLineNumber(),
                d.getColumnNumber(),
                d.getMessage(null));
        }

        fileManager.close();
    }
}

2.4 Diagnostic - 诊断信息体系

诊断信息用于收集编译过程中的错误、警告和提示:

java 复制代码
import javax.tools.*;
import java.util.*;

public class DiagnosticDemo {
    public static void main(String[] args) {
        // Diagnostic.Kind 枚举值:
        // ERROR          - 编译错误,阻止编译成功
        // WARNING        - 编译警告
        // MANDATORY_WARNING - 强制性警告(如过时API)
        // NOTE           - 编译器提示信息
        // OTHER          - 其他

        // DiagnosticCollector 实现了 DiagnosticListener 接口
        // 将所有诊断信息收集到内部列表中
        DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();

        // 也可以自定义 DiagnosticListener 进行实时处理
        DiagnosticListener<JavaFileObject> listener = diagnostic -> {
            if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
                System.out.println("发现错误: 第" + diagnostic.getLineNumber()
                    + "行, 第" + diagnostic.getColumnNumber() + "列");
                System.out.println("  消息: " + diagnostic.getMessage(null));
                System.out.println("  代码: " + diagnostic.getCode());
            }
        };

        // 编译含错误的代码来观察诊断输出
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) return;

        // 使用内存编译(见下节)编译一段有错误的代码
        String badCode = "public class Bad { int x = \"hello\"; }";
        compileFromMemory("Bad", badCode, collector);

        System.out.println("\n=== 诊断信息汇总 ===");
        for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
            System.out.printf("类型=%s, 行=%d, 列=%d, 位置=%d-%d%n",
                d.getKind(), d.getLineNumber(), d.getColumnNumber(),
                d.getStartPosition(), d.getEndPosition());
            System.out.printf("  消息: %s%n", d.getMessage(null));
        }
    }

    /** 简化的内存编译方法(完整版见下节) */
    static void compileFromMemory(String className, String code,
                                   DiagnosticCollector<JavaFileObject> collector) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        JavaFileObject source = new StringJavaFileObject(className, code);
        JavaCompiler.CompilationTask task = compiler.getTask(
            null, null, collector, null, null, List.of(source));
        task.call();
    }

    /** 字符串源码文件对象 */
    static class StringJavaFileObject extends SimpleJavaFileObject {
        private final String code;
        StringJavaFileObject(String className, String code) {
            super(java.net.URI.create("string:///" + className.replace('.', '/')
                + Kind.SOURCE.extension), Kind.SOURCE);
            this.code = code;
        }
        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return code;
        }
    }
}

2.5 JavaFileObject与SimpleJavaFileObject - 文件抽象

JavaFileObject是对编译单元(源文件或类文件)的抽象,SimpleJavaFileObject是便捷基类:

java 复制代码
import javax.tools.*;
import java.net.URI;

/**
 * 内存中的Java源码文件对象
 * 将字符串形式的源码包装为编译器可识别的文件对象
 */
public class StringJavaFileObject extends SimpleJavaFileObject {
    private final String sourceCode;

    public StringJavaFileObject(String className, String sourceCode) {
        // URI格式: string:///包路径/类名.java
        super(URI.create("string:///" + className.replace('.', '/')
            + Kind.SOURCE.extension), Kind.SOURCE);
        this.sourceCode = sourceCode;
    }

    /** 编译器通过此方法获取源码内容 */
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return sourceCode;
    }
}
java 复制代码
import javax.tools.*;
import java.io.*;
import java.net.URI;

/**
 * 内存中的字节码输出文件对象
 * 编译结果不写入磁盘,而是保存在内存字节数组中
 */
public class MemoryJavaFileObject extends SimpleJavaFileObject {
    private final ByteArrayOutputStream bos = new ByteArrayOutputStream();

    public MemoryJavaFileObject(String className) {
        super(URI.create("mem:///" + className.replace('.', '/')
            + Kind.CLASS.extension), Kind.CLASS);
    }

    /** 编译器写入字节码时调用此方法 */
    @Override
    public OutputStream openOutputStream() {
        return bos;
    }

    /** 获取编译后的字节码 */
    public byte[] getBytes() {
        return bos.toByteArray();
    }
}

2.6 ForwardingJavaFileManager - 自定义文件管理

ForwardingJavaFileManager采用装饰器模式,允许在不改变标准文件管理的前提下拦截和定制文件操作:

java 复制代码
import javax.tools.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 内存文件管理器
 * 拦截编译输出,将生成的.class文件保存到内存中
 */
public class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    /** 保存编译结果: 类全限定名 -> 字节码 */
    private final Map<String, MemoryJavaFileObject> compiledClasses = new ConcurrentHashMap<>();

    protected MemoryFileManager(JavaFileManager delegate) {
        super(delegate);
    }

    /**
     * 拦截输出文件获取
     * 编译器要写入.class文件时会调用此方法
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className,
            JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        if (kind == JavaFileObject.Kind.CLASS) {
            MemoryJavaFileObject fileObject = new MemoryJavaFileObject(className);
            compiledClasses.put(className, fileObject);
            return fileObject;
        }
        return super.getJavaFileForOutput(location, className, kind, sibling);
    }

    /** 获取所有编译结果的字节码映射 */
    public Map<String, byte[]> getCompiledBytes() {
        Map<String, byte[]> result = new HashMap<>();
        compiledClasses.forEach((name, obj) -> result.put(name, obj.getBytes()));
        return result;
    }
}

2.7 完整内存编译与加载示例

以下示例展示了从字符串源码到可执行对象的完整流程:

java 复制代码
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 完整的运行时动态编译示例
 * 流程: 源码字符串 → 编译 → 内存保存字节码 → 自定义ClassLoader加载 → 反射调用
 */
public class RuntimeCompiler {

    /** 内存源码文件对象 */
    static class SourceObject extends SimpleJavaFileObject {
        private final String code;
        SourceObject(String name, String code) {
            super(URI.create("string:///" + name.replace('.', '/')
                + Kind.SOURCE.extension), Kind.SOURCE);
            this.code = code;
        }
        @Override
        public CharSequence getCharContent(boolean ignore) { return code; }
    }

    /** 内存字节码文件对象 */
    static class ClassObject extends SimpleJavaFileObject {
        private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ClassObject(String name) {
            super(URI.create("mem:///" + name.replace('.', '/')
                + Kind.CLASS.extension), Kind.CLASS);
        }
        @Override
        public OutputStream openOutputStream() { return bos; }
        byte[] getBytes() { return bos.toByteArray(); }
    }

    /** 内存文件管理器 */
    static class MemoryFM extends ForwardingJavaFileManager<JavaFileManager> {
        final Map<String, ClassObject> classes = new ConcurrentHashMap<>();
        MemoryFM(JavaFileManager fm) { super(fm); }

        @Override
        public JavaFileObject getJavaFileForOutput(Location loc, String name,
                JavaFileObject.Kind kind, FileObject sibling) {
            ClassObject co = new ClassObject(name);
            classes.put(name, co);
            return co;
        }
    }

    /** 内存类加载器 */
    static class MemoryClassLoader extends ClassLoader {
        private final Map<String, byte[]> classBytes;
        MemoryClassLoader(Map<String, byte[]> classBytes) {
            super(MemoryClassLoader.class.getClassLoader());
            this.classBytes = classBytes;
        }
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] bytes = classBytes.remove(name);
            if (bytes != null) {
                return defineClass(name, bytes, 0, bytes.length);
            }
            throw new ClassNotFoundException(name);
        }
    }

    /**
     * 编译并加载类
     * @param className 类全限定名
     * @param sourceCode Java源码
     * @return 编译后的Class对象
     */
    public static Class<?> compileAndLoad(String className, String sourceCode)
            throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            throw new IllegalStateException("需要JDK环境(非JRE)");
        }

        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        JavaFileManager stdFm = compiler.getStandardFileManager(diagnostics, null, null);
        MemoryFM memFm = new MemoryFM(stdFm);

        // 创建编译任务
        List<JavaFileObject> sources = List.of(new SourceObject(className, sourceCode));
        JavaCompiler.CompilationTask task = compiler.getTask(
            null, memFm, diagnostics, null, null, sources);

        // 执行编译
        boolean success = task.call();
        if (!success) {
            StringBuilder sb = new StringBuilder("编译失败:\n");
            for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) {
                sb.append(String.format("  [%s] 行%d: %s%n",
                    d.getKind(), d.getLineNumber(), d.getMessage(null)));
            }
            throw new RuntimeException(sb.toString());
        }

        // 从内存加载类
        Map<String, byte[]> bytesMap = new HashMap<>();
        memFm.classes.forEach((name, co) -> bytesMap.put(name, co.getBytes()));
        memFm.close();

        MemoryClassLoader loader = new MemoryClassLoader(bytesMap);
        return loader.loadClass(className);
    }

    public static void main(String[] args) throws Exception {
        // 动态编译一段代码
        String code = """
            public class DynamicGreeter {
                public String greet(String name) {
                    return "你好, " + name + "! 当前时间: " + System.currentTimeMillis();
                }
            }
            """;

        Class<?> clazz = compileAndLoad("DynamicGreeter", code);
        Object instance = clazz.getDeclaredConstructor().newInstance();
        String result = (String) clazz.getMethod("greet", String.class)
            .invoke(instance, "Java编译器API");
        System.out.println("动态编译结果: " + result);
    }
}

2.8 与注解处理器协作

编译任务可以直接注册注解处理器:

java 复制代码
import javax.tools.*;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import java.util.*;

public class AnnotationProcessorDemo {

    /** 自定义注解处理器:检查所有类是否有文档注释 */
    @SupportedAnnotationTypes("*")          // 处理所有注解
    @SupportedSourceVersion(SourceVersion.RELEASE_17)
    static class DocCheckProcessor extends AbstractProcessor {
        @Override
        public boolean process(Set<? extends TypeElement> annotations,
                               RoundEnvironment roundEnv) {
            // 遍历所有根元素(顶级类/接口)
            for (Element element : roundEnv.getRootElements()) {
                if (element.getKind() == ElementKind.CLASS) {
                    String docComment = processingEnv.getElementUtils()
                        .getDocComment(element);
                    if (docComment == null || docComment.isBlank()) {
                        processingEnv.getMessager().printMessage(
                            javax.tools.Diagnostic.Kind.WARNING,
                            "类缺少文档注释: " + element.getSimpleName(),
                            element);
                    }
                }
            }
            return false;  // 不消费注解
        }
    }

    public static void main(String[] args) throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

        String code = "public class NoDoc { public void doSomething() {} }";
        JavaFileObject source = new SimpleJavaFileObject(
            java.net.URI.create("string:///NoDoc.java"),
            JavaFileObject.Kind.SOURCE) {
            @Override
            public CharSequence getCharContent(boolean ignore) { return code; }
        };

        JavaCompiler.CompilationTask task = compiler.getTask(
            null, null, diagnostics, List.of("-proc:only"), null, List.of(source));

        // 关键:通过setProcessors注册注解处理器
        task.setProcessors(List.of(new DocCheckProcessor()));

        task.call();

        // 输出注解处理器产生的警告
        for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) {
            System.out.println("[" + d.getKind() + "] " + d.getMessage(null));
        }
    }
}

三、实际应用

3.1 在线编程/判题系统

java 复制代码
import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.util.*;
import java.util.concurrent.*;

/**
 * 简化版在线判题系统核心
 * 接收用户代码 → 编译 → 沙箱执行 → 验证输出
 */
public class OnlineJudge {

    // 限制执行时间
    private static final long TIMEOUT_MS = 5000;

    public static class JudgeResult {
        public boolean compileSuccess;
        public String compileError;
        public String output;
        public boolean passed;
    }

    /**
     * 判题入口
     * @param sourceCode 用户提交的Java源码
     * @param expectedOutput 期望输出
     */
    public static JudgeResult judge(String sourceCode, String expectedOutput) {
        JudgeResult result = new JudgeResult();
        try {
            // 1. 编译
            DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            JavaFileObject source = new SimpleJavaFileObject(
                URI.create("string:///Main.java"), JavaFileObject.Kind.SOURCE) {
                @Override
                public CharSequence getCharContent(boolean ignore) { return sourceCode; }
            };

            JavaCompiler.CompilationTask task = compiler.getTask(
                null, null, diagnostics, null, null, List.of(source));
            result.compileSuccess = task.call();

            if (!result.compileSuccess) {
                StringBuilder sb = new StringBuilder();
                diagnostics.getDiagnostics().forEach(d ->
                    sb.append(d.getMessage(null)).append("\n"));
                result.compileError = sb.toString();
                return result;
            }

            // 2. 执行(实际生产中需要安全沙箱)
            // 此处省略沙箱实现,仅演示流程
            result.output = "Hello, World!";  // 模拟输出
            result.passed = expectedOutput.equals(result.output.trim());

        } catch (Exception e) {
            result.compileError = e.getMessage();
        }
        return result;
    }

    public static void main(String[] args) {
        String code = """
            public class Main {
                public static void main(String[] args) {
                    System.out.println("Hello, World!");
                }
            }
            """;
        JudgeResult r = judge(code, "Hello, World!");
        System.out.println("编译: " + (r.compileSuccess ? "成功" : "失败"));
        System.out.println("通过: " + r.passed);
    }
}

3.2 规则引擎 - 动态规则编译

java 复制代码
import javax.tools.*;
import java.net.URI;
import java.util.*;
import java.util.function.Function;

/**
 * 基于Java编译器API的动态规则引擎
 * 规则以Java代码片段形式定义,运行时编译执行
 */
public class DynamicRuleEngine {

    /** 规则接口:输入上下文,返回布尔结果 */
    public interface Rule {
        boolean evaluate(Map<String, Object> context);
    }

    private final Map<String, Rule> rules = new HashMap<>();

    /**
     * 注册规则
     * @param ruleName 规则名
     * @param ruleExpression Java表达式,使用ctx.get("key")访问上下文
     */
    public void registerRule(String ruleName, String ruleExpression) throws Exception {
        String className = "Rule_" + ruleName;
        String code = String.format("""
            import java.util.Map;
            public class %s implements DynamicRuleEngine.Rule {
                public boolean evaluate(Map<String, Object> ctx) {
                    return %s;
                }
            }
            """, className, ruleExpression);

        // 编译并加载(复用RuntimeCompiler的逻辑)
        Class<?> clazz = RuntimeCompiler.compileAndLoad(className, code);
        rules.put(ruleName, (Rule) clazz.getDeclaredConstructor().newInstance());
    }

    /** 执行指定规则 */
    public boolean evaluate(String ruleName, Map<String, Object> context) {
        Rule rule = rules.get(ruleName);
        if (rule == null) throw new IllegalArgumentException("规则不存在: " + ruleName);
        return rule.evaluate(context);
    }

    public static void main(String[] args) throws Exception {
        DynamicRuleEngine engine = new DynamicRuleEngine();

        // 注册业务规则:金额大于1000 且 等级为VIP
        engine.registerRule("vip_discount",
            "((Number)ctx.get(\"amount\")).doubleValue() > 1000 " +
            "&& \"VIP\".equals(ctx.get(\"level\"))");

        // 注册业务规则:年龄>=18
        engine.registerRule("adult_check",
            "((Number)ctx.get(\"age\")).intValue() >= 18");

        // 测试
        Map<String, Object> ctx1 = Map.of("amount", 2000, "level", "VIP");
        Map<String, Object> ctx2 = Map.of("amount", 500, "level", "VIP");
        System.out.println("ctx1 VIP折扣: " + engine.evaluate("vip_discount", ctx1)); // true
        System.out.println("ctx2 VIP折扣: " + engine.evaluate("vip_discount", ctx2)); // false
    }
}

3.3 热部署核心机制

java 复制代码
/**
 * 热部署核心思路(伪代码级示例)
 * 实际框架如Spring Boot DevTools、JRebel更复杂
 */
public class HotDeployCore {
    private volatile ClassLoader currentLoader;
    private volatile Object currentInstance;

    /**
     * 热更新:重新编译并加载类
     * 关键:每次使用新的ClassLoader,使旧类可被GC
     */
    public synchronized void hotReload(String className, String newSource) throws Exception {
        // 1. 编译新源码
        Class<?> newClass = RuntimeCompiler.compileAndLoad(className, newSource);

        // 2. 创建新实例(旧实例等待GC回收)
        currentInstance = newClass.getDeclaredConstructor().newInstance();
        currentLoader = newClass.getClassLoader();

        // 3. 旧ClassLoader不再被引用后,其加载的类可被卸载
        System.out.println("热更新完成: " + className);
    }
}

3.4 代码静态分析

java 复制代码
import javax.tools.*;
import java.util.*;
import java.util.stream.*;

/**
 * 利用诊断信息进行代码质量分析
 * 收集所有错误和警告,生成报告
 */
public class CodeAnalyzer {

    public static class AnalysisReport {
        public List<String> errors = new ArrayList<>();
        public List<String> warnings = new ArrayList<>();
        public List<String> notes = new ArrayList<>();
        public int totalIssues;

        @Override
        public String toString() {
            return String.format("""
                === 代码分析报告 ===
                错误: %d
                警告: %d
                提示: %d
                总计: %d
                %s""",
                errors.size(), warnings.size(), notes.size(), totalIssues,
                errors.isEmpty() ? "" : "错误详情:\n" +
                    errors.stream().map(e -> "  " + e).collect(Collectors.joining("\n")));
        }
    }

    /**
     * 分析源码
     * @param sources 类名->源码的映射
     */
    public static AnalysisReport analyze(Map<String, String> sources) {
        AnalysisReport report = new AnalysisReport();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        List<JavaFileObject> sourceObjects = new ArrayList<>();
        sources.forEach((name, code) -> {
            sourceObjects.add(new SimpleJavaFileObject(
                java.net.URI.create("string:///" + name.replace('.', '/') + ".java"),
                JavaFileObject.Kind.SOURCE) {
                @Override
                public CharSequence getCharContent(boolean ignore) { return code; }
            });
        });

        // 使用 -Xlint:all 开启所有警告
        List<String> options = List.of("-Xlint:all");
        JavaCompiler.CompilationTask task = compiler.getTask(
            null, null, diagnostics, options, null, sourceObjects);
        task.call();

        for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) {
            String msg = String.format("[%s] %s:%d - %s",
                d.getKind(), d.getSource() != null ? d.getSource().getName() : "?",
                d.getLineNumber(), d.getMessage(null));
            switch (d.getKind()) {
                case ERROR -> report.errors.add(msg);
                case WARNING, MANDATORY_WARNING -> report.warnings.add(msg);
                case NOTE -> report.notes.add(msg);
                default -> {}
            }
            report.totalIssues++;
        }
        return report;
    }

    public static void main(String[] args) {
        Map<String, String> sources = Map.of(
            "Clean", "public class Clean { public int add(int a, int b) { return a+b; } }",
            "Dirty", """
                import java.util.*;
                public class Dirty {
                    public void process() {
                        List raw = new ArrayList();  // 原始类型警告
                        raw.add("hello");
                        System.out.println(raw.get(0));
                    }
                }
                """
        );
        System.out.println(analyze(sources));
    }
}

四、最佳实践与陷阱

4.1 最佳实践

实践 说明
优先使用CompilationTask compiler.run()更灵活,可收集诊断、设置选项
始终使用DiagnosticCollector 不要忽略诊断信息,编译失败时能定位问题
内存编译用ForwardingJavaFileManager 避免临时文件,性能更好
每次热部署使用新ClassLoader 确保旧类可被GC卸载,避免Metaspace泄漏
设置合理的classpath 动态编译时需要明确指定依赖路径
编译选项用--release JDK 9+推荐用--release 17替代-source 17 -target 17

4.2 常见陷阱

java 复制代码
// 陷阱1: JRE环境无编译器
// getSystemJavaCompiler() 在JRE中返回null,必须判空
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
    throw new UnsupportedOperationException("需要JDK环境");
}

// 陷阱2: 忘记关闭StandardJavaFileManager
// StandardJavaFileManager实现了Closeable,不关闭会泄漏文件句柄
try (StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null)) {
    // 使用文件管理器
}

// 陷阱3: 内存编译后忘记处理内部类
// 如外部类Outer有内部类Outer$Inner,两者都会产生字节码
// 加载时必须同时提供所有相关类的字节码

// 陷阱4: 类名与文件名不匹配
// SimpleJavaFileObject的URI中类名必须与实际声明一致
// 否则编译器报错: "类 Xxx 是公共的, 应在名为 Xxx.java 的文件中声明"

// 陷阱5: 多次编译同名类
// 同一ClassLoader不能加载同名类两次
// 热部署必须每次创建新ClassLoader

// 陷阱6: -proc选项影响编译速度
// 不需要注解处理时可用 -proc:none 跳过,加速编译
List<String> options = List.of("-proc:none");

4.3 性能建议

复制代码
编译性能优化要点:
├── 复用JavaCompiler实例(ToolProvider.getSystemJavaCompiler()可多次调用)
├── 批量编译:一次getTask传入多个源文件
├── -proc:none:无注解处理需求时跳过
├── 增量编译:只重新编译变更的文件
├── 内存编译:避免磁盘IO(ForwardingJavaFileManager)
└── 缓存:相同源码的编译结果可缓存

4.4 安全注意事项

复制代码
动态编译的安全风险:
├── 编译的代码可能包含恶意操作(文件删除、网络访问)
│   └── 解决: 配合SecurityManager(JDK17前)或独立进程/容器执行
├── 用户代码可能死循环或耗尽资源
│   └── 解决: 执行超时控制 + 资源限制
├── 反射访问系统类
│   └── 解决: 自定义ClassLoader限制可见类
└── 在线判题系统必须做沙箱隔离

五、版本演进

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    Java编译器API 版本演进                             │
├─────────┬───────────────────────────────────────────────────────────┤
│ JDK 6   │ JSR-199引入:                                             │
│ (2006)  │ · javax.tools包: JavaCompiler, ToolProvider              │
│         │ · CompilationTask, Diagnostic, DiagnosticCollector        │
│         │ · JavaFileObject, SimpleJavaFileObject                   │
│         │ · ForwardingJavaFileManager                              │
│         │ · 替代exec("javac")和非公开API                            │
├─────────┼───────────────────────────────────────────────────────────┤
│ JDK 9   │ 模块化影响:                                              │
│ (2017)  │ · javax.tools归入jdk.compiler模块                        │
│         │ · 新增编译选项: --module-path, --module-source-path       │
│         │ · StandardLocation新增MODULE_PATH等                      │
│         │ · 模块项目编译需额外配置                                  │
├─────────┼───────────────────────────────────────────────────────────┤
│ JDK 10+ │ · var类型推断等新语法编译支持                             │
│         │ · 诊断信息持续完善                                       │
├─────────┼───────────────────────────────────────────────────────────┤
│ JDK 17  │ LTS版本:                                                 │
│ (2021)  │ · 密封类、模式匹配instanceof编译支持                     │
│         │ · --release选项成熟(替代-source -target)               │
│         │ · 编译速度与诊断质量提升                                  │
├─────────┼───────────────────────────────────────────────────────────┤
│ JDK 21+ │ · 虚拟线程、记录模式等新语法                             │
│         │ · 编译基础设施持续优化                                    │
└─────────┴───────────────────────────────────────────────────────────┘

相关推荐
长友cy1 小时前
Qt官方示例D-BusListNames阅读
开发语言
蜡台1 小时前
Kotlin 零基础完整版实战教程|从语法入门到Android工程实战
android·开发语言·kotlin
jimy11 小时前
C++ 的 “移动语义”——Move Semantics
开发语言·c++
码云数智-园园1 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 优缺点对比
java·开发语言
en.en..2 小时前
竖线 |:管道、按位或、掩码组合
java·开发语言
techdashen2 小时前
Go Map 详解:键值对实际上是如何存储的
开发语言·后端·golang
程序猿乐锅2 小时前
【计算机组成原理 | 第六章】中央处理器 CPU
java·开发语言
大模型丫丫2 小时前
如何提升单体 Spring Boot 应用的并发数?
java·spring boot·后端
Terra.K2 小时前
ThreadLocal解决一个线程里面跨层传递问题
java·开发语言·后端