Android注解处理器之编译插入Log代码

AOP编程初体验中讨论了如何利用AspectJ在编译阶段插入Log代码,但是它也存在一些缺点,如:

  • 增加方法数量。每在一个切入点插入代码,就会增加一个或者更多的方法。
  • 增加整体包的大小。导入AspectJ会增加大概73K+的大小,对于现在的APP动则上百MB来说不算什么,但是为了几句简单的代码引入这么多代码实则没有必要。
  • 如果切入点是方法,我暂时没有发现如何获取参数名和参数值,这也是为什么要自己编写注解处理器之编译插入Log代码的主要原因之一。

当然它也有很多优势,如功能和切入点丰富,可以在不修改原有代码的情况下在编译阶段插入想要的代码,如果想要了解其中的原理,可以把本文作为入门之选。

Log代码插入点

本例的主要目的是将Log代码作为第一行代码插入到方法之中。具体的插入代码如下所示:

1
2
3
4
5
6
7
8
9
10
public class Person {
... ...
private void sayHello(String toWho) {
// 以下为插入的log代码
android.util.Log.d("Person", "sayHello(toWho = " + toWho + " )");
// 以上为插入的log代码
System.out.println("Hello " + toWho);
}
... ...
}

现在已经知道代码的插入点以及插入代码的具体形式,那么接下来就要开始正式编写注解和注解处理器了。

定义注解

在编写注解处理器之前,有必要先了解一下注解。

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。 — Java编程思想

定义总是那么言简意赅,还是来看实际的例子,对于Android Java开发者来说,对以下注解并不陌生:

  • @Override,表示当前方法覆盖了父类的方法,如果在没有覆盖父类的方法上使用了该注解,编译器或者IDE就会报错;
  • @Deprecated,表示被注解的元素已经过时了,建议程序员不要再使用了;
  • @SuppressWarning,顾名思义,就是抑制编译器警告。

这些都是内置注解,不过在本例中我们要编写自己的注解和注解处理器。先来定义本例中要使用的注解@SimpleLog

1
2
3
4
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface SimpleLog {
}

@Rentention@Target为元注解,元注解可以理解为可以使用在注解上的注解。对于内置元注解的解释如下:

元注解 意义
@Target 表示该注解可以用于什么地方。可能的ElementType参数包括:
CONSTRUCTOR:构造函数
FIELD:域(包括enum实例),也就是内部成员变量
LOCAL_VARIABLE:局部变量
METHOD:方法
PACKAGE:包
PARAMETER:参数
TYPE:类、接口(包括注解类型)或enum
@Retention 表示该注解信息需要保留到什么阶段。可选的RetentionPolicy参数包括:
SOURCE:注解只保留在源码(.java)阶段,会被编译器丢弃。
CLASS:注解将被保留到class文件阶段,但会被VM丢弃。
RUNTIME:注解将会被保留到运行时阶段,因此可以通过反射机制读取注解的信息。
@Documented 将此注解包含到Javadoc中。
@Inherited 允许子类继承父类中的注解。

首先@SimpleLog注解只需要保留源文件中,当通过编译后就将其丢弃,因为运行时不需要通过反射获取信息;其次@SimpleLog暂时只能用于注解类。那么这个@SimpleLog应该放在何处呢?在AndroidStudio中新建一个Java Library Module,Module名为log-annotation,然后将@SimpleLog放在此Module中。

编写注解处理器

在使用AndroidStudio编写注解处理器之前,可以先看看Javac黑客指南这篇文章,然后一步步按照文章的指引敲一遍代码和命令。当然也可以直接进入主题,在AndroidStudio中新建Java Library Module,Module名为log-compiler

添加依赖

为了方便后续的工作,先在log-compilerbuild.gradle文件中添加依赖:

1
2
3
4
5
6
7
8
9
10
11
apply plugin: 'java-library'

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':log-annotation')
implementation 'com.google.auto.service:auto-service:1.0-rc5'
implementation files(org.gradle.internal.jvm.Jvm.current().toolsJar)
}

sourceCompatibility = "7"
targetCompatibility = "7"
  1. log-compiler依赖于log-annotation
  2. 使用auto-service库可以不用编写javax.annotation.processing.Processor文件;
  3. files(org.gradle.internal.jvm.Jvm.current().toolsJar)为本地环境中tools.jar的位置,tools.jar位于/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/lib目录下,对于不同的电脑,位置不一样,所以使用环境变量最好。

注解处理器

接下来就是定义注解处理器了:

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
@AutoService(Processor.class)
public class SimpleLogProcessor extends AbstractProcessor {
private Trees trees;
private SimpleLogTranslator visitor;
private Messager messager;

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

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

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

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
Set<? extends Element> elements =
roundEnv.getElementsAnnotatedWith(SimpleLog.class);
for (Element each : elements) {
if (each.getKind() == ElementKind.CLASS) {
messager.printMessage(Diagnostic.Kind.NOTE, "handle element: " + each.getSimpleName());
JCTree jcTree = (JCTree) trees.getTree(each);
if (jcTree != null) {
visitor.setTag(each.getKind() == ElementKind.CLASS ?
each.getSimpleName().toString() :
each.getEnclosingElement().getSimpleName().toString());
jcTree.accept(visitor);
} else {
messager.printMessage(Diagnostic.Kind.NOTE, ">> jctree is null.");
}
}
}
}
return false;
}
}
  1. 使用@AutoService注解,javax.annotation.processing.Processor文件会自动生成;
  2. 重写init方法,init方法调用时可以保存必要的环境变量;
  3. getSupportedAnnotationTypes方法需要返回支持的注解,也就是前面定义的@SimpleLog
  4. getSupportedSourceVersion返回支持java版本;
  5. process方法为主要的核心处理方法,找到被@SimpleLog注解的元素,然后获取注解元素的类名,也就是Log的tag,最后使用自定义的TreeTranslator来添加Log代码。

注解处理器的process阶段可以生产新的java类代码,如butterknife,如果生产新的java类代码,最终要使用这个类,还需要利用反射来实现,反射效率问题,所以本例不使用该方法,如果感兴趣可以看这篇教程Android Annotation Processing Tutorial学习如何实现butterknife。

编译时添加Log代码

本例主要利用注解处理在编译阶段插入Log代码到.class文件中:

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
class SimpleLogTranslator extends TreeTranslator {

private TreeMaker mTreeMaker;
private Name.Table mNames;
private Messager mMessager;
private String mTag;
private JCTree.JCExpression mTagExpression;

SimpleLogTranslator(TreeMaker treeMaker, Name.Table names, Messager messager) {
mTreeMaker = treeMaker;
mNames = names;
mMessager = messager;
}

void setTag(@Nonnull String tag) {
mTag = tag;
mTagExpression = mTreeMaker.Literal(tag);
}

@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
super.visitMethodDef(jcMethodDecl);
note("visit:" + jcMethodDecl.getName());

JCTree.JCStatement logStatement = generateLogStatement(jcMethodDecl);

addLogToMethod(jcMethodDecl, logStatement);

note("added SimpleLog to method " + jcMethodDecl.name);
}

private void addLogToMethod(JCTree.JCMethodDecl jcMethodDecl, JCTree.JCStatement logStatement) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
List<JCTree.JCStatement> methodStatements = jcMethodDecl.getBody().getStatements();
if (methodStatements.size() > 0) {
JCTree.JCStatement firstStatement = methodStatements.get(0);
// super(...) statement must before log statement in constructor
if (firstStatement.toString().startsWith("super(")) {
statements.append(firstStatement);
statements.append(logStatement);
} else {
statements.append(logStatement);
statements.append(firstStatement);
}
} else {
statements.add(logStatement);
}
for (int i = 1; i < methodStatements.size(); i++) {
statements.append(jcMethodDecl.getBody().getStatements().get(i));
}

JCTree.JCBlock body = mTreeMaker.Block(0, statements.toList());

result = mTreeMaker.MethodDef(
jcMethodDecl.getModifiers(),
mNames.fromString(jcMethodDecl.getName().toString()),
jcMethodDecl.restype,
jcMethodDecl.getTypeParameters(),
jcMethodDecl.getParameters(),
jcMethodDecl.getThrows(),
body,
jcMethodDecl.defaultValue
);
}

/**
* generate statement of android.util.Log.d(TAG, params = values);
*/
private JCTree.JCStatement generateLogStatement(JCTree.JCMethodDecl jcMethodDecl) {
JCTree.JCFieldAccess logMethod = mTreeMaker.Select(
mTreeMaker.Select(
mTreeMaker.Select(
mTreeMaker.Ident(mNames.fromString("android")),
mNames.fromString("util")
),
mNames.fromString("Log")
),
mNames.fromString("d")
);

JCTree.JCExpression msgExpression = generateLogMessageExpression(jcMethodDecl);
JCTree.JCMethodInvocation methodInvocation = mTreeMaker.Apply(
List.<JCTree.JCExpression>nil(),
logMethod,
List.of(mTagExpression, msgExpression)
);
return mTreeMaker.Exec(methodInvocation);
}

/**
* generate expression of param1 = value1, param2 = value2, ... ;
*/
private JCTree.JCExpression generateLogMessageExpression(JCTree.JCMethodDecl jcMethodDecl) {
JCTree.JCExpression msgExpression = mTreeMaker.Literal(jcMethodDecl.name.toString() + "(");
if (jcMethodDecl.params.size() > 0) {
boolean first = true;
for (JCTree.JCVariableDecl jcVariableDecl : jcMethodDecl.params) {
if (!first) {
// add ,
msgExpression = mTreeMaker.Binary(JCTree.Tag.PLUS, msgExpression, mTreeMaker.Literal(", "));
}
if (jcVariableDecl.sym == null) {
continue;
}
first = false;
JCTree.JCExpression paramName = mTreeMaker.Literal(jcVariableDecl.getName().toString() + " = ");
JCTree.JCExpression paramValue = mTreeMaker.Ident(jcVariableDecl);
msgExpression = mTreeMaker.Binary(
JCTree.Tag.PLUS,
msgExpression,
mTreeMaker.Binary(JCTree.Tag.PLUS, paramName, paramValue)
);
}
}
return mTreeMaker.Binary(JCTree.Tag.PLUS, msgExpression, mTreeMaker.Literal(")"));
}

private void note(String msg) {
mMessager.printMessage(Diagnostic.Kind.NOTE, mTag + ": " + msg);
}

}

好长一段代码,不过不要担心,分步解析。

  1. visitMethodDef在编译处理被注解类的方法时,都会进入此方法,在这个阶段,可以添加Log代码到被注解类的方法中,当然还有其他visit*方法;
  2. 首先定义一个note方法,在每次Build阶段输出必要的信息;
  3. generateLogStatement根据前一步中传入的tag、访问方法的名称、参数名和参数值,生产Log的代码(也就是JCTree.JCStatement);
  4. addLogToMethod方法将generateLogStatement生产的logStatement插入被注解类方法的所有statement之前,也就是第一句代码。

代码中的每个类从其名称中大概可以推断出其意思,如JCExpression为表达式、JCStatement相当于一条语句,这就不难理解,先生成"param1 = " + param1 + " param2 = " + param2类似的表达式,param的数量取决于被注解类方法的参数数量 ;后利用表达式生成android.util.Log.d(tag, expression)语句。如果不能理解具体的意思,还可以充分利用note方法输出帮助信息,当然可以查看官方文档(不过我没有找到 -_-||)。

到这里你可能会问,@SimpleLog为什么不注解在方法上呢?其实最开始我的尝试是同时注解在方法和类上的,当注解在方法上时,从note方法输出的信息来看,代码已经注入其中,但从最终的Log输出来看并未有注入的代码,因此为了谨慎起见,暂时放弃注解在方法上,后续有机会再深入研究。

使用注解和注解处理器

前面已经编写好注解和注解处理器,现在需要将它们应用到实际项目中。在AndroidStudio中新建一个Android Phone & Tablet Module(如果已经存在,可以使用已有的)。首先在build.gradle文件中添加必要的依赖:

1
2
3
4
5
dependencies {
... ...
implementation project(':log-annotation')
annotationProcessor project(':log-compiler')
}

其次在代码中使用注解,如:

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
@SimpleLog
public class MainActivity extends AppCompatActivity {

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

@Override
protected void onRestart() {
super.onRestart();
}

@Override
protected void onStart() {
super.onStart();
}

@Override
protected void onResume() {
super.onResume();
}

@Override
protected void onPause() {
super.onPause();
}

@Override
protected void onStop() {
super.onStop();
}

@Override
protected void onDestroy() {
super.onDestroy();
}
}

那么启动应用之后会得到如下的Log输出:

1
2
3
4
5
6
2019-05-01 14:30:59.038 7466-7466/? D/MainActivity: <init>()
2019-05-01 14:30:59.045 7466-7466/? D/MainActivity: onCreate(savedInstanceState = null)
2019-05-01 14:30:59.242 7466-7466/? D/MainActivity: onStart()
2019-05-01 14:30:59.247 7466-7466/? D/MainActivity: onResume()
2019-05-01 14:31:19.496 7466-7466/com.ihuntto.simplelogger.demo D/MainActivity: onPause()
2019-05-01 14:31:19.524 7466-7466/com.ihuntto.simplelogger.demo D/MainActivity: onStop()

一个注解解决了大量样板代码的编写,的确是一个省时省力的好方法,应该合理的加以利用。

到此,在Android中使用注解处理器插入Log代码已经接近尾声了,可能你还有很多不明白的地方,且还有很多待改善的地方,如果将@SimpleLog使用到接口和抽象类中会发生什么?读者不妨试试。

生成工具

本例的代码托管在GitHub上:SimpleLogger

如果不想编写上面的代码,也可以在自己的项目中导入我的库:

1
2
implementation 'com.ihuntto:log-annotation:1.0.3'
annotationProcessor 'com.ihuntto:log-compiler:1.0.5'

总结

本文主要介绍如何使用注解处理器在编译阶段插入Log代码。如果想要在一个类的所有方法中插入Log代码,可以在这个类上使用注解,那么每次调用这个类的方法时都会打印类名作为tag、方法名和参数名与参数值作为tag的日志输出,这样可以更加专注于业务代码实现上。本文只是一个引子,可以在此基础上区分日志等级、或者插入其他样板代码等。还是那句老话,当你发现自己做着重复工作时,就是时候做出改变了。

参考文档

[1] Android Annotation Processing Tutorial
[2] Javac黑客指南