【Java動態編譯】動態編譯的應用


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()源代碼執行流程圖中,我們可以發現JavaFileObjectopenOutputStream() 方法控制了編譯后字節碼的輸出行為,編譯完成后會調用openOutputStream獲取輸出流,並寫數據(字節碼)。所以我們需要重寫JavaFileObjectopenOutputStream() 方法。

同時在執行流程圖中,我們還發現用於輸出的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM