在Java生态中,我们习惯于用
|
1
|
javac
|
命令行编译源码,然后用
|
1
|
java
|
运行字节码。但Java标准库其实内置了一套完整的编译器API——
|
1
|
javax.tools.JavaCompiler
|
,它允许你在运行时动态编译Java源码。这个能力在代码生成、规则引擎、插件系统、REPL实现等场景下极为强大,却很少被系统性地讨论。本文将从底层API入手,逐步构建一个生产级的动态编译框架,并探索如何用它实现灵活的插件化架构。

一、JavaCompiler API核心概念
|
1
|
javax.tools.JavaCompiler
|
是JDK 6引入的标准API,它封装了JDK内置的编译器(通常是
|
1
|
javac
|
的实现)。获取编译器实例非常简单:
1
2
3
4
5
6
7 import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("未找到系统Java编译器,请确保使用JDK而非JRE运行");
}
这里有个经典坑:必须用JDK运行,不能用JRE。
|
1
|
ToolProvider.getSystemJavaCompiler()
|
在JRE环境下返回
|
1
|
null
|
,因为JRE不包含编译器实现。如果你在容器化环境中部署,基础镜像要用
|
1
|
openjdk:17-jdk
|
而不是
|
1
|
openjdk:17-jre
|
。
编译器API的三个核心组件是:
- JavaFileManager:管理编译输入输出的文件抽象层,决定源码从哪读取、字节码输出到哪
- JavaFileObject:表示一个编译单元(源文件或class文件),可以来自磁盘、内存或网络
- DiagnosticListener:编译错误和警告的监听器,用于收集诊断信息
1.1 最简编译示例
从最简单的场景开始——编译磁盘上的Java文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 import javax.tools.*;
import java.io.File;
import java.util.Collections;
public class SimpleCompilerDemo {
public static void main(String[] args) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 标准文件管理器
StandardJavaFileManager fileManager =
compiler.getStandardFileManager(null, null, null);
// 指定要编译的源文件
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjects(new File("src/com/example/DynamicService.java"));
// 编译选项
List<String> options = List.of("-classpath", System.getProperty("java.class.path"));
// 执行编译
boolean success = compiler.getTask(
null, // Writer for additional output
fileManager, // FileManager
null, // DiagnosticListener (null = default)
options, // Compiler options
null, // Names of classes for annotation processing
compilationUnits // Compilation units to compile
).call();
System.out.println("编译结果: " + (success ? "成功" : "失败"));
fileManager.close();
}
}
|
1
|
getTask()
|
返回一个
|
1
|
JavaCompiler.CompilationTask
|
对象(实现了
|
1
|
Callable<Boolean>
|
),调用
|
1
|
call()
|
即可执行编译。这种设计允许你在提交编译任务后灵活控制执行时机。
二、内存中编译:突破磁盘依赖
磁盘文件编译只是基础操作。JavaCompiler API真正的威力在于内存中编译——源码来自内存字符串,编译后的字节码也输出到内存,整个过程不涉及任何磁盘I/O。这对运行时代码生成场景至关重要。

2.1 自定义JavaFileObject:字符串源码输入
要让编译器读取内存中的源码,我们需要自定义
|
1
|
JavaFileObject
|
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 import javax.lang.model.element.NestingKind;
import javax.tools.JavaFileObject;
import java.io.*;
import java.net.URI;
public class StringJavaFileObject implements JavaFileObject {
private final String binaryName;
private final String sourceCode;
private final URI uri;
public StringJavaFileObject(String binaryName, String sourceCode) {
this.binaryName = binaryName;
this.sourceCode = sourceCode;
this.uri = URI.create("string:///" + binaryName.replace('.', '/') + ".java");
}
@Override
public URI toUri() { return uri; }
@Override
public String getName() { return binaryName; }
@Override
public InputStream openInputStream() throws IOException {
return new ByteArrayInputStream(sourceCode.getBytes("UTF-8"));
}
@Override
public OutputStream openOutputStream() throws IOException {
throw new UnsupportedOperationException("这是源码文件对象,不支持输出");
}
@Override
public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
return new StringReader(sourceCode);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return sourceCode;
}
@Override
public Writer openWriter() throws IOException {
throw new UnsupportedOperationException("这是源码文件对象,不支持写入");
}
@Override
public long getLastModified() { return 0L; }
@Override
public boolean delete() { return false; }
@Override
public Kind getKind() { return Kind.SOURCE; }
@Override
public boolean isNameCompatible(String simpleName, Kind kind) {
return kind == Kind.SOURCE
&& simpleName.equals(binaryName.substring(binaryName.lastIndexOf('.') + 1));
}
@Override
public NestingKind getNestingKind() { return NestingKind.TOP_LEVEL; }
@Override
public Modifier getAccessLevel() { return Modifier.PUBLIC; }
}
2.2 自定义JavaFileManager:字节码输出到内存
默认的
|
1
|
StandardJavaFileManager
|
会把编译结果写入磁盘class文件。我们需要一个自定义的FileManager,把字节码拦截到内存中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.util.*;
public class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final Map<String, ByteJavaFileObject> classBytes = new HashMap<>();
protected MemoryFileManager(JavaFileManager fileManager) {
super(fileManager);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, JavaFileObject.Kind kind, FileObject sibling) {
ByteJavaFileObject fileObject = new ByteJavaFileObject(className, kind);
classBytes.put(className, fileObject);
return fileObject;
}
@Override
public ClassLoader getClassLoader(Location location) {
return new MemoryClassLoader(classBytes);
}
public Map<String, byte[]> getClassBytes() {
Map<String, byte[]> result = new HashMap<>();
classBytes.forEach((k, v) -> result.put(k, v.getBytes()));
return result;
}
// 字节码输出到ByteArrayOutputStream
static class ByteJavaFileObject implements JavaFileObject {
private final String className;
private final Kind kind;
private ByteArrayOutputStream outputStream;
ByteJavaFileObject(String className, Kind kind) {
this.className = className;
this.kind = kind;
this.outputStream = new ByteArrayOutputStream();
}
@Override
public OutputStream openOutputStream() {
outputStream = new ByteArrayOutputStream();
return outputStream;
}
byte[] getBytes() { return outputStream.toByteArray(); }
@Override public URI toUri() {
return URI.create("bytes:///" + className.replace('.','/'));
}
@Override public String getName() { return className; }
@Override public InputStream openInputStream() {
return new ByteArrayInputStream(outputStream.toByteArray());
}
@Override public Kind getKind() { return kind; }
@Override public boolean isNameCompatible(String s, Kind k) { return kind == k; }
@Override public NestingKind getNestingKind() { return NestingKind.TOP_LEVEL; }
@Override public Modifier getAccessLevel() { return Modifier.PUBLIC; }
@Override public long getLastModified() { return 0L; }
@Override public boolean delete() { return false; }
@Override public Reader openReader(boolean b) { throw new UnsupportedOperationException(); }
@Override public CharSequence getCharContent(boolean b) { throw new UnsupportedOperationException(); }
@Override public Writer openWriter() { throw new UnsupportedOperationException(); }
}
}
2.3 内存ClassLoader:动态加载编译产物
编译后的字节码存储在内存中,我们需要一个自定义ClassLoader来加载它们:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 public class MemoryClassLoader extends ClassLoader {
private final Map<String, byte[]> classBytes;
public MemoryClassLoader(Map<String, ByteJavaFileObject> classFileObjects) {
this.classBytes = new HashMap<>();
classFileObjects.forEach((k, v) -> this.classBytes.put(k, v.getBytes()));
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = classBytes.get(name);
if (bytes != null) {
// 从内存中定义类
return defineClass(name, bytes, 0, bytes.length);
}
// 委托给父类加载器处理classpath上的类
return super.findClass(name);
}
/**
* 加载编译产物并实例化
*/
public <T> T newInstance(String className, Class<T> interfaceType) {
try {
Class<?> clazz = loadClass(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
return interfaceType.cast(instance);
} catch (Exception e) {
throw new RuntimeException("动态加载类失败: " + className, e);
}
}
}
三、完整动态编译框架
把前面的组件组装起来,我们得到一个完整的内存编译-加载框架:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 import javax.tools.*;
import java.util.*;
public class DynamicCompiler {
private final JavaCompiler compiler;
private final List<String> options;
public DynamicCompiler() {
this.compiler = ToolProvider.getSystemJavaCompiler();
if (this.compiler == null) {
throw new IllegalStateException("需要JDK环境,JRE不包含编译器");
}
this.options = List.of("-classpath", buildClasspath());
}
/**
* 编译源码字符串并返回ClassLoader
*/
public CompileResult compile(String className, String sourceCode) {
// 收集诊断信息
DiagnosticCollector<JavaFileObject> diagnosticCollector =
new DiagnosticCollector<>();
// 创建内存文件管理器
StandardJavaFileManager stdFileManager =
compiler.getStandardFileManager(null, null, null);
MemoryFileManager memoryFileManager =
new MemoryFileManager(stdFileManager);
// 构造编译单元
JavaFileObject source = new StringJavaFileObject(className, sourceCode);
List<JavaFileObject> compilationUnits = List.of(source);
// 执行编译
JavaCompiler.CompilationTask task = compiler.getTask(
null, // output writer
memoryFileManager, // 自定义文件管理器
diagnosticCollector, // 诊断收集器
options, // 编译选项
null, // annotation processor类名
compilationUnits // 编译单元
);
boolean success = task.call();
if (!success) {
StringBuilder sb = new StringBuilder("编译失败:\n");
for (Diagnostic<?> d : diagnosticCollector.getDiagnostics()) {
sb.append(String.format(" [%s] 行%d:%d - %s\n",
d.getKind(), d.getLineNumber(),
d.getColumnNumber(), d.getMessage(null)));
}
throw new CompilationException(sb.toString());
}
return new CompileResult(memoryFileManager.getClassBytes(),
memoryFileManager.getClassLoader(null));
}
private String buildClasspath() {
return System.getProperty("java.class.path");
}
public static class CompileResult {
private final Map<String, byte[]> classBytes;
private final ClassLoader classLoader;
CompileResult(Map<String, byte[]> classBytes, ClassLoader classLoader) {
this.classBytes = classBytes;
this.classLoader = classLoader;
}
public <T> T newInstance(String className, Class<T> type) {
try {
Class<?> clazz = classLoader.loadClass(className);
return type.cast(clazz.getDeclaredConstructor().newInstance());
} catch (Exception e) {
throw new RuntimeException("实例化失败: " + className, e);
}
}
public Map<String, byte[]> getClassBytes() { return classBytes; }
}
public static class CompilationException extends RuntimeException {
public CompilationException(String message) { super(message); }
}
}
3.1 实战:动态编译并运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 // 定义接口
public interface DataProcessor {
String process(String input);
}
// 动态编译并使用
DynamicCompiler dc = new DynamicCompiler();
String source = "package com.example.dynamic;\n"
+ "public class UpperCaseProcessor implements com.example.DataProcessor {\n"
+ " @Override\n"
+ " public String process(String input) {\n"
+ " return input != null ? input.toUpperCase() : null;\n"
+ " }\n"
+ "}";
DynamicCompiler.CompileResult result =
dc.compile("com.example.dynamic.UpperCaseProcessor", source);
DataProcessor processor = result.newInstance(
"com.example.dynamic.UpperCaseProcessor", DataProcessor.class);
System.out.println(processor.process("hello world")); // 输出: HELLO WORLD
注意源码中
|
1
|
implements
|
后面使用的是全限定类名——因为动态编译的代码需要能通过classpath找到你的接口定义。这就是我们在编译选项中加入完整classpath的原因。
四、插件化架构设计
有了动态编译能力,我们可以构建一个真正灵活的插件系统。与传统的
|
1
|
ServiceLoader
|
+JAR方式不同,动态编译允许热加载源码级别的插件,无需预编译和打包。

4.1 插件注册中心
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class PluginRegistry {
private final DynamicCompiler compiler = new DynamicCompiler();
private final Map<String, Object> plugins = new ConcurrentHashMap<>();
private final Map<String, Class<?>> pluginInterfaces = new ConcurrentHashMap<>();
/**
* 注册插件接口
*/
public <T> void registerInterface(String pluginType, Class<T> interfaceClass) {
pluginInterfaces.put(pluginType, interfaceClass);
}
/**
* 从源码注册插件
*/
@SuppressWarnings("unchecked")
public <T> T registerPlugin(String pluginType, String pluginName,
String sourceCode) {
Class<?> interfaceClass = pluginInterfaces.get(pluginType);
if (interfaceClass == null) {
throw new IllegalArgumentException("未注册的插件类型: " + pluginType);
}
// 编译并实例化
String className = "plugins." + pluginType + "." + pluginName;
DynamicCompiler.CompileResult result =
compiler.compile(className, sourceCode);
Object plugin = result.newInstance(className, interfaceClass);
// 存入注册中心
String key = pluginType + ":" + pluginName;
plugins.put(key, plugin);
return (T) plugin;
}
/**
* 获取已注册插件
*/
@SuppressWarnings("unchecked")
public <T> Optional<T> getPlugin(String pluginType, String pluginName) {
return Optional.ofNullable((T) plugins.get(pluginType + ":" + pluginName));
}
/**
* 列出某类型所有插件
*/
public List<String> listPlugins(String pluginType) {
return plugins.keySet().stream()
.filter(k -> k.startsWith(pluginType + ":"))
.map(k -> k.substring(pluginType.length() + 1))
.toList();
}
/**
* 替换已注册插件(热更新)
*/
public <T> T replacePlugin(String pluginType, String pluginName,
String newSource) {
String key = pluginType + ":" + pluginName;
plugins.remove(key);
return registerPlugin(pluginType, pluginName, newSource);
}
}
4.2 实战:规则引擎插件
一个典型场景是业务规则引擎——不同客户有不同的计算规则,需要运行时动态替换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 // 规则接口
public interface PricingRule {
BigDecimal apply(BigDecimal basePrice, Map<String, Object> context);
}
// 注册中心初始化
PluginRegistry registry = new PluginRegistry();
registry.registerInterface("pricing", PricingRule.class);
// 客户A的折扣规则
String ruleA = "package plugins.pricing;\n"
+ "import java.math.BigDecimal;\n"
+ "import java.util.Map;\n"
+ "public class VipDiscount implements com.example.PricingRule {\n"
+ " @Override\n"
+ " public BigDecimal apply(BigDecimal basePrice, Map context) {\n"
+ " String tier = (String) context.getOrDefault("tier", "normal");\n"
+ " BigDecimal discount = switch(tier) {\n"
+ " case "gold" -> new BigDecimal("0.85");\n"
+ " case "platinum" -> new BigDecimal("0.75");\n"
+ " default -> BigDecimal.ONE;\n"
+ " };\n"
+ " return basePrice.multiply(discount);\n"
+ " }\n"
+ "}";
PricingRule vipRule = registry.registerPlugin("pricing", "VipDiscount", ruleA);
// 使用
Map<String, Object> ctx = Map.of("tier", "gold");
BigDecimal result = vipRule.apply(new BigDecimal("100.00"), ctx);
// result = 85.00
// 热更新:客户A调整折扣策略
PricingRule updatedRule = registry.replacePlugin("pricing", "VipDiscount", ruleA_v2);
BigDecimal newResult = updatedRule.apply(new BigDecimal("100.00"), ctx);
// newResult = 80.00 (折扣加大)
五、生产级注意事项与陷阱
5.1 类加载器泄漏与内存管理
动态编译的类由自定义ClassLoader加载,这些类和ClassLoader实例本身会被GC根引用。如果你反复编译同名类(比如热更新),旧的ClassLoader和它加载的所有类都无法被GC回收,直到所有引用清除。这在高频更新场景下会导致PermGen/Metaspace泄漏。
解决方案:
- 使用弱引用注册表,让旧插件实例可以被GC
- 限制同名类的编译频率,加版本后缀(
1VipDiscount_v2
、
1VipDiscount_v3)
- 设置合理的Metaspace大小:
1-XX:MaxMetaspaceSize=256m
- 监控JVM mxbean的
1getNonHeapMemoryUsage()
指标
1
2
3
4
5
6
7 // 监控Metaspace使用
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
long metaspaceUsed = memoryBean.getNonHeapMemoryUsage().getUsed();
System.out.println("Metaspace使用: " + (metaspaceUsed / 1024 / 1024) + " MB");
5.2 编译安全控制
运行时编译用户提供的代码是高风险操作。生产环境中必须实施安全沙箱:
| 安全措施 | 实现方式 | ||||
|---|---|---|---|---|---|
| 源码白名单 | 正则或AST检查,只允许继承指定接口,禁止反射和JNI调用 | ||||
| SecurityManager | 自定义SecurityManager限制文件/网络/线程访问(JDK 17+需加
) |
||||
| 独立ClassLoader | 每个插件用独立ClassLoader实例,卸载时断开所有引用 | ||||
| 编译超时 | 用
包装编译任务,超时则
|
||||
| 字节码校验 | 编译后用ASM检查字节码,拒绝危险操作码 |
5.3 编译性能优化
编译是CPU密集型操作。以下是生产环境的关键优化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 // 1. 编译任务并行化(多个独立源码可以并行编译)
ExecutorService compilePool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() / 2);
List<Future<CompileResult>> futures = sourceCodes.stream()
.map(sc -> compilePool.submit(() -> compiler.compile(sc.className, sc.code)))
.toList();
// 2. 编译结果缓存(相同源码哈希不重复编译)
Map<String, CompileResult> cache = new ConcurrentHashMap<>();
public CompileResult compileCached(String className, String sourceCode) {
String cacheKey = className + ":" + hash(sourceCode);
return cache.computeIfAbsent(cacheKey,
k -> compiler.compile(className, sourceCode));
}
// 3. 增量编译——只编译变化的源文件
// 利用JavaFileManager判断依赖关系,跳过未修改的编译单元
5.4 与Spring Boot集成
在Spring Boot项目中使用动态编译时,最大的挑战是classpath隔离——动态编译的代码需要能访问Spring的类,但Spring的ClassLoader层次结构与我们的
|
1
|
MemoryClassLoader
|
不兼容。推荐做法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 // 让MemoryClassLoader感知Spring的类路径
public class SpringAwareClassLoader extends MemoryClassLoader {
private final ClassLoader springClassLoader;
public SpringAwareClassLoader(
Map<String, ByteJavaFileObject> bytes,
ClassLoader springClassLoader) {
super(bytes);
this.springClassLoader = springClassLoader;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 优先从内存加载动态编译的类
byte[] bytes = classBytes.get(name);
if (bytes != null) {
return defineClass(name, bytes, 0, bytes.length);
}
// 委托给Spring ClassLoader
return springClassLoader.loadClass(name);
}
}
六、替代方案对比
动态编译不是唯一的运行时代码生成方式。根据场景选择合适的技术:
| 技术 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| JavaCompiler API | 复杂逻辑、需要类型安全 | 编译期检查、强类型、调试友好 | 启动慢、Metaspace开销 |
| ASM/Javassist | 字节码增强、AOP | 性能极高、无编译开销 | API复杂、难以调试 |
| Groovy/JS ScriptEngine | 脚本化规则、简单表达式 | 语法灵活、热加载容易 | 弱类型、性能差 |
| MethodHandle/LambdaMetafactory | 高性能函数派发 | 接近原生调用速度 | 只能生成函数、逻辑受限 |
| Annotation Processor | 编译期代码生成 | 无运行时开销、IDE支持 | 不能运行时动态添加 |
选择建议:
- 如果插件逻辑复杂且需要类型安全,选JavaCompiler API
- 如果只是简单的表达式求值,选ScriptEngine或SpEL
- 如果需要极致性能的函数派发,选LambdaMetafactory
- 如果是编译期就能确定的代码生成,选Annotation Processor
七、总结
JavaCompiler API为Java打开了运行时编译的大门,让动态代码生成和插件化架构不再是脚本语言的专利。通过自定义
|
1
|
JavaFileObject
|
和
|
1
|
JavaFileManager
|
,我们实现了完全的内存编译;通过插件注册中心和热更新机制,我们构建了灵活的插件系统。但能力越大责任越大——生产环境中必须重视类加载器泄漏、编译安全和性能优化。掌握这套API,你就在Java平台上获得了与脚本语言一样的动态性,同时保留了Java的类型安全和工具链优势。
完整的示例代码可在作者的GitHub仓库获取,包含本文所有代码和Spring Boot集成的完整示例。
汤不热吧