@lombok注解背后的原理是什么,讓我們走近自定義Java注解處理器


本文介紹了如何自定義Java注解處理器及涉及到的相關知識,看完本文可以很輕松看懂並理解各大開源框架的注解處理器的應用。

《游園不值》
應憐屐齒印蒼苔 ,小扣柴扉久不開 。
春色滿園關不住 ,一枝紅杏出牆來 。
-宋,葉紹翁

本文首發:http://yuweiguocn.github.io/

關於自定義Java注解請查看自定義注解

本文已授權微信公眾號:鴻洋(hongyangAndroid)原創首發。

基本實現

實現一個自定義注解處理器需要有兩個步驟,第一是實現Processor接口處理注解,第二是注冊注解處理器。

實現Processor接口

通過實現Processor接口可以自定義注解處理器,這里我們采用更簡單的方法通過繼承AbstractProcessor類實現自定義注解處理器。實現抽象方法process處理我們想要的功能。

public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
}

除此之外,我們還需要指定支持的注解類型以及支持的Java版本通過重寫getSupportedAnnotationTypes方法和getSupportedSourceVersion方法:

public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(CustomAnnotation.class.getCanonicalName());
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

對於指定支持的注解類型,我們還可以通過注解的方式進行指定:

@SupportedAnnotationTypes({"io.github.yuweiguocn.annotation.CustomAnnotation"})
public class CustomProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        return false;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

因為Android平台可能會有兼容問題,建議使用重寫getSupportedAnnotationTypes方法指定支持的注解類型。

注冊注解處理器

最后我們還需要將我們自定義的注解處理器進行注冊。新建res文件夾,目錄下新建META-INF文件夾,目錄下新建services文件夾,目錄下新建javax.annotation.processing.Processor文件,然后將我們自定義注解處理器的全類名寫到此文件:

io.github.yuweiguocn.processor.CustomProcessor

上面這種注冊的方式太麻煩了,谷歌幫我們寫了一個注解處理器來生成這個文件。
github地址:https://github.com/google/auto
添加依賴:

compile 'com.google.auto.service:auto-service:1.0-rc2'

添加注解:

@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
    ...
}

搞定,體會到注解處理器的強大木有。后面我們只需關注注解處理器中的處理邏輯即可。

我們來看一下最終的項目結構:

基本概念

抽象類中還有一個init方法,這是Processor接口中提供的一個方法,當我們編譯程序時注解處理器工具會調用此方法並且提供實現ProcessingEnvironment接口的對象作為參數。

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
}

我們可以使用ProcessingEnvironment獲取一些實用類以及獲取選項參數等:

方法 說明
Elements getElementUtils() 返回實現Elements接口的對象,用於操作元素的工具類。
Filer getFiler() 返回實現Filer接口的對象,用於創建文件、類和輔助文件。
Messager getMessager() 返回實現Messager接口的對象,用於報告錯誤信息、警告提醒。
Map<String,String> getOptions() 返回指定的參數選項。
Types getTypeUtils() 返回實現Types接口的對象,用於操作類型的工具類。

元素

Element元素是一個接口,表示一個程序元素,比如包、類或者方法。以下元素類型接口全部繼承自Element接口:

類型 說明
ExecutableElement 表示某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括注解類型元素。
PackageElement 表示一個包程序元素。提供對有關包及其成員的信息的訪問。
TypeElement 表示一個類或接口程序元素。提供對有關類型及其成員的信息的訪問。注意,枚舉類型是一種類,而注解類型是一種接口。
TypeParameterElement 表示一般類、接口、方法或構造方法元素的形式類型參數。
VariableElement 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數。

如果我們要判斷一個元素的類型,應該使用Element.getKind()方法配合ElementKind枚舉類進行判斷。盡量避免使用instanceof進行判斷,因為比如TypeElement既表示類又表示一個接口,這樣判斷的結果可能不是你想要的。例如我們判斷一個元素是不是一個類:

if (element instanceof TypeElement) { //錯誤,也有可能是一個接口
}

if (element.getKind() == ElementKind.CLASS) { //正確
    //doSomething
}

下表為ElementKind枚舉類中的部分常量,詳細信息請查看官方文檔。

類型 說明
PACKAGE 一個包。
ENUM 一個枚舉類型。
CLASS 沒有用更特殊的種類(如 ENUM)描述的類。
ANNOTATION_TYPE 一個注解類型。
INTERFACE 沒有用更特殊的種類(如 ANNOTATION_TYPE)描述的接口。
ENUM_CONSTANT 一個枚舉常量。
FIELD 沒有用更特殊的種類(如 ENUM_CONSTANT)描述的字段。
PARAMETER 方法或構造方法的參數。
LOCAL_VARIABLE 局部變量。
METHOD 一個方法。
CONSTRUCTOR 一個構造方法。
TYPE_PARAMETER 一個類型參數。

類型

TypeMirror是一個接口,表示 Java 編程語言中的類型。這些類型包括基本類型、聲明類型(類和接口類型)、數組類型、類型變量和 null 類型。還可以表示通配符類型參數、executable 的簽名和返回類型,以及對應於包和關鍵字 void 的偽類型。以下類型接口全部繼承自TypeMirror接口:

類型 說明
ArrayType 表示一個數組類型。多維數組類型被表示為組件類型也是數組類型的數組類型。
DeclaredType 表示某一聲明類型,是一個類 (class) 類型或接口 (interface) 類型。這包括參數化的類型(比如 java.util.Set )和原始類型。TypeElement 表示一個類或接口元素,而 DeclaredType 表示一個類或接口類型,后者將成為前者的一種使用(或調用)。
ErrorType 表示無法正常建模的類或接口類型。
ExecutableType 表示 executable 的類型。executable 是一個方法、構造方法或初始化程序。
NoType 在實際類型不適合的地方使用的偽類型。
NullType 表示 null 類型。
PrimitiveType 表示一個基本類型。這些類型包括 boolean、byte、short、int、long、char、float 和 double。
ReferenceType 表示一個引用類型。這些類型包括類和接口類型、數組類型、類型變量和 null 類型。
TypeVariable 表示一個類型變量。
WildcardType 表示通配符類型參數。

同樣,如果我們想判斷一個TypeMirror的類型,應該使用TypeMirror.getKind()方法配合TypeKind枚舉類進行判斷。盡量避免使用instanceof進行判斷,因為比如DeclaredType既表示類 (class) 類型又表示接口 (interface) 類型,這樣判斷的結果可能不是你想要的。

TypeKind枚舉類中的部分常量,詳細信息請查看官方文檔。

類型 說明
BOOLEAN 基本類型 boolean。
INT 基本類型 int。
LONG 基本類型 long。
FLOAT 基本類型 float。
DOUBLE 基本類型 double。
VOID 對應於關鍵字 void 的偽類型。
NULL null 類型。
ARRAY 數組類型。
PACKAGE 對應於包元素的偽類型。
EXECUTABLE 方法、構造方法或初始化程序。

創建文件

Filer接口支持通過注解處理器創建新文件。可以創建三種文件類型:源文件、類文件和輔助資源文件。

1.創建源文件

JavaFileObject createSourceFile(CharSequence name,
                                Element... originatingElements)
                                throws IOException

創建一個新的源文件,並返回一個對象以允許寫入它。文件的名稱和路徑(相對於源文件的根目錄輸出位置)基於該文件中聲明的類型。如果聲明的類型不止一個,則應該使用主要頂層類型的名稱(例如,聲明為 public 的那個)。還可以創建源文件來保存有關某個包的信息,包括包注解。要為指定包創建源文件,可以用 name 作為包名稱,后跟 ".package-info";要為未指定的包創建源文件,可以使用 "package-info"。

2.創建類文件

JavaFileObject createClassFile(CharSequence name,
                               Element... originatingElements)
                               throws IOException

創建一個新的類文件,並返回一個對象以允許寫入它。文件的名稱和路徑(相對於類文件的根目錄輸出位置)基於將寫入的類型名稱。還可以創建類文件來保存有關某個包的信息,包括包注解。要為指定包創建類文件,可以用 name 作為包名稱,后跟 ".package-info";為未指定的包創建類文件不受支持。

3.創建輔助資源文件

FileObject createResource(JavaFileManager.Location location,
                          CharSequence pkg,
                          CharSequence relativeName,
                          Element... originatingElements)
                          throws IOException

創建一個用於寫入操作的新輔助資源文件,並為它返回一個文件對象。該文件可以與新創建的源文件、新創建的二進制文件或者其他受支持的位置一起被查找。位置 CLASS_OUTPUT 和 SOURCE_OUTPUT 必須受支持。資源可以是相對於某個包(該包是源文件和類文件)指定的,並通過相對路徑名從中取出。從不太嚴格的角度說,新文件的完全路徑名將是 location、 pkg 和 relativeName 的串聯。

對於生成Java文件,還可以使用Square公司的開源類庫JavaPoet,感興趣的同學可以了解下。

打印錯誤信息

Messager接口提供注解處理器用來報告錯誤消息、警告和其他通知的方式。

注意:我們應該對在處理過程中可能發生的異常進行捕獲,通過Messager接口提供的方法通知用戶。此外,使用帶有Element參數的方法連接到出錯的元素,用戶可以直接點擊錯誤信息跳到出錯源文件的相應行。如果你在process()中拋出一個異常,那么運行注解處理器的JVM將會崩潰(就像其他Java應用一樣),這樣用戶會從javac中得到一個非常難懂出錯信息。

方法 說明
void printMessage(Diagnostic.Kind kind, CharSequence msg) 打印指定種類的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e) 在元素的位置上打印指定種類的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a) 在已注解元素的注解鏡像位置上打印指定種類的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v) 在已注解元素的注解鏡像內部注解值的位置上打印指定種類的消息。

配置選項參數

我們可以通過getOptions()方法獲取選項參數,在gradle文件中配置選項參數值。例如我們配置了一個名為yuweiguoCustomAnnotation的參數值。

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ yuweiguoCustomAnnotation : 'io.github.yuweiguocn.customannotation.MyCustomAnnotation' ]
            }
        }
    }
}

在注解處理器中重寫getSupportedOptions方法指定支持的選項參數名稱。通過getOptions方法獲取選項參數值。

public static final String CUSTOM_ANNOTATION = "yuweiguoCustomAnnotation";

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   try {
       String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
       if (resultPath == null) {
           ...
           return false;
       }
       ...
   } catch (Exception e) {
       e.printStackTrace();
       ...
   }
   return true;
}

@Override
public Set<String> getSupportedOptions() {
   Set<String> options = new LinkedHashSet<String>();
   options.add(CUSTOM_ANNOTATION);
   return options;
}

處理過程

Java官方文檔給出的注解處理過程的定義:注解處理過程是一個有序的循環過程。在每次循環中,一個處理器可能被要求去處理那些在上一次循環中產生的源文件和類文件中的注解。第一次循環的輸入是運行此工具的初始輸入。這些初始輸入,可以看成是虛擬的第0次的循環的輸出。這也就是說我們實現的process方法有可能會被調用多次,因為我們生成的文件也有可能會包含相應的注解。例如,我們的源文件為SourceActivity.class,生成的文件為Generated.class,這樣就會有三次循環,第一次輸入為SourceActivity.class,輸出為Generated.class;第二次輸入為Generated.class,輸出並沒有產生新文件;第三次輸入為空,輸出為空。

每次循環都會調用process方法,process方法提供了兩個參數,第一個是我們請求處理注解類型的集合(也就是我們通過重寫getSupportedAnnotationTypes方法所指定的注解類型),第二個是有關當前和上一次 循環的信息的環境。返回值表示這些注解是否由此 Processor 聲明,如果返回 true,則這些注解已聲明並且不要求后續 Processor 處理它們;如果返回 false,則這些注解未聲明並且可能要求后續 Processor 處理它們。

public abstract boolean process(Set<? extends TypeElement> annotations,
                                RoundEnvironment roundEnv)

獲取注解元素

我們可以通過RoundEnvironment接口獲取注解元素。process方法會提供一個實現RoundEnvironment接口的對象。

方法 說明
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 返回被指定注解類型注解的元素集合。
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) 返回被指定注解類型注解的元素集合。
processingOver() 如果循環處理完成返回true,否則返回false。

示例

了解完了相關的基本概念,接下來我們來看一個示例,本示例只為演示無實際意義。主要功能為自定義一個注解,此注解只能用在public的方法上,我們通過注解處理器拿到類名和方法名存儲到List集合中,然后生成通過參數選項指定的文件,通過此文件可以獲取List集合。

自定義注解:

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}

注解處理器中關鍵代碼:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   try {
       String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
       if (resultPath == null) {
           messager.printMessage(Diagnostic.Kind.ERROR, "No option " + CUSTOM_ANNOTATION +
                   " passed to annotation processor");
           return false;
       }

       round++;
       messager.printMessage(Diagnostic.Kind.NOTE, "round " + round + " process over " + roundEnv.processingOver());
       Iterator<? extends TypeElement> iterator = annotations.iterator();
       while (iterator.hasNext()) {
           messager.printMessage(Diagnostic.Kind.NOTE, "name is " + iterator.next().getSimpleName().toString());
       }

       if (roundEnv.processingOver()) {
           if (!annotations.isEmpty()) {
               messager.printMessage(Diagnostic.Kind.ERROR,
                       "Unexpected processing state: annotations still available after processing over");
               return false;
           }
       }

       if (annotations.isEmpty()) {
           return false;
       }

       for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
           if (element.getKind() != ElementKind.METHOD) {
               messager.printMessage(
                       Diagnostic.Kind.ERROR,
                       String.format("Only methods can be annotated with @%s", CustomAnnotation.class.getSimpleName()),
                       element);
               return true; // 退出處理
           }

           if (!element.getModifiers().contains(Modifier.PUBLIC)) {
               messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must be public", element);
               return true;
           }

           ExecutableElement execElement = (ExecutableElement) element;
           TypeElement classElement = (TypeElement) execElement.getEnclosingElement();
           result.add(classElement.getSimpleName().toString() + "#" + execElement.getSimpleName().toString());
       }
       if (!result.isEmpty()) {
           generateFile(resultPath);
       } else {
           messager.printMessage(Diagnostic.Kind.WARNING, "No @CustomAnnotation annotations found");
       }
       result.clear();
   } catch (Exception e) {
       e.printStackTrace();
       messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in CustomProcessor: " + e);
   }
   return true;
}

private void generateFile(String path) {
   BufferedWriter writer = null;
   try {
       JavaFileObject sourceFile = filer.createSourceFile(path);
       int period = path.lastIndexOf('.');
       String myPackage = period > 0 ? path.substring(0, period) : null;
       String clazz = path.substring(period + 1);
       writer = new BufferedWriter(sourceFile.openWriter());
       if (myPackage != null) {
           writer.write("package " + myPackage + ";\n\n");
       }
       writer.write("import java.util.ArrayList;\n");
       writer.write("import java.util.List;\n\n");
       writer.write("/** This class is generated by CustomProcessor, do not edit. */\n");
       writer.write("public class " + clazz + " {\n");
       writer.write("    private static final List<String> ANNOTATIONS;\n\n");
       writer.write("    static {\n");
       writer.write("        ANNOTATIONS = new ArrayList<>();\n\n");
       writeMethodLines(writer);
       writer.write("    }\n\n");
       writer.write("    public static List<String> getAnnotations() {\n");
       writer.write("        return ANNOTATIONS;\n");
       writer.write("    }\n\n");
       writer.write("}\n");
   } catch (IOException e) {
       throw new RuntimeException("Could not write source for " + path, e);
   } finally {
       if (writer != null) {
           try {
               writer.close();
           } catch (IOException e) {
               //Silent
           }
       }
   }
}

private void writeMethodLines(BufferedWriter writer) throws IOException {
   for (int i = 0; i < result.size(); i++) {
       writer.write("        ANNOTATIONS.add(\"" + result.get(i) + "\");\n");
   }
}

編譯輸出:

Note: round 1 process over false
Note: name is CustomAnnotation
Note: round 2 process over false
Note: round 3 process over true

獲取完整代碼:https://github.com/yuweiguocn/CustomAnnotation

關於上傳自定義注解處理器到jcenter中,請查看上傳類庫到jcenter

很高興你能閱讀到這里,此時再去看EventBus 3.0中的注解處理器的源碼,相信你可以很輕松地理解它的原理。

注意:如果你clone了工程代碼,你可能會發現注解和注解處理器是單獨的module。有一點可以肯定的是我們的注解處理器只需要在編譯的時候使用,並不需要打包到APK中。因此為了用戶考慮,我們需要將注解處理器分離為單獨的module。

參考

作者:於衛國
鏈接:https://www.jianshu.com/p/50d95fbf635c/
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM