一.简介
Aspect Oriented Programming(AOP)面向切面编程是目前比较流行的一种编程方式,切面是指从不同的角度来看待同一个事物,比如我们做一个Android app需求时候,是从业务逻辑角度考虑的,需要实现一个Activity,实现一个model,实现一个View,再在Activity里完成对View和model控制等等;再比如我们需要对整个app的所有Activity做一些性能监控,这时候就需要从整个项目的角度来考虑,需要实现的就是统一对每个Activity或基类做处理,而不是某个业务的Activity了。
AOP就是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种编程思想,也就是不需要侵入代码(比如修改基类等)就可以实现功能模块,而AspectJ就是该思想的一种具体实现方式,它是一个面向切面的框架,它扩展了Java语言。
二.原理
AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
AspectJ定义了一组注解,对应着一组概念,还有一套匹配表达式,我们通过书写匹配表达式,告知AspectJ我们想在哪些地方添加代码,然后再通过不同的注解,决定我们想以什么样的方式在源代码处做什么样的事,最后在编译时,AspectJ的编译器就会按照我们的想法,在源代码里注入新的代码,也可以理解为AspectJ hook住了编译过程,添加了一些代码。
先举个简单的小例子来验证下AspectJ的实现效果:
假如我们想要在所有的Activity的onCreate执行时,先输出一个log,那么我们定义自己的切面
@Aspect
class InheritAspect {
private companion object {
private const val TAG = "InheritAspect"
private const val ON_CREATE_EXECUTION = "execution(void *..*Activity.onCreate(..))"
}
@Pointcut(ON_CREATE_EXECUTION)
fun onCreateExecution() {
}
@Before("onCreateExecution()")
fun beforeOnCreateExecution(joinPoint: JoinPoint) {
Log.i(TAG,"onCreate start")
}
}
代码先不用管,意思就是定义了一个切面,拦截的是所有Activity的onCreate方法的执行,并且在执行时,输出一个log
下面我们来看看,运行后其中一个Activity的文件,反编译过后的结果:
//源代码
protected void onCreate(@Nullable Bundle var1) {
super.onCreate(var1);
}
//运行后
protected void onCreate(@Nullable Bundle var1) {
JoinPoint var2 = Factory.makeJP(ajc$tjp_7, this, this, var1);
//拿到InheritAspect的单例对象
InheritAspect.aspectOf().beforeOnCreateExecution(var2);
super.onCreate(var1);
}
我们可以看到,在这个Activity的onCreate方法中,先执行的就是InheritAspect类的beforeOnCreateExecution方法(JoinPoint对象就是包装了一些切点的信息),该方法就是我们上面定义的那个方法,输出了log,可见,AspectJ的编译器按照我们的想法,在源码上添加了代码。
知道了大概原理,我们就来看看怎么使用吧!
三.使用
(一)基本概念
1.连接点(JoinPoint)
连接点是程序中可以插入代码的地方,比如调用一个方法、一个方法执行中、调用一个构造器、一个构造器执行中等等,被AspectJ支持的连接点如下:
2.切点(PointCut)
切点其实就是想要在哪些地方插入一段代码,也就是一组连接点集合的逻辑组合关系,比如一个切点可以定义为:调用A类的a方法时||调用B类的b方法时;这样当调用A类的a方法这个连接点或者调用B类的b方法这个连接点时都会被该切点切入,从而插入代码
3.插入逻辑(Advice)
上面说的是定义的切点,即静态点,有了静态点后,就需要插入代码了,插入代码的方式AspectJ也定义了几种:
-
Before
在连接点之前插入代码
-
After
在连接点之后插入代码
-
Around
代理连接点,可以自定义是否执行连接点或返回何种结果等
4.切面(Aspect)
切面就是包装了连接点、切点、插入逻辑的单一模块,也是AspectJ扫描的单元,一个切面定义了一组AOP功能
以上是AspectJ的基本概念,下面带着这些概念,进入到具体的使用学习中吧!
(二)类型匹配表达式
对于AOP来说,最重要的就是要准确的找到想要切入的点了,AspectJ提供了一套匹配表达式来完成,主要的结构如下({}为可选项):
{注解}{修饰符}<返回值>{类}<方法名><方法参数>
下面我们来结合例子来学习,如何使用匹配表达式定位到具体方法
1.注解
此处的注解为标注在方法上的注解
@java.lang.Deprecated * *(..)
表示标有@java.lang.Deprecated注解的所有方法
2.修饰符
public * *(..)
表示所有public的方法,这个不用多说
3.返回值
* *(..)
*表示任意类型,所以该表达式表示任何方法
void *(..)
表示返回值为void的所有方法
java.lang.String *(..)
表示返会String类型的所有方法
java.lang.String+ *(..)
+表示子类,所以该表达式表示返回String及其子类类型的所有方法
java.lang.* *(..)
表示返回java.lang包下所有类型的方法
java.lang.String* *(..)
*也可以作为任意字符个数的匹配(前缀后缀),该表达式表示返回java.lang包下,以String为前缀的所有类型,的所有方法
java..* *(..)
…表示的是当前包及其任意子包下(包括子包的子包等),该表达式表示返回java包及其底下所有包下的类型,的所有方法
(@java.lang.Deprecated *) *(..)
此处注解声明在*上(括号包住),表示该返回值的类型上有@java.lang.Deprecated注解的所有方法
4.类
类的匹配规则大多与返回值类似
* com..*Activity+.*(..)
表示com包及其子包下,以Activity为后缀的所有类及其子类,的所有方法,此处+、*与…和返回值处一样
* (@java.lang.Deprecated *).*(..)
表示所有标有@java.lang.Deprecated注解的类的所有方法
5.方法名
方法名就相对简单了,只有前缀后缀
* on*(..)
表示所有以on为前缀的方法
6.方法参数
方法参数与上述匹配符略有不同
* *(..)
表示所有方法,此处的…指的是不限参数个数和类型
* *()
表示无参的所有方法
* *(*)
此处的*表示只有一个参数,类型任意,该表达式表示所有只有一个参数的方法
* *(java..String*+)
参数的类型表达式与上述相同,该表达式表示只有一个参数,且参数类型为java及其子包下以String为前缀的类型及其子类型,的所有方法
* *(@java.lang.Deprecated (@java.lang.Deprecated *))
外层的注解,表示方法的这个参数上声明这该注解(也可以写为* (@java.lang.Deprecated ())),而内层的注解标注在*上,表示该参数的类型上标注着该注解;所以该表达式表示,有一个标有@java.lang.Deprecated注解的参数,且该参数类型上也标有@java.lang.Deprecated注解的所有方法,如:
public void a(@Deprecated Cat cat){
}
@Deprecated
class Cat{
}
7.组合使用
以上就是类型匹配的表达式规则,下面我们组合起来写一个复杂点的表达式来加深理解:
@java.lang.Deprecated public java.lang.String com..*Activity+.test*(*,int,..)
该表达式表示:
在com及其所有子包下,以Activity为后缀的所有类及其子类中,以test为前缀的,且第一个参数是任意类型,第二个参数是int,后面的参数个数类型不限的方法,且方法是public的还带有@java.lang.Deprecated注解
(三)切入点组合表达式
上面我们学会了如何找到指定的方法,但作为一个切点,还需要有其他更加灵活的限制条件来帮助我们插入代码,因为我们往往不止要找到方法,还有满足其他一些条件才行,并且这些条件我们可以通过逻辑关系进行组合,即使用&&、||、!操作符
1.call/execution
上面说过,AspectJ认可的连接点有很多,对于方法来说,主要有两个,就是call和execution,这两个连接点一定要弄清楚区别
call是指该方法在被外部调用的时候,而execution指该方法正在被调用的时候,即在方法体内的时候,这么说可能不清楚,举个栗子:
call(* test(..))
execution(* test(..))
public void a(){
//call start
test()
//call end
}
public void test(){
//execution start
...
//execution end
}
由例子可知,如果我们的切点是基于call的,则我们的代码是插入到a方法里调用test方法前后的,而不能管理test方法内部的执行过程;
如果我们的切点是基于execution的,则我们的代码是插入到test方法里面的具体执行逻辑的前后的,可以影响test方法的内部执行
----------华丽的分割线----------
以上的内容其实都是编译时静态决定的,比如* *(android.content.Context+),我们拦截的是所有带有一个Context或其子类型参数的方法,这在编译期通过方法形参类型声明就已经确定了,但是如果我们想准确截获的是这些方法内,实际参数类型是Activity的呢?这就需要插入代码进行运行时判断了,也就是需要一些运行时的筛选条件帮我们定位,下面我们就来看看AspectJ为我们提供了哪些!
需要注意的就是以下的这些动态筛选条件,都不支持表达式匹配,而只能使用全路径(因为是在业务代码里添加的代码)
2.this
连接点JoinPoint提供了this这个属性,这个属性代表的是当前的AOP代理对象,也可以理解为上下文环境,举个栗子就明白了:
class AspectJActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_aspectj2)
val animal: Animal = Cat()
//call
animal.action()
}
}
class AspectJActivity2 : AspectJActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
abstract class Animal {
abstract fun action()
}
class Cat : Animal() {
override fun action() {
//execution
}
}
(1)call(* action(..))&&this(AspectJActivity2)
对于call来说,连接点执行的上下文this就是AspectJActivity2的对象,也就是当前Activity的实际类型,因为AspectJ会在call处添加动态判断代码:
if(xxx instanceof AspectJActivity2){//执行插入代码段}
(2)execution(* action(..))&&this(Cat)
对于execution来说,连接点的上下文已经转移到了方法内部,即该方法的对象内部了,此时this就是Cat对象,原理与上述相同,也是加入动态判断代码决定是否执行
3.target
连接点JoinPoint提供了target这个属性,这个属性代表的是真正执行该方法的对象,也就是该方法的对象了
还举上述例子的话
call(* action(..))&&target(Cat)
execution(* action(..))&&target(Cat)
对于call和execution来说,target都是Cat对象,因为真正执行action方法的是Cat对象
其原理其实也是在需要的地方,加入动态代码段进行判断:
if(xxx instanceof Cat){//执行插入代码段}
4.within
连接点JoinPoint提供了within这个属性,这个属性是个静态属性,即该连接点的执行声明在哪个类型中,withinType就是哪个类型
还举上述例子
call(* action(..))&&within(AspectJActivity)
execution(* action(..))&&within(Cat)
当运行AspectJActivity2时,在基类里也会执行action,但此时的withinType仍然是AspectJActivity
对于execution,withinType就是定义方法的类,即action方法的类本身
5.args
连接点JoinPoint提供了args,用来约束实参的类型,举个栗子:
class AspectJActivity2 : AspectJActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
a(this)
}
}
fun a(context:Context){
}
call(* a(Context+))&&args(Activity)
execution(* a(Context+))&&args(Activity)
对于方法a,接受的参数类型是Context,而我们使用args约束为Activity,则实际调用时,只有实参类型为Activity时才会执行插入代码
也是通过插入动态判断代码实现的:
if(xxx instanceof Activity){//执行插入代码段}
6.注解支持
对于以上的动态筛选条件,都是匹配类的,还有一种情况就是注解,比如想要指定在运行中的“this”持有某个注解,此时就不能直接写在this()里了,因为this()里只支持限定类
对此AspectJ提供了相应的匹配符来实现,其实就是在原有的匹配符前面加上@符号,括号里仍然写类名(注解类)罢了,并且向源代码中插入动态判断代码:使用xxx.getClass().isAnnotationPresent(xxx.class)
(1)@this/@target/@within
@this(java.lang.Deprecated)/@target(java.lang.Deprecated)/@within(java.lang.Deprecated)
表示this/target/withinType对象带有java.lang.Deprecated注解
(2)@args
@args(java.lang.Deprecated,java.lang.SafeVarags)
表示第一个实际参数对象带有java.lang.Deprecated注解,第二个实际参数带有java.lang.SafeVarags注解
(3)@annotation
@annotation(java.lang.Deprecated)
表示执行的方法带有java.lang.Deprecated注解,感觉和类型匹配表达式里的意思一致
7.组合使用
以上就是动态条件的定义方式,下面通过一个组合例子加深一下理解:
call(* *(..))&&this(com.aspect.AspectJActivity2)&&target(com.model.Cat)&&within(com.aspect.AspectJActivity)&&args(com.model.Animal)&&@args(java.lang.Deprecated)
该表达式表示的连接点为:当前上下文环境(this)是AspectJActivity2对象,且方法的执行对象(target)是Cat对象,且调用声明在AspectJActivity类中,且第一个实际参数是Animal类型,且该实际参数类型上标有Deprecated注解,的所有方法,被外部调用时
8.定义切点
现在我们学会了如何书写切点表达式,那么接下来我们看看如何定义一个切点吧
AspectJ提供了@PointCut注解来实现切点的定义,我们只需要写一个方法,定义该注解,并且将注解的value设置为我们的切点表达式即可—代表该方法就是一个切点,具体切点就是我们定义的表达式,如下:
@Pointcut("call(* *(..))")
fun onPointCutCall() {
}
该切点表示在所有方法被外部调用时
(四)插入逻辑
现在,我们有了切点,找到了具体的插入代码位置,接下来就可以插入代码了
上面说过,AspectJ提供了插入代码的几种方式:之前、之后、整体代理,相应的也是提供了几种注解来实现,我们也只需要定义一个方法,标注相应的注解,将其value设置为想要拦截的切点(即我们上面说的PointCut),方法内部的代码就会被插入到切点位置了,并且AspectJ还可以自动提供JoinPoint类型的参数,包装基本信息供我们使用
1.@Before
该注解就是指在切入点代码执行前插入一段代码,如下:
@Before("onPointCutCall()")
fun beforePointCutCall(joinPoint: JoinPoint) {
//输出log
Log.i(TAG,"doing before")
}
表示在所有方法被外部调用前,加入一段代码:输出log
2.@After
该注解就是指在切入点代码执行后插入一段代码,默认情况下,包括切入点代码正常return后、或throw异常退出后,都会执行,如下:
@After("onPointCutCall()")
fun afterPointCutCall(joinPoint: JoinPoint) {
//输出log
Log.i(TAG,"doing after")
}
表示在所有方法被外部调用后,加入一段代码:输出log
3.@Around
该注解就是指,将切入点代码完全hook住,放到一个闭包里,整个替换为我们定义的方法体,并且我们可以拿到这个闭包内容,决定是否执行切入点源代码以及返回什么内容等等,如下:
@Around("onPointCutCall()")
fun aroundPointCutCall(joinPoint: ProceedingJoinPoint):Any? {
Log.i(TAG,"doing before")
//执行切入点源代码
val res = joinPoint.proceed()
Log.i(TAG,"doing after")
return res
}
表示在所有方法执行前,输出log,然后执行方法,最后再输出log,而返回值还是取自源代码的结果
此时参数是ProceedingJoinPoint,是JoinPoint的子类,事实上,每个连接点返回的都是该类型对象,该对象可以调用proceed()方法执行源代码
4.@AfterReturning
该注解是指在方法return(不管有没有显示的return)之后执行的hook,并且可以得到方法return的返回值
@AfterReturning(value = "xxx", returning = "result")
public void afterReturnPointCut(boolean result) {
System.out.println("result is "+result);
}
注解的returning参数指的是返回值的参数名,要与方法参数列表中的对应参数名称相同
如果方法没有返回值,那么AfterReturning捕获的result就是null
5.@AfterThrowing
该注解是指在方法抛出异常后(不包括内部try-catch)执行的hook,并且可以得到抛出的异常对象
@AfterThrowing(value = "xxx", throwing = "ex")
public void afterReturnPointCut(RuntimeException ex) {
throw ex;
}
直接的throwing参数指的是所捕获的异常的参数名,要与方法参数列表中的对应参数名称相同
6.JoinPoint
该接口时接入点的基类接口,提供了一些编译时信息和运行时信息,默认会传递给插入方法,它在每个插入方法调用前都会构建相应的对象,下面来看看它提供的一些参数意义:
/**
* this:AOP代理对象
* target:目标对象
* args:参数类型列表
* signature.methodName:连接点的方法名
* signature.declaringTypeName:连接点的方法属于的类型(编译时类型)
* sourceLocation.withinType:连接点声明类(编译时类型)
* sourceLocation.fileName:调用连接点方法的源码文件
* sourceLocation.line:调用连接点方法的源码的行数
*/
fun showJoinPoint(joinPoint: JoinPoint, tag: String = "JoinPoint") {
val thisObj = "this:${joinPoint.`this`?.javaClass?.name ?: "no"}\n"
val targetObj = "target:${joinPoint.target?.javaClass?.name ?: "no"}\n"
val args = "args:${joinPoint.args?.map { it?.javaClass?.name ?: "no" } ?: "no args"}\n"
val methodName = "methodName:${joinPoint.signature.name}\n"
val declareType = "declareType:${joinPoint.signature.declaringTypeName}\n"
val withinType = "withinType:${joinPoint.sourceLocation.withinType?.name ?: "no within type"}\n"
val sourceLocation = "sourceLocation:${joinPoint.sourceLocation.let { "${it.fileName}-${it.line}" }}\n"
}
这里要说明的是declareType,该类型指的是连接点方法属于的对象,其编译时的类型,比如call(* *(…))有两处调用:
val animal:Animal = Cat(); animal.action()
val cat:Cat = Cat(); cat.action()
对应的declareType分别是Animal和Cat
ProceedingJoinPoint
该类型是JoinPoint的子类,多出来的是proceed()方法,用于执行切入点的源代码
该类型也是运行中实际创建的JoinPoint类型,before和after执行proceed没有意义,所以一般不用;而around一般用这个类型,用于执行源代码
7.运行时参数
以上是插入代码方式,但是这还不够,因为我们很有可能在插入的代码中,去操作实际参数,甚至是this、target对象等,而我们现在的参数只有JoinPoint,这显然不够,下面就来说说如何拿到运行时的一些对象
先来看例子:
@Pointcut(value = "call(* *(..))&&this(activity)&&target(cat)&&args(value)", argNames = "activity,cat,value")
fun onPointCutCall(activity: AspectJActivity, cat: Cat, value: String) {
}
@Before(value = "onPointCutCall(activity,cat,value)", argNames = "activity,cat,value")
fun beforePointCutCall(joinPoint: JoinPoint, activity: AspectJActivity, cat: Cat, value: String) {
//do with params
}
上面的例子做到了拿到实际的运行时参数,怎么做到的呢?
AspectJ切点和插入逻辑的注解,不仅有value参数声明切点,还提供了argNames参数,用来声明参数名,而这些参数名就是定义在切入点表达式里的this/target/args里的类型,只不过和前面说的直接声明类型不一样,这里用参数名来代替了,那他们的类型又是什么呢?就是我们声明在插入方法的参数列表的同名参数的类型:
比如上面的this(activity),名字为activity,对应的插入方法的参数就是activity: AspectJActivity参数,即类型是AspectJActivity,所以就相当于this(AspectJActivity),并且调用时把this的实际对象传入到方法中,就是这么简单,只要参数名对应上就行;而第一个参数JoinPoint我们可以不用声明,因为它默认会被传入到第一个参数中
该功能只适用于动态筛选条件:this/target/args
8.@DeclareParents
该注解是指,为指的切入点的类,实现一些接口,即添加一些公共功能
@DeclareParents(value = "com.test.Poi+", defaultImpl = PoiClaimCounter.class)
public Counter counter;
该例子为:对Poi及其子类实现了一个Counter接口,默认实现为PoiClaimCounter类,为其添加了一个计数器功能
这是,Poi类及其子类就可以转换为Counter类型了,如:
@After(value = "xxx(counter)", argNames = "counter")
public void afterPoiClaimCounter(Counter counter) {
counter.count();//可以使用Poi的Counter的功能
}
(五)定义切面
以上,我们学会了如何定义切点,如何插入代码,他们都是写在一个类里的,剩下的,就是上面说过的,把他们包装在一起形成一个模块,供AspectJ的编译器进行扫描
AspectJ提供了@Aspect注解,我们只需将上述的类标有该注解即可,非常简单
@Aspect
class TestAspect {
private companion object {
private const val POINT_CUT = "call(* *(..))"
}
@Pointcut(POINT_CUT)
fun onPointCut() {
}
@Before("onPointCut()")
fun beforePointCut(joinPoint: JoinPoint) {
...
}
}
编译时,编译器就会扫描到标有@Aspect注解的该类,进行应用
四.注意事项
在实际使用的时候,发现了一些细节的点,总结了一些以供参考:
-
在类型匹配时,Number+,代表的是Number及其子类型,比如Double,Integer,但是不包括double,int,这些原始类型需要单独声明,比如int *(…)
-
对于同一个切点,如果同时声明了@After和@Before,则不能声明@Around,会报错-编译器不能决定插入顺序;但是声明其中之一和@Around是可以的
-
在匹配表达式中,如果父类的方法作为了切点,那么其子类重写的相应方法也会被作为切点,比如:
execution(void com.aspect.AspectJActivity.test())
-
AspectJActivity的test方法,以及AspectJActivity2重写的test方法,都会被切入,即使表达式没有写成com.aspect.AspectJActivity+
-
然而对于AspectJActivity的父类test方法,则不会被切入
而对于
call(void com.aspect.AspectJActivity.test())
-
在AspectJActivity中,调用this.test()方法时,会被切入
-
在AspectJActivity2中,直接调用test()方法(即this.test())也会被切入,但如果调用super.test(),则不会被切入(即使实质上调用的是AspectJActivity的test()方法),这点很神奇
-
-
关于动态筛选条件,this/target/args等,都是插入的动态判断代码,如instanceof、getClass().isAnnotationPresent()等,所以用的都是实际运行时类型,且包括了对子类的检测,即使我们没有书写成this(AspectJActivity+)的形式(事实上并不能这么写,因为动态筛选条件不支持匹配表达式)
-
对于动态筛选条件使用的注解,因为要在运行时判断,所以需要其保持在运行时,即需要
@Retention(RetentionPolicy.RUNTIME)