Java註解和註解解析器深耕,架構師必會

語言: CN / TW / HK

本文將介紹學習 元資料->元註解->執行時註解->編譯時註解處理器- >自定義框架Demo

什麼是元資料(metadata)

元資料由metadata譯來,所謂的元資料就是“關於資料的資料”,更通俗的說就是描述資料的資料,對資料及資訊資源的描述性資訊.比如說一個文字檔案,有建立時間,建立人,檔案大小等資料,這都可以理解為是元資料.

在java中,元資料以標籤的形式存在java程式碼中,它的存在並不影響程式程式碼的編譯和執行,通常它被用來生成其它的檔案或執行時知道被執行程式碼的描述資訊。java當中的javadoc和註解都屬於元資料.

什麼是註解(Annotation)?

註解是從java 5.0開始加入,可以用於標註包,類,方法,變數等.比如我們常見的@Override,再或者Android原始碼中的@hide,@systemApi,@privateApi等

對於@Override,多數人往往都是知其然而不知其所以然,今天我就來聊聊Annotation背後的祕密,開始正文.

元註解就是定義註解的註解,是java提供給我們用於定義註解的基本註解.在java.lang.annotation包中我們可以看到目前元註解共有以下幾個:

  1. @Retention

  2. @Target

  3. @Inherited

  4. @Documented

  5. @interface

下面我們將集合@Override註解來解釋著5個基本註解的用法.

@interface

@interface是java中用於宣告註解類的關鍵字.使用該註解表示將自動繼承java.lang.annotation.Annotation類,該過程交給編譯器完成.

因此我們想要定義一個註解只需要如下做即可,以@Override註解為例

public @interface Override {
}
需要注意:在定義註解時,不能繼承其他註解或介面

@Retention

@Retention:該註解用於定義註解保留策略,即定義的註解類在什麼時候存在(原始碼階段 or 編譯後 or 執行階段).該註解接受以下幾個引數: RetentionPolicy.SOURCE,RetentionPolicy.CLASS,RetentionPolicy.RUNTIME ,其具體使用及含義如下:

來看一下@Override註解的保留策略:

@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

這表明@Override註解只在原始碼階段存在,javac在編譯過程中去去掉該註解.

@Target

該註解用於定義註解的作用目標,即註解可以用在什麼地方,比如是用於方法上還是用於欄位上,該註解接受以下引數:

以@Override為例,不難看出其作用目標為方法:

@Target(ElementType.METHOD)
public @interface Override {
}

到現在,通過@interface,@Retention,@Target已經可以完整的定義一個註解,來看@Override完整定義:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Inherited

預設情況下,我們自定義的註解用在父類上不會被子類所繼承.如果想讓子類也繼承父類的註解,即註解在子類也生效,需要在自定義註解時設定@Inherited.一般情況下該註解用的比較少.

@Documented

該註解用於描述其它型別的annotation應該被javadoc文件化,出現在api doc中.

比如使用該註解的@Target會出出現在api說明中.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {




ElementType[] value();
}
藉助@Interface,@Target,@Retention,@Inherited,@Documented這五個元註解,
我們就可以自定義註解了,其中前三個註解是任何一個註解都必備具備的.

自定義註解

格式:

public @interface 註解名 {定義體}

定義體就是方法的集合,每個方法實則是聲明瞭一個配置引數.方法的名稱作為配置引數的名稱,方法的返回值型別就是配置引數的型別.和普通的方法不一樣,可以通過default關鍵字來宣告配置引數的預設值.

需要注意:

此處只能使用public或者預設的defalt兩個許可權修飾符
配置引數的型別只能使用基本型別(byte,boolean,char,short,int,long,float,double)和String,Enum,Class,annotation
對於只含有一個配置引數的註解,引數名建議設定中value,即方法名為value
配置引數一旦設定,其引數值必須有確定的值,要不在使用註解的時候指定,要不在定義註解的時候使用default為其設定預設值,對於非基本型別的引數值來說,其不能為null.

像@Override這樣,沒有成員定義的註解稱之為標記註解.

註解處理器

上面我們已經學會了如何定義註解,要想註解發揮實際作用,需要我們為註解編寫相應的註解處理器.根據註解的特性,註解處理器可以分為執行時註解處理和編譯時註解處理器.執行時處理器需要藉助反射機制實現,而編譯時處理器則需要藉助APT來實現.

無論是執行時註解處理器還是編譯時註解處理器,主要工作都是讀取註解及處理特定註解,從這個角度來看註解處理器還是非常容易理解的.

註解處理器是(Annotation Processor)是javac的一個工具,用來在編譯時掃描和編譯和處理註解(Annotation)。你可以自己定義註解和註解處理器去搞一些事情。一個註解處理器它以Java程式碼或者(編譯過的位元組碼)作為輸入,生成檔案(通常是java檔案)。這些生成的java檔案不能修改,並且會同其手動編寫的java程式碼一樣會被javac編譯。看到這裡加上之前理解,應該明白大概的過程了,就是把標記了註解的類,變數等作為輸入內容,經過註解處理器處理,生成想要生成的java程式碼。

執行時註解處理器(不建議使用)

熟悉java反射機制的同學一定對java.lang.reflect包非常熟悉,該包中的所有api都支援讀取執行時Annotation的能力,即屬性為@Retention(RetentionPolicy.RUNTIME)的註解.

在java.lang.reflect中的AnnotatedElement介面是所有程式元素的(Class,Method)父介面,我們可以通過反射獲取到某個類的AnnotatedElement物件,進而可以通過該物件提供的方法訪問Annotation資訊,常用的方法如下:

執行時註解處理器的編寫本質上就是通過反射獲取註解資訊,隨後進行其他操作。編譯一個執行時註解處理器就是這麼簡單。執行時註解通常多用於引數配置類模組。

編譯時註解處理器

不同於執行時註解處理器,編寫編譯時註解處理器(Annotation Processor Tool).

APT用於在編譯時期掃描和處理註解資訊.一個特定的註解處理器可以以java原始碼檔案或編譯後的class檔案作為輸入,然後輸出另一些檔案,可以是.java檔案,也可以是.class檔案,但通常我們輸出的是.java檔案.(注意:並不是對原始檔修改).如果輸出的是.java檔案,這些.java檔案回合其他原始碼檔案一起被javac編譯.

你可能很納悶,註解處理器是到底是在什麼階段介入的呢?好吧,其實是在javac開始編譯之前,這也就是通常我們為什麼願意輸出.java檔案的原因.

註解最早是在java 5引入,主要包含apt和com.sum.mirror包中相關mirror api,此時apt和javac是各自獨立的。從java 6開始,註解處理器正式標準化,apt工具也被直接整合在javac當中。

我們還是回到如何編寫編譯時註解處理器這個話題上,編譯一個編譯時註解處理主要分兩步:

1、繼承AbstractProcessor,實現自己的註解處理器

2、註冊處理器,並打成jar包

首先來看一下一個標準的註解處理器的格式:

public class MyAnnotationProcessor extends AbstractProcessor {


@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}


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


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


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

編寫一個註解處理器首先要對ProcessingEnvironment和RoundEnvironment非常熟悉。接下來我們一覽這兩個類的風采.首先來看一下ProcessingEnvironment類:

public interface ProcessingEnvironment {


Map<String,String> getOptions();


//Messager用來報告錯誤,警告和其他提示資訊
Messager getMessager();


//Filter用來建立新的原始檔,class檔案以及輔助檔案
Filer getFiler();


//Elements中包含用於操作Element的工具方法
Elements getElementUtils();


//Types中包含用於操作TypeMirror的工具方法
Types getTypeUtils();


SourceVersion getSourceVersion();


Locale getLocale();
}


Element

element表示一個靜態的,語言級別的構件。而任何一個結構化文件都可以看作是由不同的element組成的結構體,比如XML,JSON等。

對於java原始檔來說, Element代表程式元素:包,類,方法都是一種程式元素 ,他同樣是一種結構化文件:

package com.closedevice;             //PackageElement
public class Main{ //TypeElement
private int x; //VariableElement
private Main(){ //ExecuteableElement
}
private void print(String msg){ //其中的引數部分String msg為TypeElement
}
}

TypeMirror

這三個類也需要我們重點掌握:

DeclaredType代表宣告型別:類型別還是介面型別,當然也包括引數化型別,比如Set<String>,也包括原始型別

TypeElement代表類或介面元素,而DeclaredType代表類型別或介面型別。

TypeMirror代表java語言中的型別.Types包括基本型別,宣告型別(類型別和介面型別),陣列,型別變數和空型別。也代表通配型別引數,可執行檔案的簽名和返回型別等。TypeMirror類中最重要的是getKind()方法,該方法返回TypeKind型別,為了方便大家理解,這裡附上其原始碼:

public enum TypeKind {
BOOLEAN,BYTE,SHORT,INT,LONG,CHAR,FLOAT,DOUBLE,VOID,NONE,NULL,ARRAY,DECLARED,ERROR, TYPEVAR,WILDCARD,PACKAGE,EXECUTABLE,OTHER,UNION,INTERSECTION;
public boolean isPrimitive() {
switch(this) {
case BOOLEAN:
case BYTE:
case SHORT:
case INT:
case LONG:
case CHAR:
case FLOAT:
case DOUBLE:
return true;
default:
return false;
}
}
}

簡單來說,Element代表原始碼,TypeElement代表的是原始碼中的型別元素,比如類。雖然我們可以從TypeElement中獲取類名,TypeElement中不包含類本身的資訊,比如它的父類,要想獲取這資訊需要藉助TypeMirror,可以通過Element中的asType()獲取元素對應的TypeMirror。

然後來看一下RoundEnvironment,這個類比較簡單,一筆帶過:

public interface RoundEnvironment {
boolean processingOver();
//上一輪註解處理器是否產生錯誤
boolean errorRaised();
//返回上一輪註解處理器生成的根元素
Set<? extends Element> getRootElements();
//返回包含指定註解型別的元素的集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
//返回包含指定註解型別的元素的集合
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}

Filer

Filer用於註解處理器中建立新檔案,由於Filer用起來實在比較麻煩,後面我們會使用javapoet簡化我們的操作.

打包註解處理器的時候需要一個特殊的檔案 javax.annotation.processing.Processor 在 META-INF/services 路徑下

新建專案必要配置:

 //javapoet程式碼生成框架
implementation 'com.squareup:javapoet:1.8.0'
//註解處理器
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

編譯時註解demo示例地址:https://gitee.com/yutg/apt.git

專案結構

--apt-demo
----bindview-annotation(Java Library)//註解定義
----bindview-api(Android Library)//定義SDK介面方法
----bindview-compiler(Java Library)//註解處理器相關操作及生成java檔案
----app(Android App)

關注我獲取更多知識或者投稿