標準編譯器 API 位於 `tools.jar` (JDK 目錄),運行時需確保使用 JDK 而非 JRE。核心流程:
import javax.tools.*;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
public class DynamicCompilerDemo {
// 1. 內存源碼對象:實現 JavaFileObject 接口
static class StringSourceJavaObject extends SimpleJavaFileObject {
private final String content;
StringSourceJavaObject(String className, String content) throws Exception {
// URI 格式:string:///包名/類名.kind
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.content = content;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return content;
}
}
public static void main(String[] args) throws Exception {
// 要動態編譯的源碼字符串
String expr = "234 - ( 1 + 45 * 4 ) / 5";
String className = "DynamicCalculator";
String methodName = "calculate";
String source = "public class " + className + " {\n" +
" public static double " + methodName + "() {\n" +
" return " + expr + ";\n" +
" }\n" +
" public static void main(String[] args) {\n" +
" System.out.println(" + methodName + "());\n" +
" }\n" +
"}";
// 2. 獲取編譯器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("請使用 JDK 運行,而非 JRE!tools.jar 缺失。");
}
// 3. 文件管理器
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
// 4. 構建編譯單元
StringSourceJavaObject sourceObject = new StringSourceJavaObject(className, source);
Iterable extends JavaFileObject> compilationUnits = Arrays.asList(sourceObject);
// 5. 編譯任務
JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, null, null, null, compilationUnits);
// 6. 執行編譯
Boolean success = task.call();
if (!success) {
System.err.println("編譯失敗!");
return;
}
// 7. 自定義類加載器:從文件管理器獲取字節碼
// 簡化演示:直接用當前線程類加載器(編輸出到默認輸出位置時可行)
// 生產環境建議實現 ForwardingJavaFileManager 攔截字節碼輸出
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class> clazz = loader.loadClass(className);
Method method = clazz.getMethod(methodName);
Object result = method.invoke(null);
System.out.println("表達式: " + expr);
System.out.println("計算結果: " + result); // 輸出: 197.0
}
}
上述代碼依賴編譯器默認輸出到類路徑,生產環境不可靠。正確做法是自定義 `ForwardingJavaFileManager` 攔截 `.class` 字節碼數組:
class MemoryFileManager extends ForwardingJavaFileManager {
private final Map classBytes = new HashMap<>();
MemoryFileManager(StandardJavaFileManager mgr) { super(mgr); }
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) throws IOException {
return new SimpleJavaFileObject(URI.create("mem:///" + className), Kind.CLASS) {
@Override public OutputStream openOutputStream() {
return new ByteArrayOutputStream() {
@Override public void close() {
classBytes.put(className, toByteArray());
}
};
}
};
}
public byte[] getClassBytes(String className) { return classBytes.get(className); }
}
class MemoryClassLoader extends ClassLoader {
private final MemoryFileManager manager;
MemoryClassLoader(MemoryFileManager mgr, ClassLoader parent) {
super(parent); this.manager = mgr;
}
@Override protected Class> findClass(String name) {
byte[] bytes = manager.getClassBytes(name);
if (bytes == null) throw new ClassNotFoundException(name);
return defineClass(name, bytes, 0, bytes.length);
}
}
MemoryFileManager memMgr = new MemoryFileManager(fileManager);
JavaCompiler.CompilationTask task = compiler.getTask(null, memMgr, null, null, null, compilationUnits);
task.call();
MemoryClassLoader loader = new MemoryClassLoader(memMgr, Thread.currentThread().getContextClassLoader());
Class> clazz = loader.loadClass(className);
// 反射調用...
| 維度 | 風險/影響 | 對策 |
|---|---|---|
| 代碼注入 | 用戶輸入惡意代碼 (System.exit, 文件讀寫, 反射攻擊) | 安全管理器 SecurityManager / 字節碼檢測 / 白名單限制導入包 |
| 類加載器洩漏 | 動態類無法卸載,Metaspace OOM | 使用獨立 ClassLoader,用完丟棄;或 JDK 10+ ClassDataSharing |
| 編譯性能 | Javac 編譯較慢 (數十~百毫秒) | 緩存編譯結果;簡單表達式用 MVEL/Aviator 等輕量表達式引擎 |
| 調試困難 | 動態代碼無源碼行號 | 生成源碼時保留行號信息,或輸出 .java 文件到臨時目錄 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| JavaCompiler API | 零依賴、原生、生成標準字節碼 | 啟動慢、API 繁瑣、安全風險 | 框架開發、代碼生成、規則引擎核心 |
| Janino | 極快 (內存編譯、無中間文件)、支持表達式/類體/腳本 | 第三方依賴、語法子集 | 高性能表達式求值、規則引擎 |
| ASM/ByteBuddy | 直接操作字節碼、極高性能、無編譯步驟 | 學習曲線陡峭、易出錯 | AOP、Mock、動態代理、高性能代碼生成 |
| ScriptEngine (JS/Groovy) | 語法靈活、生態豐富 | 性能較差、引入重依賴 | 用戶腳本、快速原型 |
Java 動態編譯是「核武器級」能力:運行時將字符串變為可執行邏輯。掌握 `JavaCompiler API` + 自定義 `FileManager` + `ClassLoader` 三件套,即可構建表達式引擎、熱插件系統、代碼生成器。但請謹記:權力越大,責任越大——必須建立代碼審計、沙箱隔離、緩存策略、Metaspace 監控等防護體系。簡單表達式計算優先考慮 Janino 或 Aviator,保留動態編譯給真正需要「生成類」的場景。