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

APT

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

作用时期

编译阶段

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

应用场景

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

  • 常规的做法是这样的:
微信入口文件
  • 最终我们希望的结果是这样的
代码自动生成的结果

开发需求

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

开发步骤

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

划分模块
  • 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 需要继承的那个类的字节码文件,例如我们需要将生成的 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 注解。下面在看看如何去定义一个注解处理器。

    1. 定义一个类,继承 AbstractProcessor 这个抽象类

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

public class WXProcessor extends AbstractProcessor{
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}
    1. 如何让编译器知道有这么一个注解处理器呢?

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

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 表示一个元素,它有几个实现类

  • 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) {
            
            //在这里我们已经明确的知道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]
}
  • 转化Element集合为具体的TypeElemnt类型集合

在这里我们已经明确的知道WXEntryAnnotation只能用于type类型,因此可以使用ElementFilter.typesIn将其转化为具体的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 对象,那么这个字节码是无法通过 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 去访问。那么下面来了解一下这个类的基本使用。

  • WXEntryAnnotationVisitor 继承 SimpleAnnotationValueVisitor7(版本要对应)
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);
}
  • 访问 Class 类型的回调

在 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

使用

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

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

源代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,733评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,432评论 25 707
  • 我 还是很喜欢你,可是我们怎么慢慢走远了,变得陌生,变得无言,越来越没话说,或许我们都只是对方生命中的过客,但那些...
    戏天大大阅读 275评论 0 1
  • 001认真对待小承诺 其实,我没想过学生会不信任老师,因为对于班上每个孩子,我像个大家长一样操心着他们。但有时候对...
    OQ熊阅读 104评论 0 0