APT动态生成代码的实际应用场景
APT
Annotation Processing Tool 注解处理器。
APT
在编译时期
就会扫描标识有某一些注解的源代码,并对这些源代码和注解做一些额外的操作,例如获取注解的属性信息,获取标识该注解的源代码类或类成员的一些信息等操作。
作用时期
编译阶段
我们可以利用编译时期,通过 APT
扫描到这些注解和源代码并生成一些额外的源文件。
应用场景
我们都知道微信支付和微信登录都需要在我们的包名下面新建一个
package.wxapi
这样的一个包,并在该包下创建对应的微信入口类,例如WXEntryActivity
和WXPayActivity
这两个类,那么我就感觉很反感需要在我们自己的包目录下去新建这么两个类,那么能不能通过注解处理器的方式,在编译时期就帮我们将这件事给完成呢?答案是可以,下面我们就来探讨如何去实现。
- 常规的做法是这样的:
- 最终我们希望的结果是这样的
开发需求
需求:新建一个类 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 注解模块和注解处理器模块
- 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 需要继承的那个类的字节码文件,例如我们需要将生成的 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
注解。下面在看看如何去定义一个注解处理器。
- 定义一个类,继承 AbstractProcessor 这个抽象类
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 表示一个元素,它有几个实现类
- 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();
}
}
对于这个库如何使用,可以去官网查看。