JVM系列六(自定義插入式註解器).
一、概述
從前面 文章 中我們可以瞭解到,javac 的三個步驟中,程式設計師唯一能干預的就是註解處理器部分,註解處理器類似於編譯器的外掛,在這些外掛裡面,可以讀取、修改、新增抽象語法樹中的任意元素。因此,只要有足夠的創意,程式設計師可以通過自定義插入式註解處理器來實現許多原本只能在編碼中完成的事情。我們常見的 Lombok、Hibernate Validator 等都是基於自定義插入式註解器來實現的。
要實現註解處理器首先要做的就是繼承抽象類 javax.annotation.processing.AbstractProcessor,然後重寫它的 process() 方法,process() 方法是 javac 編譯器在執行註解處理器程式碼時要執行的過程。
public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
該方法有兩個引數,“annotations” 表示此處理器所要處理的註解集合;“roundEnv” 表示當前這個 Round 中的語法樹節點,每個語法樹節點都表示一個 Element(javax.lang.model.element.ElementKind 可以檢視到相關 Element)。
該方法的返回值是一個 boolean 型別,通知編譯器這個 Round 中的程式碼是否發生變化,是否需要構建新的 JavaCompiler 例項,是否需要開啟新的 Round。
除了 process() 方法外,還有兩個可以配合使用的 Annotations:
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes 表示註解處理器對哪些註解感興趣,“*” 表示對所有的註解都感興趣;@SupportedSourceVersion 指出這個註解處理器可以處理最高哪個版本的 Java 程式碼。
另外 AbstractProcessor 還有一個很常用的例項變數 “processingEnv”,它在 init() 方法執行的時候建立,它代表了註解處理器框架提供的一個上下文環境,要建立新的程式碼、向編譯器輸出資訊、獲取其他工具類等都需要用到這個例項變數。
public synchronized void init(ProcessingEnvironment processingEnv) {
// ...
}
tips:每一個註解處理器在執行的時候都是單例的。
二、自定義
我們現在要自定義一個插入式註解器 — NameCheckProcessor,它要做的事情是對 Java 程式命名進行檢查,檢查的規則如下:
- 類(或介面):符合駝式命名法,首字母大寫
- 方法:符合駝式命名法,首字母小寫
欄位:
- 類或例項變數:符合駝式命名法,首字母小寫
- 常量要求全部是大寫字母或下劃線構成,並且第一個字元不能是下劃線。
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {
private NameChecker nameChecker;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
nameChecker = new NameChecker(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
for (Element element : roundEnv.getRootElements()) {
nameChecker.checkNames(element);
}
}
return false;
}
}
從上面程式碼可以看到,NameCheckProcessor 最高能處理 JDK1.8 的程式碼,並對所有的註解都感興趣,而在 process() 方法中是把當前 Round 中的每一個 RootElement 傳遞到一個名為 NameChecker 的檢查器中檢查邏輯,process() 方法返回 false,因為它只是檢查命名規範,並未改變語法樹。
NameChecker 負責檢查命名規範,這是它 github程式碼連結,哈哈,具體程式碼就不在文章裡貼了,再貼一下文章就沒法看了都。
NameChecker 通過一個繼承 javax.lang.model.util.ElementScanner8 的 NameCheckScanner 類,以 Visitor 模式來完成對語法樹的遍歷,分別執行 visitType()、visitExecutable() 和 visitVariable() 來訪問類、方法和欄位,這 3 個 visit 方法對各自的命名規則做相應的檢查。
自定義註解器寫好了,那麼問題來了,註解器怎麼用呢?
- 通過 javac 命令的 “-processor” 引數來執行編譯時需要附帶的註解處理器,如果有多個註解處理器的話,用逗號進行分割。
- 通過 JAVA SPI 載入。在 resources 目錄下新增 META-INF/services 目錄,目錄內新增名為 javax.annotation.processing.Processor 的檔案,內容是自定義註解器的全類名,一行表示一個註解器。
三、應用
這裡主要介紹下利用 Java SPI 載入自定義註解器的方式,我們的目標是生成一個 jar 包,類似於 Lombok ,這樣其它應用一旦引用了這個 jar 包,自定義註解器就能自動生效了。
1. 生成註解器 jar 包
首先,我們先來看下自定義註解器的目錄結構,在 javax.annotation.processing.Processor 檔案中是自定義註解器的全類名。
org.jvm.processor.name.check.NameCheckProcessor
然後,在 pom.xml 中配置 proc 屬性,如果不配置的話,會有個 WARNNING 提示— 找不到 processor 的異常。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
最後,愉快的使用 mvn clean install 來 build 你的註解器 jar 包吧!
2. 使用註解器 jar 包
首先,在 pom.xml 中引入註解器 jar 包的依賴
<dependency>
<groupId>org.jvm.processor</groupId>
<artifactId>processor</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
其實,進行到這一步你的自定義註解器已經生效了!另外,maven-compiler-plugin 支援手動對需要執行的註解器進行設定。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>
org.jvm.processor.name.check.NameCheckProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
tips: maven-compile-plugin 等編譯外掛會吞掉 javax.annotation.processing.Messager 所列印的東西,而手動使用 javac 編譯器則不會。
四、總結
上文的註解器案例主要參考《深入理解 JVM 虛擬機器》,後來又在網上看了一些大家的實踐,覺得還挺開拓思維的,大家可以試試看。
- https://blog.csdn.net/qiaoyl113/article/details/80063602
- https://www.jianshu.com/p/554c5491bea6
自定義註解器這東西,類似於攔截器功能,只要思維都大膽,感覺能玩出花來!
上文的演示的程式碼可參見:https://github.com/JMCuixy/jvm-d