摘要:對語法樹的掃描,同樣提供了掃描器。詞法分析過程如下圖所示語法分析,即根據語法由序列生成抽象語法樹,對應實現類為。生成的抽象語法樹如下圖所示的實現原理依賴開發(fā)的典型的第三方庫有,代碼自動生成的和,代碼檢查的和,編譯階段完成依賴注入的等。
原文:http://nullwy.me/2017/04/java...
如果覺得我的文章對你有用,請隨意贊賞
javac 是 Java 代碼的編譯器 [openjdk, oracle ],初學 Java 的時候就應該接觸過。本筆記整理一些 javac 相關的高級用法。
javac 命令行javac 命令行工具,官方文檔有完整的使用說明,doc。當然也可以,運行 javac -help 或 man javac 查看幫助信息。下面是經典的 hello world 代碼:
package com.test.javac; public class Hello { public static void main(String[] args) { System.out.println("hello world"); } }
編譯與運行
$ tree # 代碼目錄結構 . ├── pom.xml └── src └── main ├── java │ └── com │ └── test │ └── javac │ └── Hello.java └── resources $ mkdir -p target/classes # 創(chuàng)建 class 文件的存放目錄 $ javac src/main/java/com/test/javac/Hello.java -d target/classes $ java -cp "target/classes" com.test.javac.Hello hello worldjavac 相關 API
除了使用命令行工具編譯 Java 代碼,JDK 6 增加了規(guī)范 JSR-199 和 JSR-296,開始還提供相關的 API。Java 編譯器的實現代碼和 API 的整體結構如圖所示[doc]:
綠色標注的包是官方 API(Official API),即 JSR-199 和 JSR-296,黃色標注的包為(Supported API),紫色標注的包代碼全部在 com.sun.tools.javac.* 包下,為內部 API(Internal API)和編譯器的實現類。完整的包說明如下:
javax.annotation.processing - 注解處理 (JSR-296)
javax.lang.model - 注解處理和編譯器 Tree API 使用的語言模型 (JSR-296)
javax.lang.model.element - 語言元素
javax.lang.model.type - 類型
javax.lang.model.util - 語言模型工具
javax.tools - Java 編譯器 API (JSR-199)
com.sun.source.* - 編譯器 Tree API,提供 javac 工具使用的抽象語法樹 AST 的只讀訪問
com.sun.tools.javac.* - 內部 API 和編譯器的實現類
全部源碼都位于 langtools 下,在 JDK 中的 tools.jar 可以找到。com.sun.tools.javac.* 包下全部代碼中都有Sun標注的警告:
This is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice.Java 編譯器 API
首先,看下 JSR-199 引入的 Java 編譯器 API。在沒有引入 JSR-199 前,只能使用 javac 源碼提供內部 API,上文提到的使用命令 javac 編譯 Hello.java 的等價寫法如下:
import com.sun.tools.javac.main.Main; public class JavacMain { public static void main(String[] args) { Main compiler = new Main("javac"); compiler.compile(new String[]{"src/main/java/com/test/javac/Hello.java", "-d", "target/classes"}); } }
JSR-199 的等價寫法:
import javax.tools.*; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.Arrays; public class Jsr199Main { public static void main(String[] args) throws URISyntaxException, IOException { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector可插拔式注解處理 APIdiagnostics = new DiagnosticCollector<>(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); File file = new File("src/main/java/com/test/javac/Hello.java"); Iterable extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(file)); compiler.getTask(null, fileManager, diagnostics, Arrays.asList("-d", "target/classes"), null, compilationUnits).call(); for (Diagnostic extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) { System.out.format("Error on line %d in %s %s ", diagnostic.getLineNumber(), diagnostic.getSource().toUri(), diagnostic.getMessage(null)); } fileManager.close(); } }
JSR-269(Pluggable Annotation Processing API)。要理解注解處理,需要先了解 Java 代碼的編譯過程,編譯過程如下圖所示 [doc]:
整個過程就是
源代碼經過詞法解析和語法解析,生成語法樹。然后將遇到的類符號以及在類內部定義的符號填充入(enter)符號表。
所有注解處理器會被處理,若處理器生成新的代碼或 class 文件,編譯過程會重新開始,直到沒有新的文件生成。
語義分析和代碼生成,即類型檢查、控制流分析、泛型的類型擦除、去除語法糖、字節(jié)碼生成等操作。
代碼示例:
@SupportedSourceVersion(SourceVersion.RELEASE_7) @SupportedAnnotationTypes("*") public class VisitProcessor extends AbstractProcessor { private MyScanner scanner; @Override public void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.scanner = new MyScanner(); } public boolean process(Set extends TypeElement> types, RoundEnvironment environment) { if (!environment.processingOver()) { for (Element element : environment.getRootElements()) { scanner.scan(element); } } return true; } public class MyScanner extends ElementScanner7{ public Void visitType(TypeElement element, Void p) { System.out.println("類 " + element.getKind() + ": " + element.getSimpleName()); return super.visitType(element, p); } public Void visitExecutable(ExecutableElement element, Void p) { System.out.println("方法 " + element.getKind() + ": " + element.getSimpleName()); return super.visitExecutable(element, p); } public Void visitVariable(VariableElement element, Void p) { if (element.getEnclosingElement().getKind() == ElementKind.CLASS) { System.out.println("字段 " + element.getKind() + ": " + element.getSimpleName()); } return super.visitVariable(element, p); } } }
編譯器 API 的 CompilationTask 的 setProcessors 方法可以傳入注解處理器,代碼如下(被編譯的 java 文件就是 VisitProcessor.java):
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollectordiagnostics = new DiagnosticCollector<>(); VisitProcessor processor = new VisitProcessor(); StandardJavaFileManager manager = compiler.getStandardFileManager(diagnostics, null, null); File file = new File("src/main/java/com/test/proc/visit/VisitProcessor.java"); Iterable extends JavaFileObject> sources = manager.getJavaFileObjectsFromFiles(Arrays.asList(file)); CompilationTask task = compiler.getTask(null, manager, diagnostics, Arrays.asList("-d", "target/classes"), null, sources); task.setProcessors(Arrays.asList(processor)); task.call(); manager.close();
或者也通過 javac 命令編譯,指定注解處理器通過 -processor 參數選項。另外,若 classpath 中存在目錄 META-INF/services/(或 jar 包中存在),并有 javax.annotation.processing.Processor 文件,在該文件中填寫的注解處理器類名(多個的話,換行填寫),編譯器就會自動使用這下填寫的注解處理器進行注解處理。
運行輸出結果如下:
類 CLASS: VisitProcessor 類 CLASS: MyScanner 方法 CONSTRUCTOR:方法 METHOD: visitType 方法 METHOD: visitExecutable 方法 METHOD: visitVariable 方法 CONSTRUCTOR: 字段 FIELD: scanner 方法 METHOD: init 方法 METHOD: process
可以看到整個類文件被掃描,包括內部類以及全部方法、構造方法和字段。注解處理在填充符號表之后進行,ElementScanner 類掃描的 Element 其實就是符號 Symbol。從 Symbol 類的定義可以看到這一點。
public abstract class Symbol extends AnnoConstruct implements Element
填充符號表前一步是構造語法樹。對語法樹的掃描,com.sun.source.* 同樣提供了掃描器TreeScanner。使用 TreeScanner 掃描 java 代碼的示例代碼如下所示:
@SupportedSourceVersion(SourceVersion.RELEASE_7) @SupportedAnnotationTypes("*") public class VisitTreeProcessor extends AbstractProcessor { private Trees trees; private MyScanner scanner; @Override public void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.trees = Trees.instance(processingEnv); this.scanner = new MyScanner(); } public boolean process(Set extends TypeElement> types, RoundEnvironment environment) { if (!environment.processingOver()) { for (Element element : environment.getRootElements()) { TreePath path = trees.getPath( element ); scanner.scan(path, null); } } return true; } public class MyScanner extends TreePathScanner{ public Tree visitClass(ClassTree node, Void p) { System.out.println("類 " + node.getKind() + ": " + node.getSimpleName()); return super.visitClass(node, p); } public Tree visitMethod(MethodTree node, Void p) { System.out.println("方法 " + node.getKind() + ": " + node.getName()); return super.visitMethod(node, p); } public Tree visitVariable(VariableTree node, Void p) { if (this.getCurrentPath().getParentPath().getLeaf() instanceof ClassTree) { System.out.println("字段 " + node.getKind() + ": " + node.getName()); } return super.visitVariable(node, p); } } }
運行輸出結果如下:
類 CLASS: VisitTreeProcessor 方法 METHOD:字段 VARIABLE: trees 字段 VARIABLE: scanner 方法 METHOD: init 方法 METHOD: process 類 CLASS: MyScanner 方法 METHOD: 方法 METHOD: visitClass 方法 METHOD: visitMethod 方法 METHOD: visitVariable
需要注意的是,獲取語法樹是通過工具類 Trees 的 getTree 方法完成的。另外,可以看到 com.sun.source.* 包下暴露的 API 對語法樹只能做只讀操作,功能有限,要想修改語法樹必須使用 javac 的內部 API。
javac 內部 API針對語句 int y = x + 1; 的詞法分析,即根據詞法將字符序列轉換為 token 序列,對應實現類為 com.sun.tools.javac.parser.Scanner。詞法分析過程如下圖所示 ref [RednaxelaFX ]:
語法分析,即根據語法由 token 序列生成抽象語法樹,對應實現類為 com.sun.tools.javac.parser.Parser。生成的抽象語法樹如下圖所示:
依賴 JSR-269 開發(fā)的典型的第三方庫有,代碼自動生成的 Lombok 和 Google Auto,代碼檢查的 Checker 和 Google Error Prone,編譯階段完成依賴注入的 Google Dagger 2 等。
現在看下 Lombok 的實現源碼。Lombok 提供 @NonNull, @Getter, @Setter, @ToString, @EqualsAndHashCode, @Data等注解,自動生成常見樣板代碼 boilerplate,解放開發(fā)效率。Lombok 支持 javac 和 ecj (Eclipse Compiler for Java)。對于 javac 編譯器對應的注解處理器是 LombokProcessor,然后經過一些處理過程,每個注解都會有特定的 handler 來處理,@NonNull 對應 HandleNonNull、@Getter 對應 HandleGetter、@Setter 對應 HandleSetter、@ToString 對應 HandleToString、@EqualsAndHashCode 對應HandleEqualsAndHashCode、@Data 對應 HandleData。閱讀這些 handler 的實現,可以看到樣板代碼的生成依賴的就是 com.sun.tools.javac.* 包。
為了試驗和學習 javac 內部 API 的功能,本人嘗試重新實現 Lombok 的 @Data 注解,簡單實現了自動生成 getter 和 setter 的功能,代碼參見 github,使用 @Data 的代碼見 link。
參考資料The Java programming language Compiler Group http://openjdk.java.net/group...
2008-03 The Hacker"s Guide to Javac http://scg.unibe.ch/archive/p...
2015-09 Java Compiler API https://www.javacodegeeks.com...
2015-09 Java Annotation Processors https://www.javacodegeeks.com...
2011-05 How does lombok work? http://stackoverflow.com/q/61...
莫樞 RednaxelaFX :JVM分享——Java程序的編譯、加載與執(zhí)行 http://www.valleytalk.org/201...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74629.html
摘要:使用方法能以簡單的注解形式來簡化代碼,提高開發(fā)人員的開發(fā)效率。能通過注解的方式,在編譯時自動為屬性生成構造器方法。出現的神奇就是在源碼中沒有和方法,但是在編譯生成的字節(jié)碼文件中有和方法。沒法實現多種參數構造器的重載。 1 Lombok背景介紹 官方介紹如下: Project Lombok makes java a spicier language by addi...
摘要:提高編碼效率使代碼更簡潔消除冗長代碼避免修改字段名字時忘記修改方法名提高下逼格以上就是的優(yōu)點,當然,的優(yōu)點遠遠不止以上幾點,使用,你可以更加優(yōu)雅高效的編輯代碼。實戰(zhàn)完成了上述準備之后,就可以愉快的使用進行編碼了。接下來是使用簡化后的代碼。 Lombok介紹 近來偶遇一款擼碼神器,介紹給大家~相信許多小伙伴都深有體會,POJO類中的千篇一律的getter/setter,construct...
摘要:可標注在類內部生成一個名為類名的內部類,用于快速構建。流程是這樣的編譯源代碼,并生成語法樹尋找實現了的代碼,并調用。尋找被標注了注解的類,修改生成的語法樹。將語法樹生成為字節(jié)碼就到這里了它還具備很多好用的功能,你可以去這里看看。 能做什么? 在使用lombok之前: public class Book { private Integer id; private St...
摘要:使用,簡化代碼為了簡化與,提供了一種機制,幫助我們自動生成這些樣板代碼。但是,在實際項目中,完全沒有使用到。源碼審查是一個源碼審查工具。最新版已經支持的全部注解,不再認為是沒有使用的變量。 一個典型的 Java 類 public class A { private int a; private String b; public int getA() { ret...
摘要:本文參考自來自周志明深入理解虛擬機第版,拓展內容建議讀者可以閱讀下這本書。和構造方法一一對應,是同一概念在兩個級別的含義收斂的操作自動保證執(zhí)行父類的執(zhí)行語句塊初始化類變量字符串加操作替換為或的操作 showImg(https://segmentfault.com/img/remote/1460000016240419?w=3876&h=3614); 本文參考自來自周志明《深入理解Jav...
閱讀 2655·2021-11-24 10:44
閱讀 1896·2021-11-22 13:53
閱讀 1907·2021-09-30 09:47
閱讀 3704·2021-09-22 16:00
閱讀 2431·2021-09-08 09:36
閱讀 2312·2019-08-30 15:53
閱讀 2790·2019-08-30 15:48
閱讀 976·2019-08-30 15:44