Java Compiler API使用

引言

Java Compiler API 是 Java 提供的一套用于在运行时编译 Java 源代码的工具。Java Compiler API的最大应用场景之一是jsp页面的编译。Tomcat把jsp编译为java文件,然后再编译为class文件。

除了 JSP 编译,Java Compiler API 还广泛应用于:

  • 代码生成工具:如 Lombok、MapStruct 等。
  • 动态脚本引擎:在运行时动态加载和执行 Java 代码。
  • 热部署:在不重启应用的情况下更新代码。

编译磁盘源码

最简单的使用方法是获取JavaCompiler对象,然后编译一个java文件。

在使用 JavaCompiler.run(InputStream in, OutputStream out, OutputStream err, String... arguments) 方法时,很多开发者对前三个流参数和后面的命令行参数感到困惑。为了让你的代码更加健壮并便于调试,我们需要彻底理解它们的机制。该方法接收 3个固定流参数 + N个可变的命令行参数。

参数位置 参数名称 作用说明 推荐用法
参数 1 in (InputStream) 为编译器提供输入(如交互式输入)。 通常传 null,使用系统默认 System.in
参数 2 out (OutputStream) 接收编译器的正常输出信息(如 -verbose 信息)。 null 使用 System.out,或传自定义流捕获日志。
参数 3 err (OutputStream) 接收编译器的错误和警告信息。 null 使用 System.err,建议捕获此流以分析编译错误。
参数 4+ arguments (String...) 标准的 javac 命令行参数列表。 见下文详细拆解。

arguments 的执行逻辑:

  • "-sourcepath", "."
    • 含义:指定源文件的查找路径。
    • 原理:告诉编译器去哪里找引用的源文件(非 .class 文件,而是 .java 文件)。这里设置为当前目录。
    • 注意:如果不指定,默认在当前目录查找。但在复杂项目中,显式指定可以避免 找不到符号 的错误。
  • "fibonacci.java"
    • 含义:要编译的目标文件。
    • 关键点:这是参数列表中唯一的"非选项"参数(non-option argument)。编译器会把所有无法识别为选项(即不以 - 开头的参数)都视为要编译的源文件。
    • 扩展:这里可以传入多个 .java 文件路径。
  • "-d", "."
    • 含义:指定编译生成的 .class 文件的输出目录。
    • 原理:-d 是一个选项,它后面紧跟的 . 就是该选项的值(输出目录)。
    • 重要性:如果不加 -d,编译器默认会将 .class 文件生成在与 .java 文件同级的目录,这通常会污染源码目录。
java 复制代码
/**
 * 编译外部文件demo
 * 2025-12-22
 *
 * @author 醒过来摸鱼
 */
public class Main {
    public static void main(String[] args) throws MalformedURLException, NoSuchMethodException, ClassNotFoundException,
            InvocationTargetException, IllegalAccessException {
        final JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();

        final int result = systemJavaCompiler.run(null, null, null,
                "-sourcepath", ".", "fibonacci.java", "-d", ".");
        if (result != 0) {
            System.out.println("编译失败");
            return;
        }
        URL url = new URL("file://./");
        final Class<?> aClass = new URLClassLoader(new URL[]{url}).loadClass("cn.edu.ncepu.Fibonacci");
        final Method fibonacci = aClass.getDeclaredMethod("fibonacci", int.class);
        System.out.println(fibonacci.invoke(null, 5));
    }
}

追踪源码发现,最终调用的是com.sun.tools.javac.main.JavaCompiler#compile方法。

但是这种方式有局限性:

  • 只能编译已经存在磁盘里的java文件。
  • 是只能把编译结果存入磁盘文件。
  • 无法处理内存中的源代码或动态生成的代码。

编译内存源码

如果源码来源于网络、内存、或者其他地方,而编译后的字节码,不存储在磁盘,就需要用另外一种方式,这也是JAVA compiler API最难的地方。如果要编译任意来源的java源码,比如内存里的java代码,需要五大步骤。

步骤一 自定义JavaFileObject以支持内存源码

JavaFileObject即可以代表java源码,也可以代表java class文件。前两个步骤都是继承SimpleJavaFileObject类。

新建一个类继承SimpleJavaFileObject并重写getCharContent方法,以支持内存源码。

java 复制代码
/**
 *
 * 2025-12-22
 *
 * @author 醒过来摸鱼
 */
public class StringJavaSource extends SimpleJavaFileObject {

    // 存储源代码的字符串
    private final String code;
    /**
     * 构造函数
     * @param fullClassName 类的全限定名,例如 "com.example.Hello"
     * @param code      源代码字符串
     */
    public StringJavaSource(String fullClassName, String code) {
        super(getUri(fullClassName), Kind.SOURCE);
        this.code = code;
    }

    private static URI getUri(String fullClassName) {
        // 1. 提取单纯的类名 (去掉包路径)
        // 找到最后一个点,取后面的部分
        int dotIndex = fullClassName.lastIndexOf('.');
        String classNameOnly = (dotIndex == -1) ? fullClassName : fullClassName.substring(dotIndex + 1);


        // 2. 关键修改:URI 中只使用单纯的类名,不要带路径
        // 原来可能是: "string:///" + fullClassName + Kind.SOURCE.extension
        // 现在改为: "string:///" + classNameOnly + Kind.SOURCE.extension
        URI uri = URI.create("string:///" + classNameOnly + Kind.SOURCE.extension);
        return uri;
    }

    /**
     * 2. 核心重写方法
     * 当编译器需要读取源代码时,会调用这个方法
     * @param ignoreEncodingErrors 是否忽略编码错误
     * @return CharSequence 返回源代码字符序列
     */
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        // 直接返回内存中的字符串
        return code;
    }
}

步骤二 自定义JavaFileObject 以存储编译结果

如果要自定义一个类,来存储编译编译结果,就必须新建一个类继承SimpleJavaFileObject,然后重写openOutputStream方法。JDK自带的编译器会调用这个方法,将字节码,也就是byte数组写入这个流中。

java 复制代码
/**
 *
 * 2025-12-22
 *
 * @author 醒过来摸鱼
 */
public class ByteArrayJavaClass extends SimpleJavaFileObject {

    // 1. 定义一个输出流,用于接收编译器写入的字节码
    protected ByteArrayOutputStream outputStream;

    /**
     * 构造函数
     * @param className 类的全限定名,例如 "com.example.Hello"
     */
    public ByteArrayJavaClass(String className) {
        // 2. 调用父类构造器
        // URI: 定义一个假的 URI,协议用 "byte://" 或 "string://" 都可以,主要是为了符合规范
        // Kind: 指定这是一个 CLASS 文件(而不是 SOURCE 源文件)
        super(URI.create("byte:///" + className + Kind.CLASS.extension), Kind.CLASS);
    }

    /**
     * 3. 核心重写方法
     * 当编译器(JavaCompiler)需要写入字节码时,会调用这个方法获取输出流
     * @return OutputStream 编译器会把字节码写入这个流
     * @throws IOException
     */
    @Override
    public OutputStream openOutputStream() throws IOException {
        // 每次调用时,初始化或清空流
        outputStream = new ByteArrayOutputStream();
        return outputStream;
    }

    /**
     * 4. 提供给外部获取字节码的方法
     * 当编译完成后,我们通过这个方法拿到字节数组,用于加载类
     * @return 字节码数组
     */
    public byte[] getCompiledBytes() {
        if (outputStream == null) {
            return new byte[0];
        }
        return outputStream.toByteArray();
    }

}

第三步 自定义JavaFileManager

自定义一个JavaFileManager,重写getJavaFileForOutput方法。

但是写入编译结果之后,是很难找到编译结果的,所以使用一个HashMap去存储结果。

java 复制代码
public class CustomJavaFileManager extends ForwardingJavaFileManager {

    private HashMap<String, ByteArrayJavaClass> cache = new HashMap<>();
    /**
     * Creates a new instance of ForwardingJavaFileManager.
     *
     * @param fileManager delegate to this file manager
     */
    protected CustomJavaFileManager(JavaFileManager fileManager) {
        super(fileManager);
    }

    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
                                               FileObject sibling) {
        final ByteArrayJavaClass byteArrayJavaClass = new ByteArrayJavaClass(className);
        cache.put(className, byteArrayJavaClass); 
        return byteArrayJavaClass;
    }

    public HashMap<String, ByteArrayJavaClass> getCache() {
        return cache;
    }
}

第四步 自定义CassLoader

java 复制代码
public class MemoryClassLoader extends ClassLoader {

    private CustomJavaFileManager fileManager;

    public MemoryClassLoader(CustomJavaFileManager fileManager) {
        this.fileManager = fileManager;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 1. 从文件管理器的 Map 中获取编译好的类对象
        ByteArrayJavaClass javaClass = fileManager.getCache().get(name);
        if (javaClass == null) {
            // 如果找不到,尝试加载系统类(比如 Object, String 等)
            return super.findClass(name);
        }

        // 2. 获取字节码
        byte[] byteCode = javaClass.getCompiledBytes();

        // 3. defineClass 是 ClassLoader 的 native 方法,用于将字节码转换为 Class 对象
        return defineClass(name, byteCode, 0, byteCode.length);
    }
}

第五步 编译并反射

如果实现这种编译(有些地方叫动态编译)必须创建一个task,通过JavaCompiler#getTask方法来实现。

java 复制代码
/**
 * 编译测试代码
 * 2025-12-22
 *
 * @author 醒过来摸鱼
 */
public class CompileMain {
    public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, ClassNotFoundException {
        final JavaCompiler systemJavaCompiler = ToolProvider.getSystemJavaCompiler();

        try (final CustomJavaFileManager fileManager = new CustomJavaFileManager(
                systemJavaCompiler.getStandardFileManager(null, null, null))) {

            String javaCode = Files.readString(Paths.get("Fibonacci.java"));
            final String className = "cn.edu.ncepu.Fibonacci";
            final StringJavaSource source = new StringJavaSource(className, javaCode);
            final JavaCompiler.CompilationTask task = systemJavaCompiler.getTask(null, fileManager,
                    null, null, null, Arrays.asList(source));
            task.call();

            final Class<?> aClass = new MemoryClassLoader(fileManager).loadClass("cn.edu.ncepu.Fibonacci");
            final Method fibonacci = aClass.getDeclaredMethod("fibonacci", int.class);
            System.out.println(fibonacci.invoke(null, 5));
        }

    }
}
相关推荐
一勺菠萝丶8 分钟前
PDF24 转图片出现“中间横线”的根本原因与终极解决方案(DPI 原理详解)
java
姓蔡小朋友12 分钟前
Unsafe类
java
一只专注api接口开发的技术猿26 分钟前
如何处理淘宝 API 的请求限流与数据缓存策略
java·大数据·开发语言·数据库·spring
superman超哥26 分钟前
Rust 异步递归的解决方案
开发语言·后端·rust·编程语言·rust异步递归
荒诞硬汉27 分钟前
对象数组.
java·数据结构
期待のcode28 分钟前
Java虚拟机的非堆内存
java·开发语言·jvm
黎雁·泠崖28 分钟前
Java入门篇之吃透基础语法(二):变量全解析(进制+数据类型+键盘录入)
java·开发语言·intellij-idea·intellij idea
仙俊红31 分钟前
LeetCode484周赛T4
java