[Android] APT (Annotation Processing Tool)

Posted by xiuyuantech on 2020-12-24

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,
确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。
注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出,减少手动的代码输入。
简单来说就是在编译期,通过注解生成.java文件。比如我们经常用的轮子Dagger2, ButterKnife,
EventBus 都在用,所以要紧跟潮流来看看APT技术的来龙去脉。

实现方式

创建Android Module命名为 app
创建Java library Module命名为 apt-annotation
创建Java library Module命名为 apt-processor 依赖 apt-annotation
创建Android library Module 命名为 apt-library依赖 apt-annotation、auto-service
可选 compileOnly files(org.gradle.internal.jvm.Jvm.current().getToolsJar())

注意不可都放在一个Module中,放在一个Module中不会生效!

功能主要分为三个部分

apt-annotation:存放自定义注解
apt-processor:注解处理器,根据apt-annotation中的注解,在编译期生成xxx.java代码
apt-library:工具类,实现业务逻辑的绑定。

结构图

apt-annotation (自定义注解)

1
2
3
4
5
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}

@Retention(RetentionPolicy.CLASS):表示编译时注解
@Target(ElementType.FIELD):表示注解范围为类成员(构造方法、方法、成员变量)

@Retention: 定义被保留的时间长短
RetentionPoicy.SOURCE、RetentionPoicy.CLASS、RetentionPoicy.RUNTIME
@Target: 定义所修饰的对象范围
TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE等

apt-processor (注解处理器)

添加依赖

1
2
3
4
5
6
dependencies {
implementation project(':apt-annotation')
implementation 'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
compileOnly files(org.gradle.internal.jvm.Jvm.current().getToolsJar()) //optional
}

自定义处理器 BindViewProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

private Messager messager;
private Trees trees;
private TreeMaker maker;
private Name.Table names;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
trees = Trees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
maker = TreeMaker.instance(context);
names = Names.instance(context).table;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
Set<? extends Element> elements = roundEnv.getRootElements();
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
JCTree tree = (JCTree) trees.getTree(each);
if (tree != null) {
if (tree instanceof JCTree.JCClassDecl) {
if (((JCTree.JCClassDecl) tree).name.toString().equals(
"WebAppInterface")) {
tree.accept(new Insert(messager, maker, names));
break;
}
}
}
}
}
}
return false;
}

class Insert extends TreeTranslator {
private Messager messager;
private TreeMaker maker;
private Name.Table names;

public Insert(Messager messager, TreeMaker maker, Name.Table names) {
this.messager = messager;
this.maker = maker;
this.names = names;
}

@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
super.visitMethodDef(jcMethodDecl);
boolean isVisited = false;
if (jcMethodDecl.mods != null && jcMethodDecl.mods.annotations != null) {
List<JCTree.JCAnnotation> ans = jcMethodDecl.mods.annotations;
for (JCTree.JCAnnotation annotation : ans) {
if (annotation.annotationType.toString().equals("JavascriptInterface")) {
isVisited = true;
break;
}
}
}
if (isVisited) {
JCTree.JCExpression invokeMethod = maker.Apply(List.nil(),
maker.Select(maker.Ident(names.fromString("mWebActivity")),
names.fromString(
"getCurrentUrl")), List.nil());
JCTree.JCVariableDecl statement = makeVarDef(maker.Modifiers(0), "surl",
memberAccess("java.lang.String"), invokeMethod);
JCTree.JCExpression invokeMethod1 = maker.Apply(List.of(memberAccess("java.lang" +
".String"), memberAccess("java.lang.String")),
memberAccess("com.dxdxmm.app.component.web.WebBridgeHelperKt" +
".isHostVerify"), List.of(maker.Ident(names.fromString("surl")),
maker.Literal(jcMethodDecl.name.toString())));
JCTree.JCVariableDecl statement1 = makeVarDef(maker.Modifiers(0), "verify",
maker.TypeIdent(TypeTag.BOOLEAN), invokeMethod1);
JCTree.JCIf statement2 = maker.If(maker.Ident(names.fromString("verify")),
jcMethodDecl.body, maker.Skip());
JCTree.JCReturn statement3 = maker.Return(maker.Literal(false));
JCTree.JCReturn statement4 = maker.Return(maker.Literal(""));

ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
if (!jcMethodDecl.name.toString().equals("<init>")) {
statements.append(statement);
statements.append(statement1);
statements.append(statement2);
if (jcMethodDecl.restype != null) {
if (jcMethodDecl.restype.toString().equals("String")) {
statements.append(statement4);
} else if (jcMethodDecl.restype.toString().equals("boolean")) {
statements.append(statement3);
}
}
}
JCTree.JCBlock body = maker.Block(0, statements.toList());
result = maker.MethodDef(
jcMethodDecl.getModifiers(),
names.fromString(jcMethodDecl.name.toString()),
jcMethodDecl.restype,
jcMethodDecl.typarams,
jcMethodDecl.params,
jcMethodDecl.thrown,
body,
jcMethodDecl.defaultValue
);
note(result.toString());
}
}

private JCTree.JCVariableDecl makeVarDef(JCTree.JCModifiers modifiers, String name,
JCTree.JCExpression vartype,
JCTree.JCExpression init) {
return maker.VarDef(
modifiers,
names.fromString(name),
vartype,
init
);
}

private JCTree.JCExpression memberAccess(String components) {
String[] componentArray = components.split("\\.");
JCTree.JCExpression expr = maker.Ident(names.fromString(componentArray[0]));
for (int i = 1; i < componentArray.length; i++) {
expr = maker.Select(expr, names.fromString(componentArray[i]));
}
return expr;
}
}

private void note(String msg) {
messager.printMessage(Diagnostic.Kind.NOTE, msg);
}

@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(WebModule.class.getCanonicalName());
return supportTypes;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}

init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
getSupportedAnnotationTypes:指定这个注解处理器是注册给哪个注解的,这里说明是注解BindView
getSupportedSourceVersion:指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
process:可以在这里写扫描、评估和处理注解的代码,生成Java文件

Messager : 日志打印类,有利于分析
Trees : 抽象语法树
TreeMaker : 抽象语法树操作类
Name.Table : 命名表,可根据名称找到对应的方法,变量,类
TreeTranslator : 抽象语法树转换器,可修改方法,变量,类

其他类不熟悉的自行查询,这里就不一一介绍了。

完成了Processor的部分,基本就大功告成了。

apt-library 工具类 可选

在App Module的build.gradle中添加依赖

1
2
3
dependencies {
implementation project(':apt-annotation')
}

创建注解工具类BindViewTools

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BindViewTools {

public static void bind(Activity activity) {

Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}

通过javapoet生成代码:
添加依赖

1
2
3
dependencies {
implementation 'com.squareup:javapoet:1.10.0'
}

新建ClassCreatorProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ClassCreatorProxy {
//省略部分代码...

/**
* 创建Java代码
* @return
*/
public TypeSpec generateJavaCode2() {
TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethods2())
.build();
return bindingClass;

}

/**
* 加入Method
*/
private MethodSpec generateMethods2() {
ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(host, "host");

for (int id : mVariableElementMap.keySet()) {
VariableElement element = mVariableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
}
return methodBuilder.build();
}


public String getPackageName() {
return mPackageName;
}
}

使用自定义注解

代码如下:
使用工具类时:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestActivity extends AppCompatActivity {

@BindView(R.id.test)
TextView textView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test);
BindViewTools.bind(this);
}

}

不使用工具类,只修改某个方法时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@BindView()
public class TestActivity extends AppCompatActivity {

TextView textView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test);
}

@JavascriptInterface
public void test(){

}
}

总结

APT优点:
对代码进行标记,在编译时收集信息,并做处理。

生成一套独立代码,辅助代码运行

生成代码位置的可控性(可以在任意包位置生成代码),与原有代码的关联性更为紧密方便

更为可靠的自动代码生成

自动生成的代码可以最大程度的简单粗暴,在不必考虑编写效率的情况下提高运行效率


APT缺点:

APT往往容易被误解可以实现代码插入,然而事实是并不可以

APT可以自动生成代码,但在运行时却需要主动调用

APT代码生成于Build目录,只能在运行时通过接口等方式进行操作。这意味着生成的代码必须要有一套固定的模板


APT容易被你忽视的点:

一个非常容易被你误解的点:只有被注解标记了的类或方法等,才可以被处理或收集信息。或者这样说,想要收集一些信息,只能先用注解修饰它。

产生这样误解容易引起一个问题:你可能会觉得一个需要大量注解的框架体验不好而决定放弃。

事实是怎么样呢?想一下同源的运行时注解+反射。反射可以通过一个类名便获取一个类的所有信息(方法、属性、方法参数等等等)。

编译时注解也是可以的。当你修饰一个类时,可以通过类的Element获得类的属性和方法的Element,通过属性的Element可以获得属性所属类的信息,

通过方法的Element可以获得所属类和其参数的信息。说白了,编译时注解你也完全可以当反射来理解。