1、動態編譯
動態編譯,簡單來說就是在Java程序運行時編譯源代碼。
從JDK1.6開始,引入了Java代碼重寫過的編譯器接口,使得我們可以在運行時編譯Java源代碼,然后再通過類加載器將編譯好的類加載進JVM,這種在運行時編譯代碼的操作就叫做動態編譯。
靜態編譯:編譯時就把所有用到的Java代碼全都編譯成字節碼,是一次性編譯。
動態編譯:在Java程序運行時才把需要的Java代碼的編譯成字節碼,是按需編譯。
靜態編譯示例:
靜態編譯實際上就是在程序運行前將所有代碼進行編譯,我們在運行程序前用Javac命令或點擊IDE的編譯按鈕進行編譯都屬於靜態編譯。
比如,我們編寫了一個xxx.java
文件,里面是一個功能類,如果我們的程序想要使用這個類,就必須在程序啟動前,先調用Javac編譯器來生成字節碼文件。
如果使用動態編譯,則可以在程序運行過程中再對xxx.java
文件進行編譯,之后再通過類加載器對編譯好的類進行加載,同樣能正常使用這個功能類。
動態編譯示例:
JDK提供了對應的JavaComplier接口來實現動態編譯(rt.jar中的javax.tools包提供的編譯器接口,使用的是JDK自帶的Javac編譯器)。
一個用來進行動態編譯的類:
public class TestHello {
public void sayHello(){
System.out.println("hello word");
}
}
編寫一個程序來對它進行動態編譯:
public class TestDynamicCompilation {
public static void main(String[] args) {
//獲取Javac編譯器對象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//獲取文件管理器:負責管理類文件的輸入輸出
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
//獲取要被編譯的Java源文件
File file = new File("/project/test/TestHello.java");
//通過源文件獲取到要編譯的Java類源碼迭代器,包括所有內部類,其中每個類都是一個JavaFileObject
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
//生成編譯任務
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
//執行編譯任務
task.call();
}
}
啟動main函數,會發現在程序運行過程中,使用了Javac編譯器對類TestHello進行了編譯,並生成了字節碼文件TestHello.class。
以上就是動態編譯的簡單使用。如果我們想使用這個類TestHello,也可以在程序運行中通過類加載器對這個已經編譯的類進行加載。
使用JavaComplier接口來實現動態編譯時JDK1.6才引入的,在此之前,也可以通過如下方式實現動態編譯:
Runtime run = Runtime.getRuntime();
Process process = run.exec("javac -cp e:/project/test/TestHello.java");
該方法的本質是啟動一個新的進程來使用Javac進行編譯。
2、動態編譯的應用
(1)、從源碼文件編譯得到字節碼文件
剛才我們使用動態編譯完成了輸入一個Java源文件(.java),再到輸出字節碼文件(.class)的操作。這是從源碼文件編譯得到字節碼文件的方式,實質上也是從磁盤輸入,再輸出到磁盤的方式。
(2)、從源碼字符串編譯得到字節碼文件
假如現在有一串字符串形式的Java代碼,那如何使用動態編譯將這些字符串代碼編譯成字節碼文件?這是從源碼字符串編譯得到字節碼文件的方式,實質上也是從內存中得到源碼,再輸出到磁盤的方式。
根據剛才的代碼,我們知道編譯任務getTask()
這個方法一共有 6 個參數,它們分別是:
Writer out
:編譯器的一個額外的輸出 Writer,為 null 的話就是 System.err;JavaFileManager fileManager
:文件管理器;DiagnosticListener<? super JavaFileObject> diagnosticListener
:診斷信息收集器;Iterable<String> options
:編譯器的配置;Iterable<String> classes
:需要被 annotation processing 處理的類的類名;Iterable<? extends JavaFileObject> compilationUnits
:要被編譯的單元們,就是一堆 JavaFileObject。
根據getTask()
的參數,我們知道編譯器執行編譯所需要的對象類型並不是文件File
對象,而是JavaFileObject
對象。因此,要實現從字符串源碼編譯得到字節碼文件,只需要把字符串源碼變為JavaFileObject
對象即可。
但JavaFileObject
是一個接口,它的標准實現類SimpleJavaFileObject
提供的一些方法是面向類源碼文件(.java)和字節碼文件(.class)的,而我們進行動態編譯時輸入的是字符串源碼,所以我們需要自行實現JavaFileObject
,以使JavaFileObject
對象能裝入我們的字符串源碼。
具體的實現方法就是可以直接繼承SimpleJavaFileObject
類,再重寫其中的一些方法使它能夠裝入字符串即可。
可以通過查看compiler.getTask().call()
的源代碼來查看具體用到了SimpleJavaFileObject
的那些方法,這樣我們才知道需要重寫 SimpleJavaFileObject
的哪些方法。
一篇大佬分析getTask().call()
源代碼執行流程的文章介紹得很十分詳細,強烈推薦:Java 類運行時動態編譯技術.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html。
簡單的流程如下:
在上圖中,getTask().call()
會通過調用作為參數傳入的JavaFileObject
對象的getCharContent()
方法獲得字符串序列,即源碼的讀取是通過 JavaFileObject
的 getCharContent()
方法,那我們只需要重寫getCharContent()
方法,即可將我們的字符串源碼裝進JavaFileObject
了。
構造SourceJavaFileObject
實現定制的JavaFileObject
對象,用於存儲字符串源碼:
public class SourceJavaFileObject extends SimpleJavaFileObject {
private String source; //源碼字符串
//返回源碼字符串
public SourceJavaFileObject(String name, String sourceStr){
super(URI.create("String:///" + name + Kind.SOURCE.extension),Kind.SOURCE);
this.source = sourceStr;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException{
if(source == null) throw new IllegalArgumentException("source == null");
else return source;
}
}
則創建JavaFileObject
對象時,變為了:
//使用重寫getCharContent方法后的JavaFileObject構造參數
JavaFileObject sourceFileObject = new SourceJavaFileObject(className, source);
//執行編譯
Boolean result = compiler.getTask(null, fileManager, null, null, null, Arrays.asList(sourceFileObject)).call();
由於我們自定了JavaFileObject
,文件管理器 fileManager
更像是一個工具類用於把 File
對象數組自動轉換成 JavaFileObject 列表
,換成手動生成 compilationUnits
列表並傳入也是可行的。(上述代碼就是使用了Arrays.asList()
手動生成 compilationUnits
列表)。
至此,只需要調用getTask().call()
就能將字符串形式的源碼編譯成字節碼文件了。
(3)、從源碼字符串編譯得到字節碼數組
如果我們進行動態編譯時,想要直接輸入源碼字符串並且輸出的是字節碼數組,而不是輸出字節碼文件,又該如何實現?實際上,這是從內存中得到源碼,再輸出到內存的方式。
在getTask().call()
源代碼執行流程圖中,我們可以發現JavaFileObject
的 openOutputStream()
方法控制了編譯后字節碼的輸出行為,編譯完成后會調用openOutputStream
獲取輸出流,並寫數據(字節碼)。所以我們需要重寫JavaFileObject
的 openOutputStream()
方法。
同時在執行流程圖中,我們還發現用於輸出的JavaFileObject
對象是JavaFileManager
的getJavaFileForOutput()
方法提供的,所以為了讓編譯器編譯完成后,將編譯得到的字節碼輸出到我們自己構造的JavaFileObject
對象,我們還需要重寫JavaFileManager
。
構造ClassFileObject
,實現定制的JavaFileObject
對象,用於存儲編譯后得到的字節碼:
public static class ClassFileObject extends SimpleJavaFileObject {
private ByteArrayOutputStream byteArrayOutputStream; //字節數組輸出流
//編譯完成后會回調OutputStream,回調成功后,我們就可以通過下面的getByteCode()方法獲取編譯后的字節碼字節數組
@Override
public OutputStream openOutputStream() throws IOException {
return byteArrayOutputStream;
}
//將輸出流中的字節碼轉換為字節數組
public byte[] getCompiledBytes() {
return byteArrayOutputStream.toByteArray();
}
}
這樣,我們就擁有了自定義的用於存儲字節碼的JavaFileObject
。同時還通過添加getByteCode()
方法來獲得JavaFileObject
對象中用於存放字節碼的輸出流,並將其轉換為字節數組。
接下來,就需要重寫JavaFileManager
,使編譯器編譯完成后,將字節碼存放在我們的ClassFileObject
。具體做法是直接繼承ForwardingJavaFileManager
,再重寫需要的getJavaFileForOutput()
方法即可。
public static class MyJavaObjectManager extends ForwardingJavaFileManager<JavaFileManager>{
private ClassFileObject classObject; //我們自定義的JavaFileObject
//重寫該方法,使其返回我們的ClassJavaFileObject
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
classObject= new ClassJavaFileObject (className, kind);
return classObject;
}
}
構造完畢,接下來直接傳入getTask執行即可:
//執行編譯
Boolean result = compiler.getTask(null,new MyJavaObjectManager(), null, null, null, Arrays.asList(sourceFileObject)).call();
注意這里傳入的JavaFileObject
,是前面構造的存儲字符串源碼的sourceFileObject
,而不是我們用來存儲字節碼的sourceFileObject
。
至此,我們使用動態編譯完成了將字符串源碼編譯成字節碼數組。隨后我們可以使用類加載器加載 byte[]中的字節碼即可。
3、總結
動態編譯是在Java程序運行時編譯源代碼,動態編譯配合類加載器就可以在程序運行時編譯源代碼,並動態加載。
JDK提供了對應的JavaComplier接口來實現動態編譯。
動態編譯中存放源碼和字節碼的對象都是JavaFileObject
,因此如果我們想要修改源碼的輸入方式或者字節碼的輸出方式的,可以自主實現JavaFileObject
接口。同時,由於編譯器是通過JavaFileManager
來管理輸入輸出的,因此也需要自主實現JavaFileManager
接口。
由於能力有限,可能存在錯誤,感謝指出。以上內容為本人在學習過程中所做的筆記。參考的書籍、文章或博客如下:
[1]seanwangjs. Java 類運行時動態編譯技術.https://seanwangjs.github.io/2018/03/13/java-runtime-compile.html
[2]Throwable.深入理解Java的動態編譯.博客園.https://www.cnblogs.com/throwable/p/13053582.html
[3]執筆記憶的空白.java動態編譯實現.騰訊雲雲社區.https://cloud.tencent.com/developer/article/1764721?from=information.detail