一、概述 從前面 "文章" 中我們可以瞭解到,javac 的三個步驟中,程式員唯一能幹預的就是註解處理器部分,註解處理器類似於編譯器的插件,在這些插件裡面,可以讀取、修改、添加抽象語法樹中的任意元素。因此,只要有足夠的創意,程式員可以通過自定義插入式註解處理器來實現許多原本只能在編碼中完成的事情。我 ...
一、概述
從前面 文章 中我們可以瞭解到,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://github.com/JMCuixy/jvm-demo