前言
本文是AOP入门系列的基础文章,不会讲太多很深入的东西,如果为你想学习AOP,但是遇到了种种障碍而无法进一步学习,那本文或许能给你提供一些帮助和思路。
我个人在学习aop的过程中也和很多新手一样碰到过各种麻烦的问题,比如说在入门aop的时候,需要做aspectj
相关的配置,这一步失败了就无法继续coding
;比如,切点表达式写错了,那就无法拦截原生方法做下一步的业务逻辑的操作;甚至说对aop相关的很多概念模糊不清,只知道几个常用的相关注解@Aspect
、@Before
、@Around
,只能看着别人的demo去敲一下常用的案例却无法在自己的项目中灵活的定制自己的aop。不过没有关系,这个系列,我会从基础---应用---原理
帮大家了解aop相关的重要知识。
推荐阅读系列文章
Android aop Advice(通知、增强)
Android aop(AspectJ)查看新的代理类
Android AOP面向切面编程详解
防止按钮连续点击
下面开始讲解aop切点表达式,在这之前我希望你做好2件事:
- 1、aop是做什么的?能解决什么问题?为什么要用它?
- 2、android studio中配置好aop。推荐使用aspectjx
一、案例分析:
@Aspect
public class Test {
final String TAG = Test.class.getSimpleName();
@Before("execution(* *..MainActivity.on*(..))")
public void logLifeCycle(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getThis().getClass().getSimpleName();
Log.e(TAG, "class:" + className+" method:" + methodSignature.getName());
}
@Around("execution(* *..MainActivity.testAOP())")
public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String key = proceedingJoinPoint.getSignature().toString();
Log.e(TAG, "onActivityMethodAroundFirst: before " + key+"do something");
//执行原方法
proceedingJoinPoint.proceed();
Log.e(TAG, "onActivityMethodAroundSecond: after " + key+"do something");
}
}
这里写了2个方法,logLifeCycle()
是在MainActivity
的生命周期的方法中打印log
日志;onActivityMethodAround
是拦截MainActivity
中的一个testAOP()
方法,在原方法执行执行前、后做一些事情。
重点是搞清楚@Aspect
、@Before
、@Around
这几个注解,难点是切点表达式的书写。"execution(* *..MainActivity.on*(..))"
和execution(* *..MainActivity.testAOP())
就是切点表达式。
这个地方是非常容易犯错的,因为很多人对这个规则没有搞懂,对很多概念模糊不清,所以很多新手难以自定义切点表达式。
下面从几个基础概念出发,带你逐步掌握自定义切点表达式。
(提示:别看见概念有点多,分类也多就害怕了,其实不用害怕的,这里先做一个全方位的了解,后面写表达式的时候是很简单的)
二、AOP相关的几个基础概念
-
Joinpoint(连接点)
:是指那些被拦截到的点,比如说方法的调用,成员属性的访问甚至异常的处理; -
Pointcut(切入点)
:所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。 -
Advice(通知)
:所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。 -
Weaving(织入)
:是指把增强应用到目标对象来创建新的代理对象的过程. AspectJ 采用编译期织入和类装在期织入 。 -
Aspect(切面)
:是切入点和通知(引介)的结合 。
2.1Advice(通知)
类型 | 描述 |
---|---|
Before | 前置通知, 在目标执行之前执行通知 |
After | 后置通知, 目标执行后执行通知 |
Around | 环绕通知, 在目标执行中执行通知, 控制目标执行时机 |
AfterReturning | 后置返回通知, 目标返回时执行通知 |
AfterThrowing | 异常通知, 目标抛出异常时执行通知 |
2.2切入点指示符
切入点指示符用来指示切入点表达式目的,AspectJ切入点指示符如下:
execution
:用于匹配方法执行的连接点within
:限制链接点匹配指定的类型this
:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配target
:限制链接点匹配目标对象为指定类型的类@within
:匹配所有使用了xx注解的类(注意是类)@annotation
: 匹配使用了xx注解的方法(注意是方法)
2.3切入点表达式语法
比如上面案例中的execution
表达式
execution(* *..MainActivity.on*(..))
-
execution()
:切入点指示符; -
第一个*
:表示任意方法返回类型; -
*..
:表示省略了MainActivity
的包名,当然这里也可以写出完整包名; -
on*
表示MainActivity
中所有以on
开头的方法; -
(..)
:表示方法的参数可以是任意类型且个数任意
AspectJ类型匹配的通配符:
1、 *:匹配任何数量字符;
2、..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
3、+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
三、@Pointcut
3.1、@Pointcut的基本定义
这里还是以打印Activity生命周期为例,先看看之前的写法
@Before("execution(* *..MainActivity.on*(..))")
public void logLifeCycle(JoinPoint joinPoint) { }
这里直接用通知@Before
+切入点表达式就完成了。
下面看看另一种实现方式
定义切点
//切点表达式
@Pointcut("execution(* *..MainActivity.on*(..))")
//切点签名方法
private void log(){}
使用定义的Pointcut
//使用切点方法
@Before("log()")
public void logLifeCycle(JoinPoint joinPoint) { }
这2种方式的实现效果是完全一样的,但是方法2比方法1更灵活。@Pointcut是专门用来定义切点的,它让切点表达式可以复用。
3.2、@Pointcut带参数
3.2切点指示符
切点指示符是切点定义的关键字,切点表达式以切点指示符开始。可以切点指示符来告诉切点将要匹配什么,有以下9种切点指示符:execution
、within
、this
、target
、@within
、@annotation
,下面一一介结这几种切点指示符。
提示:由于篇幅有限,下面只展示切点表达式,完整代码可以在文末地址下载查看
execution
execution
表达式语法:
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
注意:execution
的粒度为方法,也就是说是匹配方法的
除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。
execution
是一种使用频率比较高的切点指示符,它以方法的执行作为切入点。它可以说是最重要的,其它的指示符都是辅助它的。
比如通过如下表达式可以精确地匹配到Person
类里的eat()
方法
@Before("execution(* com.zx.aop1.Person.eat())")
如果要匹配Person
类里的所有方法,可以使用通配符。
@Before("execution(* com.zx.aop1.Person.*(..))")
第一个*
表示返回值为任意类型,第二个*
表示这个类里的所有方法,()
括号表示参数列表,括号里的用两个点号表示匹配任意个参数,包括0个。
within
为了方便类型(如接口、类名、包名)过滤方法,AOP 提供了within
关键字。其语法格式如下:
within(<type name>)
注意:within
的粒度为类
匹配com.zx.aop1.person.Student类中的所有方法
@Pointcut("within(com.zx.aop1.person.Student)")
匹配com.zx.aop1.person包及其子包中所有类中的所有方法
@Pointcut("within(com.zx.aop1.person..*)")
匹配com.zx.aop1.person.Person类及其子类的所有方法
@Pointcut("within(com.zx.aop1.person.Person+)")
匹配所有实现com.zx.aop1.person.Human接口的类的所有方法,包括接口方法和实现类的额外方法
@Pointcut("within(com.zx.aop1.person.Human+)")
args
用于匹配当前执行的方法传入的参数 (args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用)
@Pointcut(value = "execution(* com.zx.aop1.MainActivity.testArgs(..)) && args(arg1)")
private void pc1(String arg1){}
/**
* 通过args()传参数
* @param arg1
*/
@Before("pc1(arg1)")
public void testArgs1(String arg1){
Log.e(TAG, "testArgs1--- : "+arg1 );
}
这种获取参数的方式不太灵活,而且开销大,所以可以用另外一种更加便捷的方式获取参数,可以通过joinPoint.getArgs()
的方式去拿方法参数。比如上面例子,可以用如下方式实现:
@Pointcut(value = "execution(* com.zx.aop1.MainActivity.testArgs(..))")
private void pc2(){}
/**
* 通过JoinPoint连接点的方式去拿方法参数
* @param joinPoint
*/
@Before("pc2()")
public void testArgs2(JoinPoint joinPoint){
for (Object arg : joinPoint.getArgs()) {
Log.e(TAG, "testArgs2---: "+arg );
}
}
@within
语法:
@within(注解类型)
匹配所有使用了CheckWithin
注解的类(注意是类)
@Pointcut("@within(com.zx.aop1.CheckWithin)")
只能作用于类,不能是方法,也不能是接口。
@annotation
匹配使用了CheckAop
注解的方法(注意是方法)
@Pointcut("@annotation(com.zx.aop1.CheckAop)")
private void aAnnotation1() {
}
@After("aAnnotation1()")
public void testaAnnotation(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
CheckAop checkAop = method.getAnnotation(CheckAop.class);
Log.e(TAG, "testaAnnotation--: "+checkAop.value() );
}
这里通过JoinPoint
拿到对应的方法,再通过反射获取该方法的注解的值。此外还有一种简单的方法来获取注解值。
@Pointcut(value = "@annotation(checkAop)")
private void aAnnotation2(CheckAop checkAop) {
}
@After("aAnnotation2(checkAop)")
public void testaAnnotation2(CheckAop checkAop) {
Log.e(TAG, "testaAnnotation2---: "+checkAop.value() );
}
target
用来匹配的链接点所属目标对象必须是指定类型的实例。
public interface Human {
void setGender(int gender);
}
定义接口实现方法
public class Person implements Human {
public int gender;
@Override
public void setGender(int gender) {
this.gender = gender;
}
}
定义调用代码
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Person person = new Person();
person.setGender(1);
}
}
Aspect定义1:target() && call()
@Before("target(com.zx.aop1.person.Human) && call(* *..setGender(..))")
public void testThis(JoinPoint joinPoint){
Log.e(TAG, "testThis ----: "+joinPoint.toString() );
Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
}
Aspect定义2:target() && execution()
@Before("target(com.zx.aop1.person.Human) && execution(* *..setGender(..))")
public void testThis(JoinPoint joinPoint){
Log.e(TAG, "testThis ----: "+joinPoint.toString() );
Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
}
结果分析:当使用
target()
时,不管用execution()
还是call()
,Target对象都是Person
,而this对象是不一样的。
this
用来匹配链接点所属的对象引用是某个特定类型的实例。
Aspect定义3:target() && call()
@Before("this(com.zx.aop1.person.Human) && call(* *..setGender(..))")
public void testThis(JoinPoint joinPoint){
Log.e(TAG, "testThis ----: "+joinPoint.toString() );
Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
}
结果:这里是没有任何日志输出的,因为这里是没有插入代码的。
原因待会儿再说。
Aspect定义4:this() && execution()
@Before("this(com.zx.aop1.person.Human) && execution(* *..setGender(..))")
public void testThis(JoinPoint joinPoint){
Log.e(TAG, "testThis ----: "+joinPoint.toString() );
Log.e(TAG, "getTarget() ----: "+joinPoint.getTarget() );
Log.e(TAG, "getThis() ----: "+joinPoint.getThis() );
}
结果分析:当使用
this()
时,如果用call()
,this对象是空,如果用execution()
还是,this对象都是Person
。原因也很简单,本案例的this对象只能是Human
接口的实现类,而当你用用call()
时,插入代码是在MainActivity
中。
target
与this
总结:
1、target指代的是切点方法的所有者,而this指代的是被织入代码所属类的实例对象。
2、如果当前要代理的类没有实现某个接口就用this
;如果实现了某个接口,就使用target
。
组合切点表达式
&&
:要求连接点同时匹配两个切点表达式||
:要求连接点匹配至少一个切入点表达式!
:要求连接点不匹配指定的切入点表达式
匹配所有实现Human
接口的类的所有方法且方法的第一个参数为int
类型
@Pointcut("within(com.zx.aop1.person.Human+) && execution(* com.zx.aop1.person...* *(int,..))")
更多切点指示符和切点表达式的应用,请关注后续文章!
写文不易,如果本文对你有所帮助,请点个关注 或 赞!