#頭條創作挑戰賽#
Java 的一個重要特性是動态的類加載機制。通過在運作時動态地加載類,Java 程式可以實作很多強大的功能。下面通過一個具體的執行個體來說明 Java 程式中,如何動态地編譯 Java 源代碼、加載類和執行類中的代碼。這裡的代碼示例适用的版本是 Java 8。
示例所實作的功能很簡單,就是對表達式求值。輸入的是類似 1 + 1 或 3 * (2 + 3) 這樣的表達式,傳回的是表達式的值。示例的做法是動态建立一個 Java 源檔案,編譯該檔案生成 class 檔案,加載 class 檔案之後再執行。比如,需要求值的表達式是 1 + 1,那麼所生成的 Java 源檔案如下所示,其中 1 + 1 的部分是動态的。
public class Calculator {
public static Object calculate() {
return 1 + 1;
}
}
我們隻需要編譯該源檔案,加載編譯之後的 class 檔案,再通過反射 API 來調用其中的 calculate 方法就可以得到表達式求值的結果。
編譯
第一步是動态生成 Java 源代碼并編譯。生成 Java 源代碼比較簡單,直接用字元串連接配接就可以了。當然了,在生成邏輯比較複雜時,推薦的做法是使用字元串模闆引擎,如 Handlebars。在下面的代碼中,getJavaSource 方法生成 Java 源代碼,compile 方法進行編譯。
在進行編譯的時候,使用的是 JDK 标準的 JavaCompiler 接口。從源代碼字元串中建立了一個 JavaFileObject 對象作為編譯時的源代碼單元。編譯時的選項 -d 指定了編譯結果的輸出路徑,這裡是一個臨時檔案夾。compile 方法的傳回值是一個 Pair 對象,包含了 class 檔案的路徑,以及随機生成的 Java 包的名稱。
public class DynamicCompilation {
private static final String CLASS_NAME = "Calculator";
public static Pair<Path, String> compile(String expr) throws IOException {
String packageName = "z" + UUID.randomUUID().toString().replace("-", "");
Path outputPath = Files.createTempDirectory("expr");
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,
null, null);
compiler.getTask(null, fileManager, null, ImmutableList.of(
"-d", outputPath.toAbsolutePath().toString()
), null,
Collections.singletonList(
new StringContentJavaFileObject(CLASS_NAME,
getJavaSource(packageName, expr))))
.call();
return Pair.of(outputPath, packageName + "." + CLASS_NAME);
}
private static String getJavaSource(String packageName, String expr) {
return "package " + packageName + "; "
+ "public class " + CLASS_NAME
+ " { public static Object calculate() { "
+ "return " + expr + "; }" +
"}";
}
}
上面的代碼用到了一個幫助類 StringContentJavaFileObject,表示從字元串建立的 JavaFileObject 對象。
public class StringContentJavaFileObject extends SimpleJavaFileObject {
private final String content;
public StringContentJavaFileObject(String name, String content) {
super(URI.create("string:///" + name + Kind.SOURCE.extension),
Kind.SOURCE);
this.content = content;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return content;
}
}
加載
編譯完成之後的第二步是動态加載類。這一步并沒有實作自定義的類加載器,而且使用内置的系統類加載器。系統類加載器通過 ClassLoader.getSystemClassLoader() 方法來擷取。系統類加載器在 classpath 上查找類。這裡用了一個比較 hack 的技巧來動态修改系統類加載器的 classpath。
在下面的代碼中,ClasspathUpdater 的 addPath 方法可以把一個 Path 對象表示的路徑,添加到系統類加載器的查找路徑中。這是因為系統類加載器自身是 URLClassLoader 類型的加載器,其中的 addURL 方法可以添加新的查找路徑。隻不過 addURL 方法是 protected,這裡通過反射 API 來進行調用。
public class ClasspathUpdater {
public static void addPath(Path path) {
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
try {
Method method = URLClassLoader.class.getDeclaredMethod("addURL",
URL.class);
method.setAccessible(true);
method.invoke(classLoader, path.toUri().toURL());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
上面介紹的 ClasspathUpdater 類中的使用技巧,隻對 Java 8 生效。在 Java 9 引入子產品系統時,對系統類加載器進行了修改。系統類加載器被替換成了應用類加載器。應用類加載器不再是 URLClassLoader 類型了,就不能使用這個技巧了。
執行
最後一步就是執行動态加載的 Java 類。這一步比較簡單,隻需要用 Class.forName 方法來查找 Java 類,再找到對應的 Method 對象,直接調用即可。下面的代碼給出了示例。
public class Invoker {
public static Object invoke(String className) {
try {
Method method = Class.forName(className).getDeclaredMethod("calculate");
return method.invoke(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
完整的執行過程
最後把整個流程串起來。在下面的代碼中,需要求值的表達式是 (1 + 1) * 3 / 5.0。首先調用 DynamicCompilation.compile 方法進行動态編譯,得到 class 檔案的路徑和完整的類名。class 檔案的路徑通過 ClasspathUpdater.addPath 方法添加到 classpath 中。完整的類名則傳遞給 Invoker.invoke 方法來執行。最後輸出的結果是表達式的值。
public class Main {
public static void main(String[] args) throws IOException {
Pair<Path, String> result = DynamicCompilation.compile("(1 + 1) * 3 / 5.0");
ClasspathUpdater.addPath(result.getLeft());
System.out.println(Invoker.invoke(result.getRight()));
}
}