Java 6 引入 `javax.tools.JavaCompiler` API,使得在運行時將字符串形式的 Java 源碼編譯為字節碼、加載到 JVM、實例化對象成為可能。這是實現動態腳本、規則引擎、熱插拔、代碼生成器的基石。本文構建一個完整、可生產的動態編譯引擎,涵蓋源碼管理、編譯任務、類文件管理、自定義類加載器全鏈路。
package com.example.dynamic;
import javax.tools.SimpleJavaFileObject;
import java.net.URI;
public class CharSequenceJavaFileObject extends SimpleJavaFileObject {
private final String content;
public CharSequenceJavaFileObject(String className, String content) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.content = content;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return content;
}
}
package com.example.dynamic;
import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
public class JavaClassObject extends SimpleJavaFileObject {
protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();
public JavaClassObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}
public byte[] getBytes() {
return bos.toByteArray();
}
@Override
public OutputStream openOutputStream() throws IOException {
return bos;
}
}
package com.example.dynamic; import javax.tools.*; import java.io.IOException; public class ClassFileManager extends ForwardingJavaFileManager{ private JavaClassObject classObject; public ClassFileManager(StandardJavaFileManager standardManager) { super(standardManager); } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { if (classObject == null) { classObject = new JavaClassObject(className, kind); } return classObject; } public JavaClassObject getClassObject() { return classObject; } }
package com.example.dynamic;
import java.net.URL;
import java.net.URLClassLoader;
public class DynamicClassLoader extends URLClassLoader {
public DynamicClassLoader(ClassLoader parent) {
super(new URL[0], parent); // 無額外 URL,委派給父加載器
}
public Class> loadClass(String fullName, JavaClassObject classObject) {
byte[] classData = classObject.getBytes();
return defineClass(fullName, classData, 0, classData.length);
}
}
package com.example.dynamic;
import javax.tools.*;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class DynamicEngine {
private static final DynamicEngine INSTANCE = new DynamicEngine();
public static DynamicEngine getInstance() { return INSTANCE; }
private final URLClassLoader parentClassLoader;
private final String classpath;
private DynamicEngine() {
this.parentClassLoader = (URLClassLoader) getClass().getClassLoader();
this.classpath = buildClassPath();
}
private String buildClassPath() {
StringBuilder sb = new StringBuilder();
for (URL url : parentClassLoader.getURLs()) {
String path = url.getFile();
// Windows 路徑可能以 / 開頭,去除
if (path.startsWith("/")) path = path.substring(1);
sb.append(path).append(File.pathSeparator);
}
return sb.toString();
}
/**
* 核心方法:源碼字符串 → 實例對象
*/
public Object compileAndInstantiate(String fullClassName, String javaCode) throws Exception {
// 1. 獲取系統編譯器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new IllegalStateException("未找到系統編譯器,請確保運行在 JDK 而非 JRE 環境");
}
// 2. 診斷收集器
DiagnosticCollector diagnostics = new DiagnosticCollector<>();
// 3. 文件管理器
StandardJavaFileManager stdManager = compiler.getStandardFileManager(diagnostics, null, null);
ClassFileManager fileManager = new ClassFileManager(stdManager);
// 4. 構建編譯單元
List compilationUnits = new ArrayList<>();
compilationUnits.add(new CharSequenceJavaFileObject(fullClassName, javaCode));
// 5. 編譯選項
List options = new ArrayList<>();
options.add("-encoding"); options.add("UTF-8");
options.add("-classpath"); options.add(classpath);
// 可選:-parameters 保留參數名、-Xlint:unchecked 等
// 6. 執行編譯任務
JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, diagnostics, options, null, compilationUnits);
Boolean success = task.call();
if (!success) {
StringBuilder err = new StringBuilder("編譯失敗:\n");
for (Diagnostic> d : diagnostics.getDiagnostics()) {
err.append(formatDiagnostic(d));
}
throw new RuntimeException(err.toString());
}
// 7. 加載並實例化
JavaClassObject classObject = fileManager.getClassObject();
DynamicClassLoader classLoader = new DynamicClassLoader(parentClassLoader);
Class> clazz = classLoader.loadClass(fullClassName, classObject);
return clazz.getDeclaredConstructor().newInstance(); // 假設有無參構造器
}
private String formatDiagnostic(Diagnostic> d) {
return String.format(" 行%d:%d [%s] %s%n",
d.getLineNumber(), d.getColumnNumber(), d.getKind(), d.getMessage(null));
}
}
public class DynamicCompileDemo {
public static void main(String[] args) throws Exception {
// 1. 定義動態類源碼 (實現某個介面以便類型安全調用)
String className = "com.example.dynamic.GreetingImpl";
String source = """
package com.example.dynamic;
public class GreetingImpl implements Greeting {
public String sayHello(String name) {
return "Hello, " + name + "! (動態編譯於 " + java.time.LocalDateTime.now() + ")";
}
}
""";
// 2. 編譯並獲取實例
DynamicEngine engine = DynamicEngine.getInstance();
Object instance = engine.compileAndInstantiate(className, source);
// 3. 類型安全調用 (需共享介面)
if (instance instanceof Greeting greeting) {
System.out.println(greeting.sayHello("World"));
}
}
}
interface Greeting { String sayHello(String name); }
Listunits = List.of( new CharSequenceJavaFileObject("pkg.A", "package pkg; public class A {}"), new CharSequenceJavaFileObject("pkg.B", "package pkg; public class B extends A {}") ); // 同一 CompilationTask 編譯所有單元
options.add("-processor"); options.add("com.example.MyProcessor");
// 或使用 ServiceLoader 機制自動發現
// Key: sourceHash (MD5/SHA256) + className + optionsHash // Value: byte[] classData // 命中緩存時直接 defineClass,跳過編譯
`ToolProvider.getSystemJavaCompiler()` 在 JRE 或某些打包環境返回 `null`。解決方案:
| 組件 | 核心作用 | 關鍵方法 |
|---|---|---|
| JavaCompiler | 編譯入口 | `getTask()`、`getStandardFileManager()` | SimpleJavaFileObject | 抽象文件表示 | `getCharContent()` (讀源碼)、`openOutputStream()` (寫字節碼) |
| ForwardingJavaFileManager | 委派模式管理文件 | `getJavaFileForOutput()` 導向自定義輸出 |
| DiagnosticCollector | 收集編譯錯誤/警告 | `getDiagnostics()` |
| ClassLoader.defineClass | 字節碼 → Class 對象 | `defineClass(name, bytes, off, len)` (protected) |
掌握 `JavaCompiler` API,賦予 Java 應用「自我進化」的能力,是構建高靈活性平台化產品的關鍵技術。