java動態編譯

編譯,一般來說就是將源代碼轉換成機器碼的過程,比如在C語言中中,將C語言源代碼編譯成a.out,,但是在Java中的理解可能有點不同,編譯指的是將java 源代碼轉換成class字節碼的過程,而不是真正的機器碼,這是因為中間隔著一個JVM。雖然對于編譯的理解不同,但是編譯的過程基本上都是相同的。但是我們熟悉的編譯大都是點擊一下Eclipse或者Intellij Idea的Run或者Build按鈕,但是在點擊后究竟發生什么?其實我沒仔細了解過,只是知道這個程序運行起來了,但是如果你使用過javac命令去編譯代碼時,可能了解的就更深一些,據說印度的Java程序員最開始編程的時候使用的都是文本編輯器而不是IDE,這樣更能接觸底層的過程。
除了使用javac命令編譯Java程序,從Java 1.6開始,我們也可以在程序運行時根據程序實際運行來構建一些類并進行編譯,這需要JDK提供給我們一些可供調用的接口來完成編譯工作。
一、編譯源碼需要啥?
那么問題來了,如果要了解運行時編譯的過程和對應的接口,首先要明白的就是編譯這個過程都會涉及哪些工具和要解決的問題?從我們熟悉的構建過程開始:

編譯工具(編譯器):顯然沒有這個東西我們啥也干不了;
要編譯的源代碼文件:沒有這個東西,到底編啥呢?
源代碼、字節碼文件的管理:其實這里靠的是文件系統的支持,包括文件的創建和管理;
編譯過程中的選項:要編譯的代碼版本、目標,源代碼位置,classpath和編碼等等,見相關文章;
編譯中編譯器輸出的診斷信息:告訴你編譯成功還是失敗,會有什么隱患提出警告信息;
按照這些信息,JDK也提供了可編程的接口對象上述信息,這些API全部放在javax.tools包下,對應上面的信息如下:
編譯器:涉及到的接口和類如下:

JavaCompiler
JavaCompiler.CompilationTask
ToolProvider
在上面的接口和類中,ToolProvider類似是一個工具箱,它可以提供JavaCompiler類的實例并返回,然后該實例可以獲取JavaCompiler.CompilationTask實例,然后由JavaCompiler.CompilationTask實例來執行對應的編譯任務,其實這個執行過程是一個并發的過程。

源代碼文件:涉及到接口和類如下:

FileObject
ForwardingFileObject
JavaFileObject
JavaFileObject.Kind
ForwardingJavaFileObject
SimpleJavaFileObject
上述后面的4個接口和類都是FileObject子接口或者實現類,FIleObject接口代表了對文件的一種抽象,可以包括普通的文件,也可以包括數據庫中的數據庫等,其中規定了一些操作,包括讀寫操作,讀取信息,刪除文件等操作。我們要用的其實是JavaFileObject接口,其中還增加了一些操作Java源文件和字節碼文件特有的API,而SimpleJavaFileObject是JavaFileObject接口的實現類,但是其中你可以發現很多的接口其實就是直接返回一個值,或者拋出一個異常,并且該類的構造器由protected修飾的,所以要實現復雜的功能,需要我們必須擴展這個類。ForwardingFileObject、ForwardingJavaFileObject類似,其中都是包含了對應的FileObject和JavaFileObject,并將方法的執行委托給這些對象,它的目的其實就是為了提高擴展性。

文件的創建和管理:涉及接口和類如下:

JavaFileManager
JavaFileManager.Location
StandardJavaFileManager
ForwardingJavaFileManager
StandardLocation

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
// 該JavaFileManager實例是com.sun.tools.javac.file.JavacFileManager
JavaFileManager manager= compiler.getStandardFileManager(collector, null, null);

JavaFileManager用來創建JavaFileObject,包括從特定位置輸出和輸入一個JavaFileObject,ForwardingJavaFileManager也是出于委托的目的。而StandardJavaFileManager是JavaFileManager直接實現類,JavaFileManager.Location和StandardLocation描述的是JavaFileObject對象的位置,由JavaFileManager使用來決定在哪創建或者搜索文件。由于在javax.tools包下沒有JavaFileManager對象的實現類,如果我們想要使用,可以自己實現該接口,也可以通過JavaCompiler類中的getStandardFileManager完成,如下:

編譯選項的管理:

OptionChecker
這個接口基本上沒有用過。

診斷信息的收集:涉及接口和類如下:

Diagnostic
DiagnosticListener
Diagnostic.Kind
DiagnosticCollector
Diagnostic會輸出編譯過程中產生的問題,包括問題的信息和出現問題的定位信息,問題的類別則在Diagnostic.Kind中定義。DiagnosticListener則是從編譯器中獲取診斷信息,當出現診斷信息時則會調用其中的report方法,DiagnosticCollector則是進一步實現了DiagnosticListener,并將診斷信息收集到一個list中以便處理。

在Java源碼運行時編譯的時候還會遇到一個與普通編譯不同的問題,就是類加載器的問題,由于這個問題過大,而且比較核心,將會專門寫一篇文章介紹。
二、如何在運行時編譯源代碼?
好了說了這么多了,其實都是為了下面的實例作為鋪墊,我們還是從上述的幾個組件來說明。

1、準備編譯器對象
這里只有一種方法,如下:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// ......
// 在其他實例都已經準備完畢后, 構建編譯任務, 其他實例的構建見如下
Boolean result = compiler.getTask(null, manager, collector, options,null,Arrays.asList(javaFileObject));

2、診斷信息的收集

// 初始化診斷收集器
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
// ......
// 編譯完成之后,獲取編譯過程中的診斷信息
collector.getDiagnostics().forEach(item -> System.out.println(item.toString()))

在這個過程中可以通過Diagnostic實例獲取編譯過程中出錯的行號、位置以及錯誤原因等信息。
3、源代碼文件對象的構建
由于JDK提供的FileObject、ForwardingFileObject、JavaFileObject、ForwardingJavaFileObject、SimpleJavaFileObject都無法直接使用,所以我們需要根據需求自定義,此時我們要明白SimpleJavaFileObject類中的哪些方法是必須要覆蓋的,可以看如下過程:

下面是調用compiler中的getTask方法時的調用棧,可以看出從main()方法中開始調用getTask方法開始,直到編譯工作開始進行,首先讀取源代碼,調用com.sun.tools.javac.main包中的readSource()方法,源代碼如下:

public CharSequence readSource(JavaFileObject filename) {try {inputFiles.add(filename);return filename.getCharContent(false);} catch (IOException e) {log.error("error.reading.file", filename, JavacFileManager.getMessage(e));return null;}
}

其中調用ClientCodeWrapper$WrappedFileObject對象中的filename.getCharContent(false)方法來讀取要編譯的源碼,源代碼如下:

public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {try {return clientFileObject.getCharContent(ignoreEncodingErrors);} catch (ClientCodeException e) {throw e;} catch (RuntimeException e) {throw new ClientCodeException(e);} catch (Error e) {throw new ClientCodeException(e);}
}

而其中的clientFileObject.getCharContent(ignoreEncodingErrors),其實就是調用我們實現的自定義的JavaFIleObject對象,因此源代碼文本是必須的,因此getCharContent方法是必須實現的,另外在編譯器編譯完成之后要將編譯完成的字節碼輸出,如下圖:

這時調用writeClass()輸出字節碼,通過打開一個輸出流OutputStream來完成該過程,因此openOutputStream()這個方法也是必須實現的。因此該類的實現如下:

public static class MyJavaFileObject extends SimpleJavaFileObject {private String source;private ByteArrayOutputStream outPutStream;// 該構造器用來輸入源代碼public MyJavaFileObject(String name, String source) {// 1、先初始化父類,由于該URI是通過類名來完成的,必須以.java結尾。// 2、如果是一個真實的路徑,比如是file:///test/demo/Hello.java則不需要特別加.java// 3、這里加的String:///并不是一個真正的URL的schema, 只是為了區分來源super(URI.create("String:///" + name + Kind.SOURCE.extension), Kind.SOURCE);this.source = source;}// 該構造器用來輸出字節碼public MyJavaFileObject(String name, Kind kind){super(URI.create("String:///" + name + kind.extension), kind);source = null;}@Overridepublic CharSequence getCharContent(boolean ignoreEncodingErrors){if(source == null){throw new IllegalArgumentException("source == null");}return source;}@Overridepublic OutputStream openOutputStream() throws IOException {outPutStream = new ByteArrayOutputStream();return outPutStream;}// 獲取編譯成功的字節碼byte[]public byte[] getCompiledBytes(){return outPutStream.toByteArray();}
}

4、文件管理器對象的構建
文件管理對象顯然也是不能直接使用JDK提供的接口,因為只有ForwardingJavaFileManager是一個類,其他的都是接口,而且在ForwardingJavaFileManager中構造器又是protected,所以如果想定制化使用的話,需要實現接口或者繼承類,如果只是簡單使用,可以如下:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
// 該JavaFileManager實例是com.sun.tools.javac.file.JavacFileManager
JavaFileManager manager= compiler.getStandardFileManager(collector, null, null);

但是compiler.getStandardFileManager()返回的是com.sun.tools.javac.file.JavacFileManager實例,這個不是公開的類,所以我們無法直接使用,只能通過這種調用返回實例。

但是我們課也可以構造自己的FileManager,為了更好的構建,需要理解JavaFileManager在內存中編譯時的使用過程,如下:

在編譯過程中,首先是編譯器會遍歷JavaFileManager對象,獲取指定位置的所有符合要求的JavaFileObject對象,甚至可以遞歸遍歷,這時調用的是list()方法,該方法會掃面所有涉及的到的包,包括一個類和它實現的接口和繼承的類:

之后根據獲取到的JavaFileObject對象,獲取它的二進制表示的名稱,通過調用inferBinaryName()方法;

之后是輸出編譯類,而類的表示為JavaFileObject對象,注意此時的JavaFileObject.Kind為CLASS,調用的方法是getJavaFileForOutput(),注意該方法的調用是在JavaFileObject中openOutputStream()方法之前,如下圖:

既然了解了上述的流程,我們自定義的文件管理器如下:

private static Map<String, JavaFileObject> fileObjects = new ConcurrentHashMap<>();
// 這里繼承類,不實現接口是為了避免實現過多的方法
public static class MyJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {protected MyJavaFileManager(JavaFileManager fileManager) {super(fileManager);}@Overridepublic JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {JavaFileObject javaFileObject = fileObjects.get(className);if(javaFileObject == null){super.getJavaFileForInput(location, className, kind);}return javaFileObject;}@Overridepublic JavaFileObject getJavaFileForOutput(Location location, String qualifiedClassName, JavaFileObject.Kind kind, FileObject sibling) throws IOException {JavaFileObject javaFileObject = new MyJavaFileObject(qualifiedClassName, kind);fileObjects.put(qualifiedClassName, javaFileObject);return javaFileObject;}
}

5、編譯選項的選擇
在使用javac命令的時候,可以添加很多的選項,在實現API完成編譯的時候也可以提供參數,比如編譯目標,輸出路徑以及類路徑等等,如下:

List<String> options = new ArrayList<>();
options.add("-target");
options.add("1.8");
options.add("-d");
options.add("/");
// 省略......
compiler.getTask(null, javaFileManager, collector, options, null, Arrays.asList(javaFileObject));

6、其他問題
想將編譯完成的字節碼輸出為文件,也不需要上面自定義JavaFileManager,直接使用JavaCompiler提供的即可,而且在自定義的JavaFileObject中也不需要實現OpenOutStream這種方法,代替要提供options.add(“-d”),options.add(“/”)等編譯選項;如果不輸出為文件按照上述的例子即可;
StandardLocation中的元素可以代替真實的路徑位置,但是不會輸出為文件,可以為一個內存中的文件;
在編譯完成之后要將字節碼文件加載進來,因此就要涉及到類加載機制,由于這也是一個很大的話題,所以后面會專門總結一篇,但是在這里還是要說明一下,由于上面編譯時沒有額外的依賴包,所以不用考慮加載依賴文件的問題,但是當如果有這樣的需求時,我們可以利用類加載的委托機制,將依賴文件的加載全部交給父加載器去做即可。
完整的代碼如下:

package com.wdx.compiler;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class CubeJavaCompiler{private static final Logger logger = LoggerFactory.getLogger(CubeJavaCompiler.class);private static final JavaCompiler _compiler = ToolProvider.getSystemJavaCompiler();private static final DiagnosticCollector<JavaFileObject>  collector = new DiagnosticCollector<>();private static final CubeJavaFileManager manager = new CubeJavaFileManager(_compiler.getStandardFileManager(collector, null, null));private static final Map<String, JavaFileObject> fileObjectMap = new ConcurrentHashMap<>();private static List<String> options = new ArrayList<>();static {options.add("-Xlint:unchecked");options.add("-target");options.add("1.8");}public static Class<?> compile(String code, String className) throws ClassNotFoundException{String qualified = className.substring(className.lastIndexOf('.') + 1, className.length());CubeJavaObject cubeJavaObject = new CubeJavaObject(qualified, code);JavaCompiler.CompilationTask task = _compiler.getTask(null, manager, collector, options, null, Arrays.asList(cubeJavaObject));task.call();//輸出診斷信息for (Diagnostic<? extends JavaFileObject> diagnostic : collector.getDiagnostics()) {try {logger.error("編譯錯誤:{}", diagnostic.toString());} catch (Exception e) {logger.error("輸出內容錯誤", e);}}return cubeJavaClassLoader.loadClass(className);}private static ClassLoader cubeJavaClassLoader = new ClassLoader() {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {JavaFileObject fileObject = fileObjectMap.get(name);if(fileObject != null){byte[] bytes = ((CubeJavaObject)fileObject).getCompiledBytes();return defineClass(name, bytes, 0, bytes.length);}try{return ClassLoader.getSystemClassLoader().loadClass(name);} catch (Exception e){logger.error("加載類失敗,{}", name, e);return super.findClass(name);}}};private static class CubeJavaObject extends SimpleJavaFileObject{private String code;private ByteArrayOutputStream outPutStream;public CubeJavaObject(String qualified, String code) {super(URI.create("String:///" + qualified + Kind.SOURCE.extension), Kind.SOURCE);this.code = code;}public CubeJavaObject(String qualified, Kind kind) {super(URI.create("String:///" + qualified + kind.extension), kind);}@Overridepublic CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {if(code == null){throw new IllegalArgumentException("code required");}return code;}@Overridepublic OutputStream openOutputStream() throws IOException {outPutStream = new ByteArrayOutputStream();return outPutStream;}public byte[] getCompiledBytes(){return outPutStream.toByteArray();}}private static class CubeJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {public CubeJavaFileManager(JavaFileManager fileManager) {super(fileManager);}@Overridepublic JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {JavaFileObject javaFileObject = fileObjectMap.get(className);if(javaFileObject == null){super.getJavaFileForInput(location, className, kind);}return javaFileObject;}@Overridepublic JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {JavaFileObject javaFileObject = new CubeJavaObject(className, kind);fileObjectMap.put(className, javaFileObject);return javaFileObject;}}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/250058.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/250058.shtml
英文地址,請注明出處:http://en.pswp.cn/news/250058.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[c++] - 簡單的冒泡

#include <iostream> using namespace std;int main() {// 利用冒泡排序實現升序序列int arr[9] {4, 2, 8, 0, 5, 7, 1, 3, 9};cout << "排序前: " << endl;for (int i 0; i < 9; i){cout << arr[i] << " ";}cout <…

Python爬蟲之解析網頁

常用的類庫為lxml, BeautifulSoup, re(正則) 以獲取豆瓣電影正在熱映的電影名為例,urlhttps://movie.douban.com/cinema/nowplaying/beijing/ 網頁分析 部分網頁源碼 <ul class"lists"><liid"3878007"class"list-item"data-title"…

騰訊企業郵箱報錯 smtp.exmail.qq.comport 465, isSSL false

一、報錯 "smtp.exmail.qq.com" port 465, isSSL false 通過網上搜索查詢一些資料&#xff0c;推測是郵箱的配置出問題了。 二、修改郵箱配置 1 // 創建屬性2 Properties props new Properties();3 props.setProperty("mail.transport.protocol", "s…

spring與JDK版本對應關系

搭建spring框架得時候要考慮jdk的版本&#xff0c;提供一下參考 JDK 8 中可以使用 Spring Framework 5.x JDK 7 中可以使用 Spring Framework 4.x JDK 6 中可以使用 Spring Framework 4.x JDK 5 中可以使用 Spring Framework 3.x

Markdown預覽功能不可用解決方案

初學者在使用Markdown時也許會遇到這個問題 原因是電腦缺少一個組件&#xff0c;解決方案很簡單&#xff0c;安裝上就好了&#xff0c;以下是鏈接 http://markdownpad.com/download/awesomium_v1.6.6_sdk_win.exe轉載于:https://www.cnblogs.com/j9oker/p/10092829.html

Linux 中yum的配置

1.進入yum的路徑 cd /etc/yum.repos.d 2.將原始的repo文件移入一個新建的backup文件下做備份 mv CentOS* backup 3.在/etc/yum.repos.d下新建一個自己的文件(這里的文件必須以repo結尾); vi zhi.repo 其中&#xff0c;第一行必須是[文件名]的格式  是一個標記 name*** 這是一…

[生態建設] - js判斷小技巧

0、參考 說明: 從幾個易得的點出發,逐步向外擴展延申,保證代碼的可靠性 1、判斷是否為某個類型 // 判斷是否為 null const isNull o > {return o null; };// 判斷是否為 undefined const isUndefined o > {return o undefined; };// 判斷是否為 null or undefined…

Spring中Bean的概念

一、Bean的定義 <beans…/>元素是Spring配置文件的根元素&#xff0c;<beans…/>元素可以包含多個<bean…/>子元素&#xff0c;每個<bean…/>元素可以定義一個Bean實例&#xff0c;每一個Bean對應Spring容器里的一個Java實例定義Bean時通常需要指定兩…

[TJOI2010]閱讀理解

題目描述 英語老師留了N篇閱讀理解作業&#xff0c;但是每篇英文短文都有很多生詞需要查字典&#xff0c;為了節約時間&#xff0c;現在要做個統計&#xff0c;算一算某些生詞都在哪幾篇短文中出現過。 輸入輸出格式 輸入格式&#xff1a; 第一行為整數N&#xff0c;表示短文篇…

ccentos 7下安裝php5.6并使用nginx + php-fpm部署多個不同端口網站

作為一個的勤雜工&#xff0c;近期因公司內部信息化的需求&#xff0c;給新進員工提供基礎的知識培訓和介紹&#xff0c;也為了給公司內部建立一個溝通交流的平臺&#xff0c;百度找了開源的百科系統HDwiki和開源的問答系統Tipask問答系統&#xff0c;蛋痛的這兩套系統均是phpm…

Zookeeper基礎使用機制原理

Znode&#xff1a; 1、Znode既是路徑(目錄)也是信息(文件) 2、Znode有兩種分類&#xff1a;一分為臨時節點(會話生命周期)和永久節點&#xff1b;二分為普通節點和順序節點 Watch&#xff1a; 1、監聽與通知機制&#xff0c;可以在節點上監聽其本身(增、刪、改)或其子節點(增、…

JS ajax請求參數格式( formData 、serialize)

1 $("#importBtn").click(function(){2 if($("#conId").val() ""){3 alert("請填寫Id");4 return;5 }6 if($("#fromWhere").val() "…

【小工具分享】 - vscode注釋自動生成

參考 關閉文件頭部注釋 點擊設置 輸入fileheader搜索 關閉頭部注釋 "fileheader.customMade" : {"autoAdd": false }

Spring的bean實例化過程

以XmlBeanFactory為例&#xff0c;最簡單的取bean方式是&#xff1a; BeanFactory factory new XmlBeanFactory(new FileSystemResource("D:\\workspace\\JavaApplication2\\src\\javaapplication2\\spring\\beans.xml")); Car obj (Car)factory.getBean("c…

最全整理瀏覽器兼容性問題與解決方案(轉)

所謂的瀏覽器兼容性問題&#xff0c;是指因為不同的瀏覽器對同一段代碼有不同的解析&#xff0c;造成頁面顯示效果不統一的情況。在大多數情況下&#xff0c;我們的需求是&#xff0c;無論用戶用什么瀏覽器來查看我們的網站或者登陸我們的系統&#xff0c;都應該是統一的顯示效…

【算法】 - 滑動窗口

1. 題目鏈接 2. 分析 最多可以將K個值從0變成1,因此滑動窗口的限制條件: 0的數量(zeros)小于K,算法過程如下 有一個滑動窗口(slipper),每次都會從A中讀入一個數當讀入的數為0時,zeros當zeros的數量大于K時,會取出slipper首部的元素,當取值為0時zeros-- 總體代碼如下: var lo…

Springboot整合thymeleaf模板

Thymeleaf是個XML/XHTML/HTML5模板引擎&#xff0c;可以用于Web與非Web應用。 Thymeleaf的主要目標在于提供一種可被瀏覽器正確顯示的、格式良好的模板創建方式&#xff0c;因此也可以用作靜態建模。你可以使用它創建經過驗證的XML與HTML模板。相對于編寫邏輯或代碼&#xff0…

Java代碼輸出到txt文件(申請專利貼源碼的必備利器)

最近公司在申請專利&#xff0c;編寫不少文檔&#xff0c;項目的代碼量實在是過于龐大。如果一個一個的復制粘貼雖然能夠完成&#xff0c;但是對于程序員而言實在沒有這個必要。shell或者python就能解決這個問題。由于我個人對于shell和python不是非常熟練的情況下&#xff0c;…

【算法】 - 動態規劃 + 位運算

題目描述 思路1: 寫一個返回2進制中1數量的函數countOne遍歷0到num,對每一個數使用countOne,并將結果保存到res中返回 var countBits function (num) {let res new Array(num 1).fill(0);for (let i 0; i < num; i) {res[i] countOne(i.toString(2));}return res; };…