在 Java 开发中,面向对象(OOP)编程是其最大的特色,但有时候为了更优雅得实现某些架构性的需求,单纯的面向对象已经满足不了当下日益复杂的业务需求,比如常见的埋点,方法日志,耗时统计等,这些如果使用简单的OOP实现就会陷入无穷的代码复制粘贴地狱中。所有我们需要引入更高效率的编程方式——AOP 来解决这些问题。AOP(Aspect-Oriented Programming,面向切面编程)是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充, AOP编程操作的主要对象是切面(aspect)。AOP的好处:每个事物逻辑位于一个位置,代码不分散,便于维护和升级业务模块更简洁,只包含核心业务代码。
APT 注解处理器
APT(Annotation Processing Tool)
是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。
Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件或其它的文件(文件具体内容由Annotation处理器的编写者决定)。
ps: 有很多主流框架都使用了
APT
的技术,代表:Dagger2,ButterKnife,ARouter等等
APT 在所有我所知的AOP工具中属于最早在编译期间处理代码的工具,它是在其编译成 .java
文件前就能介入其中,如图所示
简单使用
接下来我们看看示例代码:
1 |
|
一般写 APT自定义注解时需要使用一个几个库
1 | implementation 'com.squareup:javapoet:1.11.1' |
auto-service是用来帮你自动生成 META-INF
中的 metadata.
javapoet 则是一个帮助你生成 .java
文件的一个库.
编写完成后只需要在 build.gradle
中引入即可
1 | dependencies { |
优缺点
前面说的都是优点,比如 编译生成代码效率高,官方、gradle原生支持,使用上手简单等,但也有一定局限性
1、预留入口不编译会报红,需要先编译一次
2、调用新的生成的类还是需要写代码,使用反射效率又太差
3、无法实现定点插桩,只能生成新的类
故如果上述几个缺点不影响的话,APT绝对是你的第一选择。
AST(Abstract syntax tree)-抽象语法树
那我一定要使用APT,又想克服上述缺点有没有方法,答案是有,就是可以尝试使用 AST.
AST 即为“抽象语法树”,是编辑器对代码的第一步加工之后的结果,是一个树形式表示的源代码。源代码的每个元素映射到一个节点或子树。
Java 的编译过程可以分为三个阶段:
- 所有源文件会被解析成语法树。
- 调用注解处理器。如果注解处理器产生了新的源文件,新文件也要进行编译。
- 最后,语法树会被分析并转化成类文件。
- 大体流程 JavaTXT->词语法分析-> 生成AST ->语义分析 -> 编译字节码
那么通过操作 AST 是不是就可以通过修改抽象语法树的结构来达到对代码修改的目的呢?本应该可以的,但我自己实践下来发现修改后的 AST 无法反馈到文件上导致修改失败,不知道是什么原因,这里存个疑问。。。
操作 AST 一般通过 Javac 语法修改 或者使用工具库,比如 Rewrite 和 JavaParser
具体实现我就不展开了,请自行搜索吧。
AspectJ
AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,而且,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
AspectJ 在 Android 上的使用有两个很经典的参考,一个是 Jake Wharton 写的Hugo 方法耗时日志库,另一个则是可以直接集成到 Android 上使用的项目 AspectJX
基础概念与使用
Advice(通知): 典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。
Joint point(连接点): 程序中可能作为代码注入目标的特定的点和入口。
Pointcut(切入点): 告诉代码注入工具,在何处注入一段特定代码的表达式。
Aspect(切面): Pointcut 和 Advice 的组合看做切面。例如,在本例中通过定义一个 pointcut 和给定恰当的advice,添加一个了内存缓存的切面。
Weaving(织入): 注入代码(advices)到目标位置(joint points)的过程。
在 Android 中一般都是通过注解来使用,示例
1 |
|
具体可以参考 AspectJX
优缺点
AspectJ
方案成熟,使用起来更是方便,但集成到 Android 环境上比较麻烦,即便使用 AspectJX
框架也不能 100% 避免与 gradle 或者其他第三方框架的冲突。
Gradle Plugin
Google 提供了一整套相关 gradle 的库,即 gradle-transform-api
工具,基于这个工具可以在 gradle 的 task 中获取已经编译的 class 文件进行编辑,并替换原有 class 文件以达到字节插桩的目的。
常见使用场景:HotFix,InstantRun等
使用示例
一般使用 Gradle Plugin 插件最好的做法是将其作为一个 Java 模块,常用项目结构图如下:
logger
作为代码真正执行者,可供插件或者开发者直接调用;logger-annotations
作为注解库logger-compiler
apt的实现代码logger-plugin
gradle 的插件库
在插件中,你可以使用
groovy
或者java
甚至kotlin
来进行开发
logger-plugin
插件目录如下
1 | ├── logger-plugin |
其中 LogPlugin
为插件实现者
1 | class LogPlugin : Plugin<Project> { |
而 META-INF
则是作为插件注册文件,里面内容为
1 | implementation-class=com.cpacm.log.LogPlugin |
Plugin 实现的核心代码中需要一个继承 Transform
的类,示例中为LogTransform
,来实现字节码的插桩
1 | /** |
getName()
用来定义transform任务的名称,随意定一个就好getInputTypes()
用来限定这个transform能处理的文件类型,一般来说我们要处理的都是class文件,就返回TransformManager.CONTENT_CLASSgetScopes()
指定文件的范围,一般分为 主项目,子模块,第三方jar等等inIncremental()
是否支持增量编译transform()
主要方法在这个方法中获取输入的class文件,然后做些修改,最后输出修改后的class文件
处理字节码
字节码处理这里推荐两个库,一个是 Javassist 和 ASM。
这俩个库各有优势,Javassist
封装的更好,比较容易上手,而ASM
则是更偏重于字节码的编辑,比较灵活,但需要使用者有一些基础的字节码知识。
总结
复习一下,现在 Android 上比较知名的AOP技术方案有三种:
APT(Annotation Processing Tool)
最基础也是最有效率的注解处理工具,能够在编译早期根据注解生成新的类;AspectJ
目前最成熟的AOP工具,能根据方法名,注解等自行决定切入时机,功能齐全使用方便;Gradle Plugin
则是依托于 Google 提供的gradle-transform-api
工具,借助ASM
等工具对字节码进行修改。
三种方案比较推荐的是APT,但APT的局限性强,如果无法满足需求则再自己评测一下使用 Aspectj
或者 Gradle Plugin
。