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));
        }

    }
}
相关推荐
晨晖22 小时前
二叉树遍历,先中后序遍历,c++版
开发语言·c++
wangchen_02 小时前
C/C++时间操作(ctime、chrono)
开发语言·c++
dazhong20122 小时前
Mybatis 敏感数据加解密插件完整实现方案
java·数据库·mybatis
Dev7z2 小时前
基于MATLAB HSI颜色空间的图像美颜系统设计与实现
开发语言·matlab
superman超哥2 小时前
仓颉语言中字符串常用方法的深度剖析与工程实践
开发语言·后端·python·c#·仓颉
TDengine (老段)2 小时前
TDengine 在智能制造领域的应用实践
java·大数据·数据库·制造·时序数据库·tdengine·涛思数据
Coder_Boy_2 小时前
基于 MQTT 的单片机与 Java 业务端双向通信全流程
java·单片机·嵌入式硬件
Asurplus2 小时前
Centos7安装Maven环境
java·centos·maven·apache·yum
癫狂的兔子2 小时前
【BUG】【Python】精确度问题
python·bug