轉:https://www.cnblogs.com/yaoxiaowen/p/6753964.html
若注解在運行時(Runtime)通過反射機制來處理注解,既然是Runtime,那么總會有效率上的損耗,如果我們能夠在編譯期(Compile time)就能處理注解,那自然更好,而很多框架其實都是在編譯期處理注解,比如大名鼎鼎的bufferknife,這個過程並不復雜,只需要我們自定義注解處理器(Annotation Processor)就可以了。(Annotation Processor下文有些地方直接簡稱處理器,不要理解成cpu那個處理器)。
在Compile time注解就能起作用,這才是真正體現注解價值的地方,不過自定義Compile time的注解處理器也沒什么神秘的。注解處理器是編譯器(javac)的一個工具,它用來在編譯時掃描和處理注解。我們可以自定義一個注解,並編寫和注冊對應的處理器。在寫法上它其實就是我們自定義一個類,該類 extends javax.annotation.processing.AbstractProcessor
, AbstractProcessor
是一個abstract的基類。它以我們寫好的java源碼或者編譯好的代碼做為輸入,然后就可以通過處理器代碼來實現我們所希望的輸出了,比如輸出一份新的java代碼,此時注解管理器就以遞歸的形式進行多趟處理,直到把代碼(包括你手寫的代碼,以及注解處理器生成的代碼)中所有的注解都被處理完畢。
我們已經寫好的代碼固然是不能修改了,但是這並不影響通過注解處理器來生成新的代碼。還以bufferknife為例,寫findViewById實在太無聊了,所以我們就使用了bufferknife的注解方式省略這個過程。
public class TestMainActivity extends BaseActivity { @BindView(R.id.mainSwitchGoneBtn) Button goneBtn; ....... }
但是實際上呢,是bufferknife通過其注解處理器器來生成了相應的代碼,它生成的文件是這樣的:
public class TestMainActivity_ViewBinding<T extends TestMainActivity> implements Unbinder { protected T target; @UiThread public TestMainActivity_ViewBinding(T target, View source) { this.target = target; target.goneBtn = Utils.findRequiredViewAsType(source, R.id.mainSwitchGoneBtn, "field 'goneBtn'", Button.class); } }
所以bufferknife就是通過這種方式來麻煩了自己,方便了我們。
注解處理器是運行在它自己的虛擬機jvm當中的,也就是說,javac啟動了一個完整的java虛擬機來運行注解處理器,這點非常重要,因為這說明你編寫的注解處理器代碼,和你寫的其他java代碼是沒什么區別的。不管是你使用的API,還是設計時的思想,編碼習慣,甚至你想使用的其他第三方類庫,框架等,都是一樣的。
認識處理器
前面就說過,我們自定義的過程,就是extends AbstractProcessor
,先來看看這個抽象處理器類。
package com.yaoxiaowen.testprocessor; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; public class TestProcessor extends AbstractProcessor{ /** * 每個注解處理器都必須有一個空的構造方法(父類已經實現了),這個init方法會被構造器調用, * 並傳入一個 ProcessingEnvironment 參數,該參數提供了很多工具類, * 比如 Elements, Filer, Messager, Types * @author www.yaoxiaowen.com */ @Override public synchronized void init(ProcessingEnvironment env) { // TODO Auto-generated method stub super.init(env); } /** * 這個方法在父類中是abstract的,所以子類必須實現。 * 這個方法就是相當於 注解處理器的 入口 main()方法,我們說在編譯時,對注解進行的處理, * 比如對注解的掃描,評估和處理,以及后續的我們要做的其他操作。(比如生成其他java代碼文件), * 都是在這里發生的。 * * 參數RoundEnvironment可以讓我們查出包含特定注解的被注解元素。 * @author www.yaoxiaowen.com */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // TODO Auto-generated method stub return false; } /** * 這個方法雖然在父類當中不是 abstract的,但是我們也必須實現。 * 因為該方法的作用是指定我們要處理哪些注解的, * 比如你想處理注解MyAnnotation,可是該處理器怎么知道你想處理MyAnnotation,而不是OtherAnnotation呢。 * 所以你要在這里指明,你需要處理的注解的全稱。 * * 返回值是一個字符串的集合,包含着本處理器想要處理的注解類型的合法全稱。 * @author www.yaoxiaowen.com */ @Override public Set<String> getSupportedAnnotationTypes() { // TODO Auto-generated method stub return super.getSupportedAnnotationTypes(); } /** * 本方法用來指明你支持的java版本, * 不過一般使用 SourceVersion.latestSupported() 就可以了。 */ @Override public SourceVersion getSupportedSourceVersion() { // TODO Auto-generated method stub return super.getSupportedSourceVersion(); } }
這幾個主要方法,在代碼片段的注釋已經寫的很清楚了。
我們使用TestProcessor.java這個處理器的目的就是分析處理java代碼,而代碼是遵循一定的結構規范的,代碼文件被讀取后,各個字符串會被分解成token進行處理,而javac的編譯器首先將java代碼分解為抽象語法樹(AST)。而這個結構,在處理器內部,其實是被表示成這樣的:
package com.example; // PackageElement public class Foo { // TypeElement private int a; // VariableElement private Foo other; // VariableElement public Foo () {} // ExecuteableElement public void setA ( // ExecuteableElement int newA // TypeElement ){} }
處理器在處理代碼時,其實就是對抽象語法樹進行遍歷操作,分解出每一個的類,方法,屬性等,然后再將這些元素的內容進行處理。
而實際上,這些PackageElement,VariableElement等元素模型都是在一個專門的類包中javax.lang.model
。javax.lang.model
用來為 Java 編程語言建立模型的包的類和層次結構。 此包及其子包的成員適用於語言建模、語言處理任務和 API(包括但並不僅限於注釋處理框架)。
繼承 AbstractProcessor實現自定義處理器
我們現在通過繼承AbstractProcessor來實現一個小demo。
流程和功能如下:我們定義了一個注解SQLString
,然后實現注解處理器 DbProcessor
。該注解處理器功能很簡單,就是生成一個文件,將實現了SQLString
的屬性元素的相關內容寫入到這個文件(比如所在類的名字,屬性名,所設置的注解的值)。
我們先自定義一個注解
package com.yaoxiaowen.comp.proce.db; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.lang.model.element.Element; /** * 該注解的 使用范圍是 屬性(域) 上 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface SQLString { int value() default 0; String name() default ""; }
然后再來定義注解處理器
package com.yaoxiaowen.comp.proce.db; import java.io.File; import java.io.FileWriter; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.tools.Diagnostic; /** * @author www.yaoxiaowen.com */ public class DbProcessor extends AbstractProcessor{ private Messager messager; private int count = 0; private int forCount = 0; private StringBuilder generateStr = new StringBuilder(); @Override public synchronized void init(ProcessingEnvironment env) { // TODO Auto-generated method stub super.init(env); messager = env.getMessager(); String logStr = "enter init(), 進入 init()"; printMsg(logStr); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { // TODO Auto-generated method stub String logStr = "enter process(), 進入process"; //用來 存儲 (className, 輸出語句) 這種結構 Map<String, String> maps = new HashMap<>(); //得到 使用了 SQLString注解的元素 Set<? extends Element> eleStrSet = env.getElementsAnnotatedWith(SQLString.class); count++; for (Element eleStr : eleStrSet){ //因為我們知道SQLString元素的使用范圍是在域上,所以這里我們進行了強制類型轉換 VariableElement eleStrVari = (VariableElement)eleStr; forCount++; // 得到該元素的封裝類型,也就是 包裹它的父類型 TypeElement enclosingEle = (TypeElement)eleStrVari.getEnclosingElement(); String className = enclosingEle.getQualifiedName().toString(); generateStr.append("className = " + className); generateStr.append("\t fieldName = " + eleStrVari.getSimpleName().toString()); //得到在元素上,使用了注解的相關情況 SQLString sqlString = eleStrVari.getAnnotation(SQLString.class); generateStr.append("\t annotationName = " + sqlString.name()); generateStr.append("\t annotationValue = " + sqlString.value()); generateStr.append("\t forCount=" + forCount); generateStr.append("\n"); } generateStr.append("test File yaowen"); generateStr.append("\t count=" + count); generateFile(generateStr.toString()); return true; } @Override public Set<String> getSupportedAnnotationTypes() { // TODO Auto-generated method stub Set<String> strings = new TreeSet<>(); strings.add("com.yaoxiaowen.comp.proce.db.SQLString"); return strings; } @Override public SourceVersion getSupportedSourceVersion() { // TODO Auto-generated method stub return SourceVersion.latestSupported(); } //將內容輸出到文件 private void generateFile(String str){ try { //這是mac環境下的路徑 File file = new File( "/Users/yw/code/dbCustomProcFile"); FileWriter fw = new FileWriter(file); fw.append(str); fw.flush(); fw.close(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); printMsg(e.toString()); } } private void printMsg(String msg){ messager.printMessage(Diagnostic.Kind.ERROR, msg); } }
結合着注釋,我們知道,這個處理器的功能就是將一些信息輸出到 /Users/yw/code/dbCustomProcFile
這個文件中。
我在代碼中使用了 javax.annotation.processing.Messager
來輸出一些log信息,因為這個過程是在編譯時輸出的,所以System.out.println()
就沒用了,這個輸出信息是給使用了該處理器的第三方程序員看的,不是給該處理器的作者看的。
比如demo當中的log代碼,在最后成功的打包成jar,在另一個項目中使用時(Android Studio環境下,Eclipse我愣是沒找到哪個窗口輸出編譯信息),編譯時期輸出信息如下:
....... :app:compileSc_360DebugJavaWithJavac 注: enter init, 進入init 注: enter process, 進入process 注: Creating DefaultRealmModule 注: enter process, 進入process 注: enter process, 進入process 注: 某些輸入文件使用了未經檢查或不安全的操作。 注: 有關詳細信息, 請使用 -Xlint:unchecked 重新編譯。 :app:generateJsonModelSc_360Debug UP-TO-DATE :app:externalNativeBuildSc_360Debug ......
添加注冊信息
處理器的代碼雖然寫完了,但是這還沒完呢,剩下還有非常重要的步驟,那就是添加注冊信息。因為注解處理器是屬於javac的一個平台級的功能,所以我們的使用方式是將代碼打包成jar的形式,這樣就可以在其他第三方項目當中使用了。而在打包jar之前,則要在項目中添加注冊信息。
先看一下這個目錄的結構:
(eclipse)注冊的步驟如下:
1,選中工程,鼠標右鍵,New -> Source Folder,創建 resources文件夾,然后依次通過New -> Folder 創建兩個文件夾 : META-INF,services
2,在services文件夾下,New -> File, 創建一個文件,javax.annotation.processing.Processor。在文件中,輸入自定義的處理器的全名:com.yaoxiaowen.comp.proce.db.DbProcessor
輸入之后記得鍵入回車一下。
其實這個手動注冊的過程,也是可以不用我們麻煩的。google開發了一個注解工具AutoService,我們可以直接在處理器代碼上使用。類似下面這樣:
/** * @author www.yaoxiaowen.com */ @AutoService(Processor.class) public class DbProcessor extends AbstractProcessor{ .......
這個注解工具自動生成META-INF/services/javax.annotation.processing.Processor文件,文件里還包含了處理器的全名:com.yaoxiaowen.comp.proce.db.DbProcessor
看到這里,你也許會比較震驚,我們在注解處理器的代碼中也可以使用注解。
那么此時請再看看本文開頭的那句話
注解處理器是運行在它自己的虛擬機jvm當中的,也就是說,javac啟動了一個完整的java虛擬機來運行注解處理器.....
做完這些,我們的項目就已經完成了,下面要做的就是打包成jar了。
打包和使用jar(eclipse為例)
1: 打包jar
前面說過,編譯期的注解處理器是平台級的功能,是要注冊給javac的, 所以需要打包成jar, 我們的項目打包的名字是
AnnoCustomProce.jar
關於具體的打包過程,參見gif圖(這是從鴻洋大神的博客上學習到的)。
2: 建立新項目
eclipse下的java項目,新建立一個lib文件夾,然后將AnnoCustomProce.jar手動拷貝到這個目錄下。
3: 引用包,並啟用annotation processor。
具體操作見gif圖。
要注意一下兩個gif圖中的各種選項和配置。
使用注解
現在呢,已經大功告成了。下面就是使用了。
新建一個類,使用我們的注解。
public class AnnoCreateFile { @SQLString(name="yw") String filed; @SQLString(name="yaow", value=1) String name; /** * @author www.yaoxiaowen.com */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("hello world"); } }
當編譯完這個項目時(eclipse默認就是Build Automatically),我們就能在 /Users/yw/code/
目錄下找到 dbCustomProcFile 文件了,打開這個文件,內容如下:
className = com.yaoxiaowen.testjar.AnnoCreateFile fieldName = filed annotationName = yw
annotationValue = 0 forCount=1 className = com.yaoxiaowen.testjar.AnnoCreateFile fieldName = name
annotationName = yaow annotationValue = 1 forCount=2 test File yaowen count=1test File yaowen count=2
大功告成,我們成功的實現了一個能夠在編譯時期起作用的自定義注解處理器。
當然,這個demo沒有什么實際作用,它的功能也非常簡單,但是了解了這個過程,我們在實際需求當中,就可以通過類似的方式來實現想要的功能了。
很多時候,我們都是希望注解處理器是來輸出java代碼的,既然是代碼,那么總有格式的,這就不像簡單的文件那樣進行輸出了,輸出java代碼,一般使用一個類庫:javapoet。而我們如果在注解處理器中引入了第三方的類庫,那么將其打包成jar的過程,就和我們演示的有所不同。這點需要自行google。另外,如果想對java源碼進行游刃有余的處理,那么需要對於javax.lang.model
包下的各種Elements,工具之類的比較熟悉。具體的api,需要參考oracle的文檔.