APT
Annotation Processing Tool 注解處理器度液。
APT
在編譯時期
就會掃描標識有某一些注解的源代碼,并對這些源代碼和注解做一些額外的操作,例如獲取注解的屬性信息镜粤,獲取標識該注解的源代碼類或類成員的一些信息等操作幢踏。
作用時期
編譯階段
我們可以利用編譯時期髓需,通過 APT
掃描到這些注解和源代碼并生成一些額外的源文件。
應用場景
我們都知道微信支付和微信登錄都需要在我們的包名下面新建一個
package.wxapi
這樣的一個包房蝉,并在該包下創(chuàng)建對應的微信入口類僚匆,例如WXEntryActivity
和WXPayActivity
這兩個類,那么我就感覺很反感需要在我們自己的包目錄下去新建這么兩個類搭幻,那么能不能通過注解處理器的方式咧擂,在編譯時期就幫我們將這件事給完成呢?答案是可以檀蹋,下面我們就來探討如何去實現(xiàn)松申。
- 常規(guī)的做法是這樣的:
- 最終我們希望的結(jié)果是這樣的
開發(fā)需求
需求:新建一個類 WXDelegateEntryActivity
(在根包下)繼承至 AppCompatActivity
并對這個類標識自定義注解 WXEntryAnnotation
。在編譯時期,APT
在處理這個注解和WXDelegateEntryActivity
時就自動生成一些源文件贸桶,例如(WXEntryActivity 或者 WXPayActivity)舅逸。
開發(fā)步驟
我們將整個工程分為以下幾個小模塊
- app
編譯之后會生成 app/build/generated/source/apt/debug/包名.wxapi/WXEntryActivity.java
- lib-annotaion
存放注解的模塊。注意:該 module 是 java library 類型皇筛,并不是 Android library 類型哦琉历。
- lib-compiler
負責掃描注解,并生成注解的模塊水醋。注意:該 module 是 java library 類型旗笔,并不是 Android library 類型哦。
整個 demo 就由這三個小模塊組成拄踪。
依賴關(guān)系
他們之間的依賴關(guān)系如下:
- app 依賴 lib-annotation 注解模塊和注解處理器模塊
- lib-compiler 依賴 lib-annotation 模塊
編寫注解
- 編寫注解的作用是什么蝇恶?
讓
APT
在編譯時期就能找到標識該注解的源代碼。例如:在編譯時惶桐,APT
找到WXEntryAnnotation
注解的源代碼WXDelegateEntryActivity
撮弧。
package com.example.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface WXEntryAnnotation {
/**
* 標識我們WXEntrayActivity所在的包名
*
* 最后會生成 packageName().wxapi.WXEntryActivity 這么一個類。
* @return 返回包名
*/
String packageName();
/**
* 表示 WXEntrayActivity 需要繼承的那個類的字節(jié)碼文件耀盗,例如我們需要將生成的 WXEntrayActivity 去繼承 WXDelegateEntryActivity 那么這個 superClass 返回的就是 WXDelegateEntryActivity 的字節(jié)碼文件對象想虎。
* @return
*/
Class superClass();
}
build.gradle
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
//注意版本需要和 lib-compiler 的一致
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
編寫注解處理器
上面已經(jīng)提過,注解處理器就是用來掃描注解叛拷,并處理注解的工具舌厨。對應的模塊就是 lib-compiler
模塊。它在編譯時會掃描注解 WXEntryAnnotation
注解忿薇。下面在看看如何去定義一個注解處理器裙椭。
- 定義一個類,繼承 AbstractProcessor 這個抽象類
process(...) 就是用于處理注解的方法署浩。
public class WXProcessor extends AbstractProcessor{
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
- 如何讓編譯器知道有這么一個注解處理器呢揉燃?
這里介紹一個最簡單的方式:使用 Google 提供的一個 AutoService 注解來實現(xiàn)即可。
compile 'com.google.auto.service:auto-service:1.0-rc2'
//在 processor 類上引用即可
@AutoService(Processor.class)
public class WXProcessor extends AbstractProcessor {
- 告訴注解處理器需要處理哪些注解筋栋?
覆寫
getSupportedAnnotationTypes()
返回該處理器需要處理的注解類型即可炊汤。
@Override
public Set<String> getSupportedAnnotationTypes() {
final Set<String> supportAnnotationTypes = new HashSet<>();
final Set<Class<? extends Annotation>> supportAnnotations = getSupportAnnotations();
for (Class<? extends Annotation> supportAnnotion : supportAnnotations) {
supportAnnotationTypes.add(supportAnnotion.getCanonicalName());
}
return supportAnnotationTypes;
}
/**
* 設(shè)置需要掃描的注解
*
* @return
*/
private final Set<Class<? extends Annotation>> getSupportAnnotations() {
Set<Class<? extends Annotation>> supportAnnotations = new HashSet<>();
supportAnnotations.add(WXEntryAnnotation.class);
return supportAnnotations;
}
- 額外的配置
@AutoService(Processor.class)
//表示源碼版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class WXProcessor extends AbstractProcessor {}
build.gradle
apply plugin: 'java-library'
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':lib-annotation')
compile 'com.google.auto.service:auto-service:1.0-rc2'
}
//指定源碼版本號,需要和 WXProcessor 的源碼一致哦弊攘。
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
了解幾個常用的 API
Element 表示一個元素抢腐,它有幾個實現(xiàn)類
- VariableElement
代表成員變量元素
- ExecutableElement
代表類中的方法元素
- TypeElement
代表類元素
- PackageElement
代表包元素
在 app 模塊使用注解
@WXEntryAnnotation(packageName = "com.example", superClass = DelegateEntryActivity.class)
public class DelegateEntryActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
處理掃描出來的 Element
我們在定義 WXEntryAnnotation 注解時就指明了注解是只能使用在類或接口上,因此使用該 WXEntryAnnotation 的代碼就是使用 TypeElement 來描述的襟交。
/**
* @param set the annotation types requested to be processed
* @param roundEnvironment environment for information about the current and prior round
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set != null && !set.isEmpty()) {
WXEntryAnnotationVisitor visitor = new WXEntryAnnotationVisitor(processingEnv.getFiler());
for (TypeElement typeElement : set) {
//在這里我們已經(jīng)明確的知道WXEntryAnnotation只能用于type類型迈倍,因此可以使用ElementFilter.typesIn將其轉(zhuǎn)化為具體的TypeElement類型的集合。
Set<TypeElement> typeElements = ElementFilter.typesIn(elementsAnnotatedWith);
//遍歷使用該注解的類捣域,方法啼染,屬性
for (TypeElement element : typeElements) {
List<? extends AnnotationMirror> annotationMirrors = element.getAnnotationMirrors();
for (AnnotationMirror annotationMirror : annotationMirrors) {
//判斷當前處理的注解就是掃描出來的注解
if (annotationMirror.getAnnotationType().asElement().getSimpleName().toString().equals(typeElement.getSimpleName().toString())) {
//獲取注解的值
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
AnnotationValue value = entry.getValue();
value.accept(visitor, null);
}
}
}
}
}
return true;
}
return false;
}
下面的操作是對 process 方法的每一步的解釋(具體可以下源碼查看):
- 獲取使用了自定義注解的元素集合
//set就是 process 方法的參數(shù)
for (TypeElement typeElement : set) {
//使用該注解的元素集合
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(typeElement);
//測試輸出
System.out.println(elementsAnnotatedWith);
//[com.example.DelegateEntryActivity]
}
- 轉(zhuǎn)化Element集合為具體的TypeElemnt類型集合
在這里我們已經(jīng)明確的知道WXEntryAnnotation只能用于type類型宴合,因此可以使用ElementFilter.typesIn將其轉(zhuǎn)化為具體的TypeElement類型的集合。DelegateActivity 使用了 WXEntryAnnotation 注解
Set<TypeElement> typeElements = ElementFilter.typesIn(elementsAnnotatedWith);
- 遍歷取出使用 WXEntryAnnotation 注解的元素
for (TypeElement element : typeElements) {
//element表示使用了WXEntryAnnotation的元素
}
- 取出當前元素的注解集合
當前元素可能不止使用了 WXEntryAnnotation 這一個注解迹鹅,例如還使用 @Deprecated 那么就需要對其進行過濾卦洽。
List<? extends AnnotationMirror> annotationMirrors = element.getAnnotationMirrors();
- 過濾出 WXEntryAnnotation 這個注解信息
for (AnnotationMirror annotationMirror : annotationMirrors) {
if (annotationMirror.getAnnotationType().asElement().getSimpleName().toString().equals(typeElement.getSimpleName().toString())) {
...
}
}
- 取出WXEntryAnnotation注解信息
以下方法是獲取一個注解的信息,在 Map 中的泛型可以看到
ExecutableElement : 表示注解方法徒欣,例如 packageName()或者superClass()
AnnotationValue : 表示注解的值逐样,這個就是我們需要的東西了。
//獲取注解的值
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();
- 取出注解中AnnotationValue的對應的值
因為 WXEntryAnnotation 有個方法是 superClass 返回的是 Class 對象打肝,那么這個字節(jié)碼是無法通過 AnnotationValue 直接獲取的,那么這節(jié)介紹使用
AnnotationValueVisitor
注解訪問器來獲取對應的值挪捕。
//注解訪問器
WXEntryAnnotationVisitor visitor = new WXEntryAnnotationVisitor(processingEnv.getFiler());
//獲取注解的值
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
AnnotationValue value = entry.getValue();
value.accept(visitor, null);
}
AnnotationVisitor 注解訪問器
在上一步的操作中粗梭,最終會將表示注解值的 AnnotationValue 對象交給 AnnotationVisitor 去訪問。那么下面來了解一下這個類的基本使用级零。
- WXEntryAnnotationVisitor 繼承 SimpleAnnotationValueVisitor7(版本要對應)
public class WXEntryAnnotationVisitor extends SimpleAnnotationValueVisitor7<Void, Void> {
}
- 訪問字符串類型的回調(diào)
覆寫 visitString 即可断医,當訪問到 packageName 這個屬性時,那么該方法就會被回調(diào)奏纪。
@Override
public Void visitString(String s, Void aVoid) {
this.packageName = s;
if (typeMirror != null && packageName != null) {
//生成微信源代碼
generateWXEntryCode();
}
return super.visitString(s, aVoid);
}
- 訪問 Class 類型的回調(diào)
在 WXEntryAnnotation 中有一個屬性是 superClass 鉴嗤,當 visitor 訪問到這種屬性時就會回調(diào) visitType 并且將其以 TypeMirror 的方式返回。
@Override
public Void visitType(TypeMirror typeMirror, Void p) {
this.typeMirror = typeMirror;
if (typeMirror != null && packageName != null) {
//生成微信源代碼
generateWXEntryCode();
}
return p;
}
--
基于上面兩步序调,已經(jīng)可以拿到對應的 packageName 和 typeMirror 對象了醉锅,那么接下來工作就是根據(jù) packageName 和 typeMirror 去創(chuàng)建對應的微信入口文件了。
生成微信入口源文件
現(xiàn)在我們來探討一下如何去生成 WXEntryActivity 发绢?
目前 GITHUB 中有一個專門用于代碼生成的開源框架javapoet
使用
- 在 lib-compiler 中引入該庫
compile 'com.squareup:javapoet:1.9.0'
- 編寫生成代碼
/**
* WXEntryActivity代碼生成
*/
private final void generateWXEntryCode() {
TypeSpec targetActivityTypeSpec =
TypeSpec.classBuilder("WXEntryActivity")
.addModifiers(Modifier.FINAL)
.addModifiers(Modifier.PUBLIC)
.superclass(TypeName.get(this.typeMirror))
.build();
final JavaFile javaFile =
JavaFile.builder(this.packageName + ".wxapi", targetActivityTypeSpec)
.build();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
對于這個庫如何使用硬耍,可以去官網(wǎng)查看。