寫在前面
該文章是繼Java註解解析-基礎+執行時註解(RUNTIME)之後,使用註解處理器處理CLASS
註解的文章。通過完整的Demo例子介紹整個註解處理器的搭建流程以及注意事項,你將知道如何去搭建自己的註解處理器。前提是你知道如何去寫自定義註解,不清楚的可以點選我上面的連結哦~
介紹
顧名思義註解處理器就是javac
包中專門用來處理註解的工具。所有的註解處理器都必須繼承抽象類AbstractProcessor
然後重寫它的幾個方法。註解處理器是執行在它自己的JVM中。javac
啟動一個完整Java虛擬機器來執行註解處理器,這意味著你可以使用任何你在其他java
應用中使用的的東西。其中抽象方法process
是必須要重寫的,再該方法中註解處理器可以遍歷所有的原始檔,然後通過RoundEnvironment
類獲取我們需要處理的註解所標註的所有的元素,這裡的元素可以代表包,類,介面,方法,屬性等,具體的解釋放在下一章節,因為這裡面牽扯到的東西實在是太多了。再處理的過程中可以利用特定的工具類自動生成特定的.java檔案或者.class檔案,來幫助我們處理自定義註解。
下面開始搭建
1.建立
首先註解處理器需要javax包的支援,我們的Android環境下是訪問不到javax包的,除非我們自己配置。
//app:build.gradle中加入依賴,一定要使用provided files來引入.
provided files(`/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/jre/lib/rt.jar`)
複製程式碼
所以我們需要建立Java Library包來提供javax環境,另外註解處理器要被打包進jar包裡面才能被系統識別,這就是選用ava Library的原因,目前註解註解框架均是如此。
建立好Module之後就可以寫我們的自定義的註解處理器了。首先需要繼承抽象類AbstractProcessor
,然後重寫process()
方法。該方法是核心方法,該方法將遍歷原始碼,找出我們想要註解標註的元素。單單重寫這一個方法是不夠的, 在我們的開發中往往需要重寫init()
,getSupportedSourceVersion()
,getSupportedAnnotationTypes()
這幾個方法就可以了。另外再Java7及其以後我們還可以使用註解@SupportedSourceVersion()
和@SupportedAnnotationTypes()
去替代上面的方法,見於該註解有Java版本的限制,所以還是建議直接重寫方法為好。
public class AnnotationTwoProcessor extends AbstractProcessor {
private Messager messager; //用於列印日誌
private Elements elementUtils; //用於處理元素
private Filer filer; //用來建立java檔案或者class檔案
//該方法再編譯期間會被注入一個ProcessingEnvironment物件,該物件包含了很多有用的工具類。
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnvironment.getMessager();
elementUtils = processingEnvironment.getElementUtils();
filer = processingEnvironment.getFiler();
}
/**
* 該方法將一輪一輪的遍歷原始碼
* @param set 該方法需要處理的註解型別
* @param roundEnvironment 關於一輪遍歷中提供給我們呼叫的資訊.
* @return 改輪註解是否處理完成 true 下輪或者其他的註解處理器將不會接收到次型別的註解.用處不大.
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
/**
* 返回我們Java的版本.
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
/**
* 返回我們將要處理的註解
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<>();
annotataions.add(MyAnnotion.class.getCanonicalName());
return annotataions;
}
}
複製程式碼
2.註冊
註冊註解處理器的方法有兩種:
第一種: 處理器必須被打包進一個jar包裡面,這也是為什麼要建立一個Java Module。該jar包需要有一個特定路徑resources/META-INF/services
的檔案javax.annotation.processing.Processor
,該檔案的路徑和名稱都是特定的,然後將我們宣告註解處理器的全路徑寫到該檔案中,這樣Java虛擬機器會自動找該路徑下中我們宣告的處理器。
我們再建立資料夾的時候一定要確定其命名的準確性。建立檔案的時候直接右鍵->new file,保證我們的檔案的名字為javax.annotation.processing.Processor
。
建立成功之後我們將自定義的註解處理器的全路徑寫到該檔案裡面。
這樣問題來了,如我我們寫了多個註解處理器該怎麼宣告呢?接著看。如果我們一個Module裡面宣告瞭多個註解處理器,再該檔案中宣告的時候每個註解處理器要換行宣告,執行的順序就按宣告的順序去執行。這裡需要對註解處理器的執行順序解釋一下,再編譯期間並不是一個註解處理器完全的處理結束再開始下一個的,而是在掃描一輪原始碼 的時候註冊的第一個處理器先執行一輪,然後註冊的第二個處理器開始執行然後。。。三個。。四個。第二輪的時候還是註冊的第一個處理器先執行,然後第二個。。。三個。。。這裡面的深刻解釋會在下一篇講解,這篇只是使用。
第二種: 當覺得第一種方法配置繁瑣的時候就會有新的更簡潔的方式出現。谷歌公司推出的使用註解去註冊註解處理器。
-
新增依賴,可以去GitHub上面查詢最新版本。
implementation `com.google.auto.service:auto-service:1.0-rc4` 複製程式碼
-
我們就可以使用了,它的作用和我們手寫的作用是一樣的。不過註釋的處理器的處理順序跟你類建立的順序是一致的,跟方法一中檔案宣告的順序不一樣。
@AutoService(Processor.class) public class AnnotationTwoProcessor extends AbstractProcessor { } 複製程式碼
總的來說方式1的註冊方式目前僅在EventBus裡面有用到,方式2還是推薦使用的,比如:Arouter,BufferKnife等流行框架都是採用的這種方式註冊的,方便快捷。
3.APP引用註解處理器
再引用註解處理器的Module的時候坑其實挺多的。首先我們得確保處理器所在的jar包會新增到我們執行的專案中,註解處理器才會被得到執行,這裡呢因為原始碼不清楚,所以只好自己去試了。Application引入註解處理器包的時候並不像我們引入其它的Android Module一樣,這裡列舉三種種引入方法。
-
plugin: `com.android.application`
我們可以使用implementation,compile,api直接引用註解處理器的module,但是會有一個編譯錯誤:
Error:Execution failed for task `:app:javaPreCompileDebug`. > Annotation processors must be explicitly declared now. The following dependencies on the compile classpath are found to contain annotation processor. Please add them to the annotationProcessor configuration. - annotation_processor.jar (project :annotation_processor) Alternatively, set android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true to continue with previous behavior. Note that this option is deprecated and will be removed in the future. See https://developer.android.com/r/tools/annotation-processor-error-message.html for more details. 複製程式碼
我們需要加一段程式碼,程式碼位置如下所示,這樣就可以愉快的引入註解處理器了:
android { defaultConfig { javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } } } } 複製程式碼
另外我們可以用annotationProcessor引入註解處理器,這個引入方式是為了替換Gradle版本2.2之前的apt引入方法。該引入方式專門用來處理註解處理器。使用該引入方式的時候不會出現錯誤提示,也是Android中推薦使用的引入方法,該方式也是主流方式。
annotationProcessor project(`:annotation_processor`) 複製程式碼
-
apply plugin: `com.android.library`
前提module一定要被android application引入。module裡面引入註解處理器的module的話,基本跟android application中一致,這裡說一下兩個不同點annotationProcessor和implementation方式的引入都不會執行註解處理器,真實的原理並不清楚,只能猜測是application才能正真的處理註解,所以得把依賴暴露出去。這點再android library的module中一定得注意。不太建議該方式引入。
-
apply plugin: `java-library`
前提java library一定要被android application引入。宣告這樣的module 的話我們就可以直接引入註解處理器了,也不用擔心用什麼方式引入。不過這種場景不太多。
如何確保你的註解處理器已經註冊成功了。首先你已經自定義好了一個註解,並且該註解已經用到了你的程式碼裡面。如下程式碼所示,你已經設定了我們要處理的是那種型別的註解(程式碼1所示),然後再我們的process方法裡面打上日誌(程式碼2所示),然後點選Android Studio
的Make Project
按鈕,之後開啟Gradle Console視窗看build資訊,當你發現資訊中列印了程式碼2所示的資訊之後就表明你的註解處理器已經執行起來了。如果沒有列印資訊的話嘗試 clean一下專案然後重新Make Project。如果發現沒有列印日誌的話,嘗試檢視註解處理器是否已經註冊和註解處理器所在的module是否被android application成功引入。
@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnvironment.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//程式碼2
messager.printMessage(Diagnostic.Kind.NOTE,"日誌開始---------------");
return false;
}
//程式碼1
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportedOptions = new HashSet<>();
supportedOptions.add(MyAnnotion.class.getCanonicalName());
return supportedOptions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
}
複製程式碼
4.自動生成Java檔案
走到該步驟的時候,你要確保你的註解處理器已經正常的執行。我們使用註解處理器處理原始碼註解的目的,就是再編譯期間完成我們對某個註解的處理工作。比如:BufferKnife再編譯期間會在使用特定註解的檔案路徑生成***—ViewBinding的原始檔去處理特定註解。這裡用一個Demo去演示如何生成程式碼:
先看我的註解:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.LOCAL_VARIABLE})
public @interface MyAnnotion {
String value() default "ssssss";
}
複製程式碼
在我的MainActivity上面使用註解:
@MyAnnotion()
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
複製程式碼
接著使用註解處理器去處理註解,生成對應的MainActivity_ViewBinding原始檔。
public class AnnotationProcessor extends AbstractProcessor {
private Messager messager;
private Elements elementUtils;
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnvironment.getMessager();
elementUtils = processingEnvironment.getElementUtils();
filer = processingEnvironment.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printMessage(Diagnostic.Kind.NOTE,"日誌開始---------------");
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(MyAnnotion.class);
for (Element element:elementsAnnotatedWith) {
if(element.getKind() == ElementKind.CLASS){
TypeElement typeElement = (TypeElement) element;
PackageElement packageElement = elementUtils.getPackageOf(element);
String packagePath = packageElement.getQualifiedName().toString();
String className = typeElement.getSimpleName().toString();
try {
JavaFileObject sourceFile = filer.createSourceFile(packagePath + "." + className + "_ViewBinding", typeElement);
Writer writer = sourceFile.openWriter();
writer.write("package "+packagePath +";
");
writer.write("import "+packagePath+"."+className+";
");
writer.write("public class "+className+"_ViewBinding"+" {
");
writer.write("
");
writer.append(" public "+className +" targe;
");
writer.write("
");
writer.append("}");
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
messager.printMessage(Diagnostic.Kind.NOTE,"日誌結束---------------");
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportedOptions = new HashSet<>();
supportedOptions.add(MyAnnotion.class.getCanonicalName());
return supportedOptions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
}
複製程式碼
結果展示:
注意一下生成的位置!我們可以直接再我們正常的程式碼中應用到該檔案,因為該檔案是會生成class檔案的。
5.總結
該文章只是介紹瞭如何搭建起一個Java註解處理器,沒有更深入的去講解AbstractProcessor類以及我們再處理註解的過程中用到的各種類的API。當然接下來的文章就會詳細的介紹註解處理器所使用到的類,方法,屬性等的用法和意義,這一定是史上最全的註解處理器API。之後你會更加隨心所欲的去構建自己的註解框架。
下章節 史上最全的註解處理器API指導!
歡迎大家留言提出文章中出現的錯誤以及不理解的地方,會在第一時間進行更正和解答~