Android进阶之路架构之路Android开发经验谈

APT动态生成代码的实际应用场景

2018-05-01  本文已影响563人  未见哥哥

APT

Annotation Processing Tool 注解处理器。 APT编译时期就会扫描标识有某一些注解的源代码,并对这些源代码和注解做一些额外的操作,例如获取注解的属性信息,获取标识该注解的源代码类或类成员的一些信息等操作。

作用时期

编译阶段

我们可以利用编译时期,通过 APT 扫描到这些注解和源代码并生成一些额外的源文件。

应用场景

我们都知道微信支付和微信登录都需要在我们的包名下面新建一个 package.wxapi 这样的一个包,并在该包下创建对应的微信入口类,例如 WXEntryActivityWXPayActivity 这两个类,那么我就感觉很反感需要在我们自己的包目录下去新建这么两个类,那么能不能通过注解处理器的方式,在编译时期就帮我们将这件事给完成呢?答案是可以,下面我们就来探讨如何去实现。

微信入口文件 代码自动生成的结果

开发需求

需求:新建一个类 WXDelegateEntryActivity(在根包下)继承至 AppCompatActivity 并对这个类标识自定义注解 WXEntryAnnotation。在编译时期,APT在处理这个注解和WXDelegateEntryActivity时就自动生成一些源文件,例如(WXEntryActivity 或者 WXPayActivity)。

开发步骤

我们将整个工程分为以下几个小模块

划分模块

编译之后会生成 app/build/generated/source/apt/debug/包名.wxapi/WXEntryActivity.java

存放注解的模块。注意:该 module 是 java library 类型,并不是 Android library 类型哦。

注解类

负责扫描注解,并生成注解的模块。注意:该 module 是 java library 类型,并不是 Android library 类型哦。

整个 demo 就由这三个小模块组成。

依赖关系

他们之间的依赖关系如下:

依赖关系图 app依赖 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 需要继承的那个类的字节码文件,例如我们需要将生成的 WXEntrayActivity 去继承 WXDelegateEntryActivity 那么这个 superClass 返回的就是 WXDelegateEntryActivity 的字节码文件对象。
     * @return
     */
    Class superClass();

}

build.gradle

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}
//注意版本需要和 lib-compiler 的一致
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

编写注解处理器

上面已经提过,注解处理器就是用来扫描注解,并处理注解的工具。对应的模块就是 lib-compiler 模块。它在编译时会扫描注解 WXEntryAnnotation 注解。下面在看看如何去定义一个注解处理器。

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

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

这里介绍一个最简单的方式:使用 Google 提供的一个 AutoService 注解来实现即可。

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;
}
/**
 * 设置需要扫描的注解
 *
 * @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 表示一个元素,它有几个实现类

代表成员变量元素

代表类中的方法元素

代表类元素

代表包元素

在 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) {
            
            //在这里我们已经明确的知道WXEntryAnnotation只能用于type类型,因此可以使用ElementFilter.typesIn将其转化为具体的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 方法的参数
for (TypeElement typeElement : set) {
    //使用该注解的元素集合
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(typeElement);
    
    //测试输出
System.out.println(elementsAnnotatedWith);
    //[com.example.DelegateEntryActivity]
}

在这里我们已经明确的知道WXEntryAnnotation只能用于type类型,因此可以使用ElementFilter.typesIn将其转化为具体的TypeElement类型的集合。DelegateActivity 使用了 WXEntryAnnotation 注解

Set<TypeElement> typeElements = ElementFilter.typesIn(elementsAnnotatedWith);
for (TypeElement element : typeElements) {
    //element表示使用了WXEntryAnnotation的元素
}

当前元素可能不止使用了 WXEntryAnnotation 这一个注解,例如还使用 @Deprecated 那么就需要对其进行过滤。

List<? extends AnnotationMirror> annotationMirrors = element.getAnnotationMirrors();
for (AnnotationMirror annotationMirror : annotationMirrors) {
    if (annotationMirror.getAnnotationType().asElement().getSimpleName().toString().equals(typeElement.getSimpleName().toString())) {
        ...
    }
}

以下方法是获取一个注解的信息,在 Map 中的泛型可以看到

ExecutableElement : 表示注解方法,例如 packageName()或者superClass()

AnnotationValue : 表示注解的值,这个就是我们需要的东西了。

//获取注解的值
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();

因为 WXEntryAnnotation 有个方法是 superClass 返回的是 Class 对象,那么这个字节码是无法通过 AnnotationValue 直接获取的,那么这节介绍使用 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 去访问。那么下面来了解一下这个类的基本使用。

public class WXEntryAnnotationVisitor extends SimpleAnnotationValueVisitor7<Void, Void> {
    
}

覆写 visitString 即可,当访问到 packageName 这个属性时,那么该方法就会被回调。

@Override
public Void visitString(String s, Void aVoid) {
    this.packageName = s;
    
    if (typeMirror != null && packageName != null) {
        //生成微信源代码
        generateWXEntryCode();
    }
    return super.visitString(s, aVoid);
}

在 WXEntryAnnotation 中有一个属性是 superClass ,当 visitor 访问到这种属性时就会回调 visitType 并且将其以 TypeMirror 的方式返回。

@Override
public Void visitType(TypeMirror typeMirror, Void p) {

    this.typeMirror = typeMirror;
    if (typeMirror != null && packageName != null) {
        //生成微信源代码
        generateWXEntryCode();
    }
    return p;
}

--

基于上面两步,已经可以拿到对应的 packageName 和 typeMirror 对象了,那么接下来工作就是根据 packageName 和 typeMirror 去创建对应的微信入口文件了。

生成微信入口源文件

现在我们来探讨一下如何去生成 WXEntryActivity ?

目前 GITHUB 中有一个专门用于代码生成的开源框架javapoet

使用

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();
      }
}

对于这个库如何使用,可以去官网查看。

源代码

上一篇下一篇

猜你喜欢

热点阅读