一、概念
AOP 是 Aspect Oriented Programming 的缩写,即“面向切面编程”,是面向对象编程(OOP)的一种补充。AspectJ是其中的一种。使用 AOP,可以在编译期间对代码进行动态管理, 以达到统一维护的目的。利用 AOP 可以对业务逻辑的各个模块进行隔离,从而使得业务间耦合度降低,提高程序的可重用性及开发的效率。
aop的优点:假设要给两个类的每个方法统计执行耗时,就必须在这两个类的方法中都加上耗时统计的代码。因为面向对象的设计,让类与类之间无法联系,同样的代码仍然会分散到各个方法中。
AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的。AOP思想中,类与切面的关系如下图所示:
应用场景
利用 AOP,我们可以在无浸入的在宿主中插入一些代码逻辑,从而可以实现一些特殊的功能,如下:
- 日志埋点:编写相应的切面类,再定义合适的PointCut用来匹配我们的织入目标的方法。就可以在编译期间插入埋点代码,从而达到自动埋点即全埋点的效果。
- 性能监控
- 动态权限控制
- 代码调试
但Android利用AspectJ实现AOP,也存在一些局限性:
- 无法织入第三方库
- 由于定义的切点依赖编程语言,目前该方案我无法兼容Lambda语法
- 会有一些兼容性方面的问题,比如:D8、Gradle4.x等。
AOP的实现方式
静态AOP
在编译器,切面直接以字节码的形式编译到目标字节码文件中。
1.AspectJ
AspectJ属于静态AOP,它是在编译时进行增强,会在编译时期将AOP逻辑织入到代码中。
由于是在编译器织入,所以它的优点是不影响运行时性能,缺点是不够灵活。
总所周知,ButterKnife、Dagger、GreenDao、Protocol Buffers
这些常用的注解生成框架都会在编译过程中生成代码。而 使用 AndroidAnnotation 结合 APT 技术 来生成代码的时机,是在编译最开始的时候介入的。但是 AOP 是在编译完成后生成 dex 文件之前的时候,直接通过修改 .class 文件的方式,来直接添加或者修改代码逻辑的。
2.AbstractProcessor
自定义一个AbstractProcessor,在编译期去解析编译的类,并且根据需求生成一个实现了特定接口的子类(代理类)
动态AOP
1.JDK动态代理
通过实现InvocationHandler接口,可以实现对一个类的动态代理,通过动态代理可以生成代理类,从而在代理类方法中,在执行被代理类方法前后,添加自己的实现内容,从而实现AOP。
2.动态字节码生成
在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中,没有接口也可以织入,但扩展类的实例方法为final时,则无法进行织入。比如Cglib。
3.自定义类加载器
在运行期,目标加载前,将切面逻辑加到目标字节码里。如:Javassist
Javassist
是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。
4.ASM
ASM可以在编译期直接修改编译出的字节码文件,也可以像Javassit一样,在运行期,类文件加载前,去修改字节码。
Android 接入 AspectJ的方式
AspectJ官方并不支持AndroidStudio开发,但官方提供了AspectTools,它包含了AspectJ编译器ajc,提供了aspectj语言的编译和织入功能,可以把aspectJ文件织入目标java代码,然后生成class文件。
目前开源的主要有2种方案:
- AspectJX
- aspectjrt (aspectjtools,aspectjweaver)
本文使用AspectJX。为什么选用AspectJX而不是基础的AspectJ或其他?目前其他的AspectJ相关插件和框架都不支持AAR或者JAR切入的,而且也不支持子module,并对于Kotlin支持也不友好。
二、AspectJX接入步骤
1、在项目根目录的build.gradle里依赖AspectJX
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'//aspectjx依赖
}
2、在app项目的build.gradle里引入aspectjx插件
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'android-aspectjx' //引入aspectjx插件
}
apply plugin: 'kotlin-android'
android {
signingConfigs {
//...
}
defaultConfig {
//...
}
aspectjx {
//排除所有package的class文件及库(jar文件)
// exclude 'androidx'
// exclude 'versions.9'
//只织入指定包路径下的类
include 'com.dj.example'
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
//···
// implementation 'org.aspectj:aspectjrt:1.9.5'
}
}
3、支持include和exclude加快编译速度
需要注意的是,需要在android闭包下,添加aspectjx闭包。AspectJX默认会处理所有的二进制代码文件和库,为了提升编译效率以及规避部分第三方库出现的编译兼容性问题。AspectJ提供exclude来排除指定包名下的文件,或者使用include来只处理指定包名下的文件。两者都不用的话,可能会报错"ClassNotFoundExceptiong: Didn't find class on path:DexPathList"。
三、基本概念及语法
AOP术语
Advice:增强
也叫通知。增强是织入到目标类连接点上到一段程序代码。增强除了用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。
JoinPoint:连接点
程序执行的某个特定位置,如类开始初始化前、类初始化后、类中某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点。
PointCut:切点
每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,连接点是程序类中客观存在的事物。AOP通过“切点”定位特定的连接点。
Aspect:切面
切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义,AspectJX将切面所定义的横切逻辑织入到切面所指定的连接点中。
Weaving:织入
织入是将增强添加到目标类具体连接点上到过程。AOP像一台织布机,将目标类、增强通过AOP这台织布机无缝地编织到一起。
Target:目标对象
增强逻辑的织入目标类。
Aspect通知类型
类型 | 解释 |
---|---|
before:前置通知 (应用:各种校验) | 在方法执行前执行,如果通知抛出异常,阻止方法运行 |
afterReturning:后置通知(应用:常规数据处理) | 方法正常返回后执行,如果方法中抛出异常,通知无法执行 必须在方法执行后才执行,所以可以获得方法的返回值。 |
around:环绕通知(应用:十分强大,可以做任何事情) | 方法执行前后分别执行,可以阻止方法的执行必须手动执行目标方法 |
afterThrowing:抛出异常通知(应用:包装异常信息) | 方法抛出异常后执行,如果方法没有抛出异常,无法执行 |
after:最终通知(应用:清理现场) | 方法执行完毕后执行,无论方法中是否出现异常 |
切点表达式
语法:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
修饰符和异常可以省略。
AspectJ 支持三种通配符:
*
匹配任意字符,只匹配一个元素
..
匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 *
联合使用
+
表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+
,表示继承该类的所有子类包括本身.
逻辑运算符。
切点表达式由切点函数组成,切点函数之间还可以进行逻辑运算,组成复合切点。
-
&&
:与操作符。相当于切点的交集运算。xml配置文件中使用切点表达式,&是特殊字符,所以需要转义字符&;来表示。 -
||
:或操作符。相当于切点的并集运算。 -
!
:非操作符,相当于切点的反集运算。
1、execution:匹配方法的执行。
execution(public * *(..))
//匹配目标类的所有public方法,第一个`*`代表返回类型,第二个`*`代表方法名,`..`代表方法的参数。
execution(**User(..))
//匹配目标类所有以User为后缀的方法。第一个*代表返回类型,*User代表以User为后缀的方法
execution(* com.cad.demo.User.*(..))
//匹配 User 类里的所有方法
2、within:匹配包或子包中的方法(了解)
within(com.ys.aop...*)
3、this:匹配实现接口的代理对象中的方法(了解)
this(com.ys.aop.user.UserDAO)
4、target:匹配实现接口的目标对象中的方法(了解)
target(com.ys.aop.user.UserDAO)
5、args:匹配参数格式符合标准的方法(了解)
args(int,int)
6、bean(id) 对指定的bean所有的方法(了解)
bean(‘userServiceId’)
四、接入问题
问题1:
- Caused by: org.gradle.api.InvalidUserCodeException: Build was configured to prefer settings repositories over project repositories but repository 'MavenLocal' was added by plugin 'android-aspectjx'
dependencyResolutionManagement {
// repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
//···
}
}
修改settings.gradle中的配置,将repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)改为repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)。
问题2
java.lang.NoSuchMethodError: No static method aspectOf()
在写aspectjx的类,增加aspectOf()方法。例如为TestAop:
public static TestAop aspectOf() {
return new TestAop();
}
问题3
zip file is empty
可能是在app项目的build.gradle里中aspectjx没有配置exclude规则导致,或者切点表达式错误。
问题4
java.lang.ClassNotFoundException: Didn't find class "im.dj.example.source.XXX" on path: DexPathList
kotlin java混编导致的一些问题。改成了java后解决。
五、使用场景及具体api分析
ProceedingJoinPoint分析
ProceedingJoinPoint 数据结构及使用分析
Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法,proceed是aop代理链执行的方法。
1、获取切入点所在目标对象
Object targetObj =joinPoint.getTarget();
// 可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
String className = joinPoint.getTarget().getClass().getName();
2、获取切入点方法的名字
String methodName = joinPoint.getSignature().getName()
3、获取方法的参数
获取切入点方法的参数列表
Object[] args = proceedingJoinPoint.getArgs();
例子如下:
//函数入参值
Object[] values = proceedingJoinPoint.getArgs();
//变量名
String[] names = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();
void apply(final StreamSession session,int serial);
StreamCallback call = new StreamCallback();
StreamSession sesOne = new StreamSession();
call.apply(sesOne,3);
则values为[sesOne,3],names为[session,serial],sesOne是一个对象。
六、简单例子
例子功能实现对被注解的方法进行耗时统计。
注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeConsume {
}
切面类
@Aspect
public class TimeConsumeAspect {
private static final String TAG = "TimeConsumeAspect";
public static TimeConsumeAspect aspectOf(){
return new TimeConsumeAspect();
}
@Pointcut("execution(@im.whale.stream.aop.time.TimeConsume * *(..))")
public void methodTimeConsumePoint(){
}
//通过注解,获取到被注解的方法
@Around("methodTimeConsumePoint()")
public Object aroundMethodConsume(ProceedingJoinPoint joinPoint) throws Throwable{
long beforeTime = SystemClock.elapsedRealtime();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
Log.e(TAG, "aspect aroundShopMall: exception");
String name = joinPoint.getSignature().getName();
long afterTime = SystemClock.elapsedRealtime();
Log.d(TAG, "aspect2: "+name+" 耗时="+(afterTime-beforeTime)+"ms");
}
String name = joinPoint.getSignature().getName();
long afterTime = SystemClock.elapsedRealtime();
Log.d(TAG, "aspect: "+name+" 耗时="+(afterTime-beforeTime)+"ms");
return result;
}
}
对需要统计耗时的方法添加注解
@TimeConsume
public String getEncodeConfig() {···}
执行结果
D aspect: getEncodeConfig 耗时=5226ms
八、参考
https://blog.csdn.net/weixin_39692761/article/details/117277052
https://blog.csdn.net/lady88888888/article/details/105980133/
https://www.jianshu.com/p/0cf692f4a2d0
// Android 全埋点解决方案
https://www.jianshu.com/p/646365eafea6
//微信刘望舒
https://mp.weixin.qq.com/s?__biz=MzAxMTg2MjA2OA==&mid=2649878566&idx=1&sn=f202f500daad30358f6127f7201dade1&chksm=83bff97db4c8706bd4ab09fa77d5328cfa02d1fc6d5c6872a51cfe585c3393e2488a0ffbb0da&scene=27
https://blog.csdn.net/fuzhongbin/article/details/126532554
// ProceedingJoinPoint及JoinPoint解析,包括获取方法的注解
https://blog.csdn.net/huluwa10526/article/details/110916709?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-110916709-blog-110196949.pc_relevant_3mothn_strategy_recovery&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-110916709-blog-110196949.pc_relevant_3mothn_strategy_recovery&utm_relevant_index=1
//AspectJ切点表达式语法
https://www.jianshu.com/p/dadc7d730489/
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx/issues/301
https://blog.csdn.net/chuyouyinghe/article/details/124443785
https://www.itcast.cn/news/20210525/16125017526.shtml