在AOP编程初体验中讨论了如何利用AspectJ在编译阶段插入Log代码,但是它也存在一些缺点,如:
- 增加方法数量。每在一个切入点插入代码,就会增加一个或者更多的方法。
- 增加整体包的大小。导入AspectJ会增加大概73K+的大小,对于现在的APP动则上百MB来说不算什么,但是为了几句简单的代码引入这么多代码实则没有必要。
- 如果切入点是方法,我暂时没有发现如何获取参数名和参数值,这也是为什么要自己编写注解处理器之编译插入Log代码的主要原因之一。
当然它也有很多优势,如功能和切入点丰富,可以在不修改原有代码的情况下在编译阶段插入想要的代码,如果想要了解其中的原理,可以把本文作为入门之选。
Log代码插入点
本例的主要目的是将Log代码作为第一行代码插入到方法之中。具体的插入代码如下所示:
1 | public class Person { |
现在已经知道代码的插入点以及插入代码的具体形式,那么接下来就要开始正式编写注解和注解处理器了。
定义注解
在编写注解处理器之前,有必要先了解一下注解。
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。 — Java编程思想
定义总是那么言简意赅,还是来看实际的例子,对于Android Java开发者来说,对以下注解并不陌生:
@Override
,表示当前方法覆盖了父类的方法,如果在没有覆盖父类的方法上使用了该注解,编译器或者IDE就会报错;@Deprecated
,表示被注解的元素已经过时了,建议程序员不要再使用了;@SuppressWarning
,顾名思义,就是抑制编译器警告。
这些都是内置注解,不过在本例中我们要编写自己的注解和注解处理器。先来定义本例中要使用的注解@SimpleLog
:
1 | @Retention(RetentionPolicy.SOURCE) |
@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-compiler
的build.gradle文件中添加依赖:
1 | apply plugin: 'java-library' |
- log-compiler依赖于log-annotation;
- 使用
auto-service
库可以不用编写javax.annotation.processing.Processor
文件; files(org.gradle.internal.jvm.Jvm.current().toolsJar)
为本地环境中tools.jar
的位置,tools.jar
位于/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/lib
目录下,对于不同的电脑,位置不一样,所以使用环境变量最好。
注解处理器
接下来就是定义注解处理器了:
1 | @AutoService(Processor.class) |
- 使用
@AutoService
注解,javax.annotation.processing.Processor
文件会自动生成; - 重写
init
方法,init
方法调用时可以保存必要的环境变量; getSupportedAnnotationTypes
方法需要返回支持的注解,也就是前面定义的@SimpleLog
;getSupportedSourceVersion
返回支持java版本;process
方法为主要的核心处理方法,找到被@SimpleLog
注解的元素,然后获取注解元素的类名,也就是Log的tag,最后使用自定义的TreeTranslator
来添加Log代码。
注解处理器的process阶段可以生产新的java类代码,如butterknife,如果生产新的java类代码,最终要使用这个类,还需要利用反射来实现,反射效率问题,所以本例不使用该方法,如果感兴趣可以看这篇教程Android Annotation Processing Tutorial学习如何实现butterknife。
编译时添加Log代码
本例主要利用注解处理在编译阶段插入Log代码到.class文件中:
1 | class SimpleLogTranslator extends TreeTranslator { |
好长一段代码,不过不要担心,分步解析。
visitMethodDef
在编译处理被注解类的方法时,都会进入此方法,在这个阶段,可以添加Log代码到被注解类的方法中,当然还有其他visit*
方法;- 首先定义一个
note
方法,在每次Build阶段输出必要的信息; generateLogStatement
根据前一步中传入的tag、访问方法的名称、参数名和参数值,生产Log的代码(也就是JCTree.JCStatement
);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 | dependencies { |
其次在代码中使用注解,如:
1 | @SimpleLog |
那么启动应用之后会得到如下的Log输出:
1 | 2019-05-01 14:30:59.038 7466-7466/? D/MainActivity: <init>() |
一个注解解决了大量样板代码的编写,的确是一个省时省力的好方法,应该合理的加以利用。
到此,在Android中使用注解处理器插入Log代码已经接近尾声了,可能你还有很多不明白的地方,且还有很多待改善的地方,如果将@SimpleLog
使用到接口和抽象类中会发生什么?读者不妨试试。
生成工具
本例的代码托管在GitHub上:SimpleLogger。
如果不想编写上面的代码,也可以在自己的项目中导入我的库:
1 | implementation 'com.ihuntto:log-annotation:1.0.3' |
总结
本文主要介绍如何使用注解处理器在编译阶段插入Log代码。如果想要在一个类的所有方法中插入Log代码,可以在这个类上使用注解,那么每次调用这个类的方法时都会打印类名作为tag、方法名和参数名与参数值作为tag的日志输出,这样可以更加专注于业务代码实现上。本文只是一个引子,可以在此基础上区分日志等级、或者插入其他样板代码等。还是那句老话,当你发现自己做着重复工作时,就是时候做出改变了。