@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/
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章