您好,登錄后才能下訂單哦!
本篇內容主要講解“Java重寫AST插件的方法是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java重寫AST插件的方法是什么”吧!
1. 介紹
隨著Java 6的發布,java編譯器已經有了開源的版本了。開源的編譯器是OpenJDK項目的一部分,可以從Java編譯器小組的網站下載 http://www.openjdk.org/groups/compiler/ 。然而就這篇文檔的例子來說,任何Java 6的版本都是可用的,因為這些例子并不會重新編譯編譯器,他們只是擴展編譯器的功能。
這篇文章介紹了Java編譯器的內在實現。首先我們給出java編譯器所包含的編譯步驟,然后我們在編寫兩個例子。兩個例子都使用到了編譯器里面的插件機制,也就是JSR269所描述的機制。然而,這兩個例子卻超出了JSR269的范圍。把JSR對象和編譯器對接,我們實現了AST的重寫。我們的例子里面,沒有使用assertion(斷言)語句,而使用了if-throw語句。
2. Java編譯器的內核
這個部分概括了OpenJDK里面的java編譯器的編譯步驟和對應的注釋。這個小節包含了一個簡短的介紹。
編譯的過程是由定義在com.sun.tools.javac.main里面的Java Compiler類來決定的。當編譯器以默認的編譯參數編譯時,它會執行以下步驟:
a) Parse: 讀入一堆*.java源代碼,并且把讀進來的符號(Token)映射到AST節點上去。
b) Enter: 把類的定義放到符號表(Symbol Table)中去。
c) Process annotations: 可選的。處理編譯單元(compilation units)里面所找到的標記(annotation)。
d) Attribute: 為AST添加屬性。這一步包含名字解析(name resolution),類型檢測(type checking)和常數折疊(constant fold)。
e) Flow: 為前面得到的AST執行流分析(Flow analysis)操作。這個步驟包含賦值(assignment)的檢查和可執行性(reachability)的檢查。
f) Desugar: 重寫AST, 并且把一些復雜的語法轉化成一般的語法。
g) Generate: 生成源文件或者類文件。
wps_clip_image-12054_thumb
2.1 Parse
想要Parse文件,編譯器要用到com.sun.tools.javac.parser.*里面的類。作為***步,詞法分析器(lexical analyzer)把輸入的字符流(character sequence)映射成一個符號流(token sequence)。然后Parser再把生成的符號流映射成一個抽象語法樹(AST)
2.2 Enter
在這個步驟中,編譯器會找到當前范圍(enclosing scope)中發現的所有的定義(definitions),并且把這些定義注冊成符號(symbols)。Enter這個步驟又分為以下兩個階段:
在***個階段,編譯器會注冊所有類的符號,并且把這寫符號和相應的范圍(scope)聯系在一起。實現方法是使用一個Visitor(訪問者)類,由上而下的遍歷AST,訪問所有的類,包括類里面的內部類。Enter給每一個類的符號都添加了一個MemberEnter對象,這個對象是由第二個階段來調用的
在第二個階段中,這些類被MemberEnter對象所完成(completed,即完成類的成員變量的Enter)。首先,MemberEnter決定一個類的參數,父類和接口。然后這些符號被添加進了類的范圍中。不像前一個步驟,這個步驟是懶惰執行的。類的成員只有在被訪問時,才加入類的定義中的。這里的實現,是通過安裝一個完成對象(member object)到類的符號中。這些對象可以在需要時調用member-enter
***,enter把所有的頂層類(top-level classes)放到一個todo-queue中,
2.3 Process Annotations
如果存在標記處理器,并且編譯參數里面指定要處理標記,那么這個過程就會處理在某個編譯單元里面的標記。JSR269定義了一個接口,可以用來寫這種Annotation處理插件。然而,這個接口的功能非常有限,并且不能用Collective Behavior擴展這種語言。主要的限制是JSR269不提供子方法的反射調用。
2.4 Attribute
為Enter階段生成的所有AST添加屬性。應當注意,Attribte可能會需要額外的文件被解析(Parse),通過SourceCompleter加入到符號表中。
大多數的環境相關的分析都是發生在這個階段的。這些分析包括名稱解析,類型檢查,常數折疊。這些都是子任務。有些子任務調用下列的一些類,但也可能調用其他的。
l Check:這是用于類型檢查的類。當有完成錯誤(completion error)或者類型錯誤時,它就會報錯。
l Resovle: 這是名字解析的類。如果解析失敗,就會報錯。
l ConstFold: 這是參數折疊類。常數折疊用于簡化在編譯時的常數表達式。
l Infer:類參數引用的類。
2.5 Flow
這個階段會對添加屬性后的類,執行數據流的檢查。存活性分析(liveness analysis) 檢查是否每個語句都可以被執行到。異常分析(Excepetion analysis) 檢查是豆每個被拋出的異常都是聲明過的,并且這些異常是否都會被捕獲。確定行賦值(definite assignment)分析保證每個變量在使用時已經被賦值。而確定性不賦值(definite unassignment)分析保證final變量不會被多次賦值。
2.6 Desugar
除去多余的語法,像內部類,類的常數,assertion斷言語句,foreach循環等。
2.7 Generate
這是最終的階段。這個階段生成許多源文件或者類文件。到底是生成源文件還是類文件取決于編譯選項。
3. 什么是JSR 269
Annotation(標記)是java 5里面引進來的,用于在源代碼里面附加元信息(meta-information).Java 6則進一步加強了標記的處理功能,即JSR269. JSR269,即插入式標記處理API,為java編譯器添加了一個插件機制。有了JSR269,就有能力為java編譯器寫一個特定的標記處理器了。
JSR269有兩組基本API,一組用于對java語言的建模,一組用于編寫標記處理器。這兩組API分別存在于javax.lang.model.* 和 javax.annotation.processing里面。JSR269的功能是通過以下的java編譯選項來調用的。
-proc:{none,only} 是否執行Annotation處理或者編譯
-processor <classes> 指定標記處理器的名字。這個選項將繞過默認的標記處理器查找過程
-processorpath <path> 指定標記處理器的位置
標記處理在javac中時默認開啟的。如果要是只想處理標記,而不想編譯生成類文件的話,用 –proc:only 選項既即可。
4. 如何用Javac打印出“Hello World!”
在這***個例子里面,我們些一個簡單的標記處理器,用于在編譯的時候打印“Hello World!”.我們用編譯器的內部消息機制來打印“hello world”。
首先,我們定義如下HelloWorld標記。
public @interface HelloWorld{ }
添加一個Dummy類使用以上的標記
@HelloWorld public class Dummy{ }
標記處理可能會發生很輪。每一輪處理器只處理特定的一些標記,并且生成的源文件或者類文件,交給下一輪來處理。如果處理器被要求只處理特定的某一輪,那么他也會處理后續的那些次,包括***一輪,就算***一輪沒有可以處理的標記。處理器可能也會去處理被這個工具生成的文件。
后一個方法處理前一輪生成的標記類型,并且返回是否這些標記會聲明。如果返回是True,那么后續的處理器就不會去處理它們。如果返回是false,那么后續處理器會繼續處理它們。一個處理器可能總是返回同樣的邏輯值,或者是根據選項改變結果。為了要寫一個標記處理器,我們用一個子類來繼承AbstractProcessor,并且用SupportedAnnotationTyps 和SupportedSourceVersion標記這個子類。這個子類必須要復寫這兩個方法:
l public synchronized void init(ProcessingEnvironment processingEnv)
l public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv)
這兩個方法都是在標記處理過程中被java編譯器調用的。***個方法用來初始化插件,只被調用一次。而第二個方法每一輪標記處理都會被調用,并且在所有處理都結束后還會調用一次。
我們的簡單的HelloWorldProcessors是這樣生成的:
import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; @SupportedAnnotationTypes("HelloWorld") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class HelloWorldProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "Hello Worlds!"); } return true; } }
第八行注冊了HelloWorld的標記處理器。也就是說,當標記出現是,就會有一系列的程序被自動調用。第九行設置了標記所支持的源代碼版本。
第12到15行復寫了初始化方法, 目前為止,我們只是調用父類的方法。
第17到24行復寫了處理方法。這個方法是由一些列被標記的程序元素來調用的。這個方法在每一輪處理時,都會調用,并且在***會多出一輪,用于對空集合的元素的處理。這樣,我們可以由一個簡單的if語句,使得***多出的那一輪什么事情都不做。在其他輪中,我們只打印一個hello world消息。我們不用System.out.print,二十使用編譯器的消息框架來打印一個消息(note類型的)。其他可能的類型是警告(warning)或者錯誤(error)。
這個方法返回true,如果你想要聲明元素已經被處理過了。
要運行這個例子,執行:
javac HelloWorldProcessor.java
javac -processor HelloWorldProcessor *.java
這個應該會輸出:
Note: Hello World!
5. 如何巧妙利用JSR269來重寫AST
在這個例子中,我們深入到編譯器自身的實現細節中去。我們利用JSR269做一些超出它本身的事情—重寫AST。這個處理器會把每一個Assertion語句替換成一個throw語句。也就是說,每當有以下語句出現時
assert cond: detail;
會被替換成:
If(!cond) throw new AssertionError(detail);
后面的這個語句不會生成assert的字節碼,而是生成一個普通的if語句,帶有一個throw重句。結果就算你的虛擬機沒有激活assertions功能時,assertions的檢查還是會被執行。這個功能對各種庫是非常有用的,因為你寫庫的時候,是沒有辦法控制用戶的VM設置的。
再次,我們還是先繼承AbstractProcessor。然而,這次我們不會針對某一個特殊的標記,而是用“*”這個符號來表示對所有的源代碼都調用處理器。
@SupportedAnnotationTypes("*") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class ForceAssertions extends AbstractProcessor { }
初始化方法如下:
private int tally; private Trees trees; private TreeMaker make; private Name.Table names; @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); trees = Trees.instance(env); Context context = ((JavacProcessingEnvironment) env).getContext(); make = TreeMaker.instance(context); names = Name.Table.instance(context); tally = 0; }
我們使用處理環境(ProcessingEnvironment)來獲得對編譯器一些組件的引用。在編譯器里面,在每次調用編譯器時都會有一個處理環境(ProcessingEnvironment)。在編譯器中,我們使用Component.instance(context)來獲得對組件的引用。
我們使用的組件如下:
l Trees – JSR269的一個工具類,用于聯系程序元素和樹節點。比如,對于一個方法元素,我們可以獲得這個元素對應的AST樹節點。
l TreeMaker – 編譯器的內部組件,是用于創建樹節點的工廠類。工廠類里面方法的命名方式跟Javac源代碼里面的方法是統一的。
l Name.Table – 另一個編譯器的內部組件。Name類是編譯器內部字符串的一個抽象。為了提高效率,Javac使用了哈希字符串。
請注意,在第39行,我們把處理環境(ProcessingEnvironment)強制轉換成了編譯器的內部類型。
***,我們把一個計數器初始化成0.這個計數器是用來記錄發生替換的數量。
處理方法如下:
@Override 46 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { Set<? extends Element> elements = roundEnv.getRootElements(); for (Element each : elements) { if (each.getKind() == ElementKind.CLASS) { JCTree tree = (JCTree) trees.getTree(each); TreeTranslator visitor = new Inliner(); tree.accept(visitor); } } } else processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, tally + " assertions inlined."); return false; }
我們遍歷所有的程序元素,為每一個類都重寫AST。在第51行,我們把JSR269的樹節點轉換成編譯器內部的樹節點。這兩種樹節點的不同之處在于,JSR269節點是停留在方法層的(即方法method是最基本的元素,不會再細分下去),而內部的AST節點,是所有元素(包括方法以下的)都可以訪問的。我們要訪問每一個語句,所以需要訪問到AST的所有節點。
樹的轉換是通過繼承TreeTranslator來完成的,TreeTranslator本身是繼承自TreeVisitor的。這些類都不是JSR269的一部分。所以,從這里開始,我們所寫的所有代碼都是在編譯器內部工作的。
在第57行,是else部分,用于報告處理過的assertion語句數量。這個語句只有在***一輪處理才會執行。
Inliner這個類實現了AST重寫。Inliner繼承了TreeTranslator,并且是標記處理器的一個內部類。注意,TreeTranslator本身是不會轉換任何節點的。
private class Inliner extends TreeTranslator {
}
為了轉換assertion語句,我們需要復寫默認的TreeTranslator.visitAssert (JCAssert) 方法,如下所示:
@Override public void visitAssert(JCAssert tree) { super.visitAssert(tree); JCStatement newNode = makeIfThrowException(tree); result = newNode; tally++; }
正在轉換的節點會被當做參數傳入到方法中。在第67行,轉換的結果,通過賦值給變量TreeTranslator.result而返回。
按照慣例,一個轉換方法應該這樣生成:
l 調用父類的轉換方法,以確保轉換可以被應用到自己點上面去。
l 執行真正的轉換
l 把轉換結果賦值給TreeTranslator.result。結果的類型不一定要和傳進來的參數的類型一樣。相反,只要java編譯器允許,我們可以返回任何類型的節點。這里TreeTranslator本身沒有限制類型,但是如果返回了錯誤的類型,那么就很有在后續過程中產生災難性后果。
我們寫一個私有函數來實現轉換,makeIfThrowException:
private JCStatement makeIfThrowException(JCAssert node) { // make: if (!(condition) throw new AssertionError(detail); List<JCExpression> args = node.getDetail() == null ? List.<JCExpression> nil() : List.of(node.detail); JCExpression expr = make.NewClass( null, null, make.Ident(names.fromString("AssertionError")), args, null); return make.If( make.Unary(JCTree.NOT, node.cond), make.Throw(expr), null); }
這個方法傳入一個assertion語句,返回一個if語句。我們可以這樣做,事因為不管是assertion還是if,他們都是語句(statement),所以在java的語法中是等價的。Java中沒有明文規定,禁止用if語句來代替assertion語句。
makeIfThrowException是用于AST重寫的方法。我們使用TreeMaker來創建新的樹節點。如果有這樣的一個表達式:
assert cond:detail;
我們就可以替換成下面的形式:
If(!cond) throw new AssertionErrror(detal);
在第73到75行,我們考慮到了detail被省略的情況。在76到81行,我們創建了一個AST節點,這個節點的作用是創建AssertionError。在第79行,我們使用Name.Table來把字符串“AssertionError”變成編譯器內部的字符串。在80行,我們再傳入73到75行創建的參數args。第77,78和81行傳入了null值,因為這個節點既沒有外部實例,也沒有類型參數,也不是在匿名類內部。
在第83行,我們對assertion的條件做了一個Not操作。84行,我們創建了一個throw表達式,***,在82到85行,我們把所有的東西都放到了if語句中。
注意:List類是java編譯器中另外一個令人印象深刻的實現。編譯器用了它自己的數據類型來實現List,而不是使用java集合框架(Java Collection Framework)。List和Pair數據類的實現,都用到了Lisp語言里面所謂的cons。Pairs是這樣實現的:
public class Pair<A, B> { public final A fst; public final B snd; public Pair(A fst, B snd) { this.fst = fst; this.snd = snd; } ... }
而List是這樣實現的:
public class List<A> extends AbstractCollection<A> implements java.util.List<A> { public A head; public List<A> tail; public List(A head, List<A> tail) { this.tail = tail; this.head = head; } ... }
并且有許多靜態的方法,可以很方便的創建List:
l List.nil()
l List.of(A)
l List.of(A,A)
l List.of(A,A,A)
l List.of(A,A,A,A...)
Pair也是一樣:
l Pair.of(A,B)
同樣,非傳統的命名方式也帶來了更漂亮的代碼
不像傳統java中用的代碼:
List list = new List();
list.add(a);
list.add(b);
list.add(c);
而現在只需要寫:
List.of(a, b, c);
5.1 運行AST重寫
為了展示AST重寫,我們使用:
public class Example { public static void main(String[] args) { String str = null; assert str != null : "Must not be null"; } }
并且執行:
javac ForceAssertions.java
javac -processor ForceAssertions Example.java
就會產生這樣的輸出:
Note: 1 assertions inlined
現在,我們我們我們禁用assertion,再執行例子:
java -disableassertions Example
得到:
Exception in thread "main" java.lang.AssertionError: Must not be null at Example.main(Example.java:1)
利用編譯器的選項 –printsource,我們甚至可以得到重寫過后的AST,并且以Java源代碼的方式顯示出來。要注意的是,我們必須重定向輸出,否者原來的源文件會被覆蓋了。
執行:
javac -processor ForceAssertions -printsource -d gen Example.java
產生結果:
public class Example { public Example() { super(); } public static void main(String[] args) { String str = null; if (!(str != null)) throw new AssertionError("Must not be null"); } }
可以發現,第9行已經被重寫過了,第3到5行加入了一個默認的構造函數。
5.2 如何把標記處理器注冊成服務
Java提供了一個注冊服務的機制。如果一個標記處理器被注冊成了一個服務,編譯器就會自動的去找到這個標記處理器。注冊的方法是,在classpath中找到一個叫META-INF/services的文件夾,然后放入一個javax.annotation.processing.Processor的文件。文件格式是很明顯的,就是要包含要注冊的標記處理器的完整名稱。每個名字都要占單獨的一行。
5.3 進一步的閱讀
Erni在他的本科畢業設計中描述了一個更復雜的編譯器修改。他不是依賴JSR269,而是直接在編譯過程中的幾個點進行直接修改。
到此,相信大家對“Java重寫AST插件的方法是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。