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