APT動態(tài)生成代碼的實際應用場景

APT

Annotation Processing Tool 注解處理器。 APT編譯時期就會掃描標識有某一些注解的源代碼,并對這些源代碼和注解做一些額外的操作,例如獲取注解的屬性信息,獲取標識該注解的源代碼類或類成員的一些信息等操作。

作用時期

編譯階段

我們可以利用編譯時期,通過 APT 掃描到這些注解和源代碼并生成一些額外的源文件。

應用場景

我們都知道微信支付和微信登錄都需要在我們的包名下面新建一個 package.wxapi 這樣的一個包,并在該包下創(chuàng)建對應的微信入口類,例如 WXEntryActivityWXPayActivity 這兩個類,那么我就感覺很反感需要在我們自己的包目錄下去新建這么兩個類,那么能不能通過注解處理器的方式,在編譯時期就幫我們將這件事給完成呢?答案是可以,下面我們就來探討如何去實現(xiàn)。

  • 常規(guī)的做法是這樣的:
微信入口文件
  • 最終我們希望的結果是這樣的
代碼自動生成的結果

開發(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 就由這三個小模塊組成。

依賴關系

他們之間的依賴關系如下:

依賴關系圖
  • app 依賴 lib-annotation 注解模塊和注解處理器模塊
app依賴
  • lib-compiler 依賴 lib-annotation 模塊
lib-compiler依賴

編寫注解

  • 編寫注解的作用是什么?

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 注解。下面在看看如何去定義一個注解處理器。

    1. 定義一個類,繼承 AbstractProcessor 這個抽象類

process(...) 就是用于處理注解的方法。

public class WXProcessor extends AbstractProcessor{
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}
    1. 如何讓編譯器知道有這么一個注解處理器呢?

這里介紹一個最簡單的方式:使用 Google 提供的一個 AutoService 注解來實現(xiàn)即可。

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

//在 processor 類上引用即可
@AutoService(Processor.class)
public class WXProcessor extends AbstractProcessor {
    1. 告訴注解處理器需要處理哪些注解?

覆寫 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;
}
/**
 * 設置需要掃描的注解
 *
 * @return
 */
private final Set<Class<? extends Annotation>> getSupportAnnotations() {
    Set<Class<? extends Annotation>> supportAnnotations = new HashSet<>();
    supportAnnotations.add(WXEntryAnnotation.class);
    return supportAnnotations;
}
    1. 額外的配置
@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)查看。

源代碼

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,941評論 6 342
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,869評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,076評論 25 708
  • 我 還是很喜歡你,可是我們怎么慢慢走遠了,變得陌生,變得無言,越來越?jīng)]話說,或許我們都只是對方生命中的過客,但那些...
    戲天大大閱讀 285評論 0 1
  • 001認真對待小承諾 其實,我沒想過學生會不信任老師,因為對于班上每個孩子,我像個大家長一樣操心著他們。但有時候?qū)?..
    OQ熊閱讀 132評論 0 0