天天看點

Java 源代碼動态編譯、類加載和代碼執行(Java 8)

作者:成富Alex

#頭條創作挑戰賽#

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()));
  }
}           

繼續閱讀