上篇文章介绍了一些切入点表达式,本篇文章进行详细阐述一下。
一、AOP切入点表达式
支持切点标识符 Spring AOP支持使用以下AspectJ切点标识符(PCD),用于切点表达式:
-
execution: 用于匹配方法执行连接点。 这是使用Spring AOP时使用的主要切点标识符。 可以匹配到方法级别 ,细粒度
-
within: 只能匹配类这级,只能指定类, 类下面的某个具体的方法无法指定,粗粒度
-
this: 匹配实现了某个接口:this(com.xyz.service.AccountService)
-
target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。
-
args: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中变量是给定类型的实例。 AOP) where the arguments are instances of the given types.
-
@target: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注解。
-
@args: 限制匹配连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
-
@within: 限制与具有给定注解的类型中的连接点匹配(使用Spring AOP时在具有给定注解的类型中声明的方法的执行)。
-
@annotation:限制匹配连接点(在Spring AOP中执行的方法具有给定的注解)。
1-1、表达式语法
访问修饰符:可不写 可以匹配任何一个访问修饰符
包名和类名:可不写 代码任意包下面的类
返回值:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名,如果被切入的方法返回值不一样可以使用*代表所有的方法值都能匹配
包名:com.* == com.jony == com.任意名字 但是只能匹配一级 比如 com.jony.service就无法匹配
如果要com.jony.service ==>com.jony.service , com.jony.* ==>com.jony.service.impl就无法匹配
com.jony..* ==>com.jony.service.impl 可以匹配
类名: 可以写*,代表任何名字的类名。 也可以模糊匹配 *ServiceImpl==> UserServiceImpl ==>RoleServiceImpl
方法名:可以写*,代表任何方法。 也可以模糊匹配 *add==> useradd ==>roleadd
参数:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名。 如果需要匹配任意参数 可以写:..
1-2、切入点标识符
1-2-1、within表达式
通过类名进行匹配 粗粒度的切入点表达式
within(包名.类名),则这个类中的所有的连接点都会被表达式识别,成为切入点。
within(com.jony.service.UserServiceImpl)
在within表达式中可以使用*号匹配符,匹配指定包下所有的类,注意,只匹配当前包,不包括当前包的子孙包。
within(com.jony.service.*)
在within表达式中也可以用*号匹配符,匹配包
within(com.jony.*.*)
在within表达式中也可以用..*号匹配符,匹配指定包下及其子孙包下的所有的类
within(com.jony..*)
1-2-2、execution()表达式
细粒度的切入点表达式,可以以方法为单位定义切入点规则
语法:execution(返回值类型 包名.类名.方法名(参数类型,参数类型…))
例子1
execution(void com.jony.service.UserServiceImpl.addUser(java.lang.String))
该切入点规则表示,切出指定包下指定类下指定名称指定参数指定返回值的方法
例子2:
execution(* com.jony.service.*.query())
该切入点规则表示,切出指定包下所有的类中的query方法,要求无参,但返回值类型不限。
例子3:
execution(* com.jony.service..*.query())
该切入点规则表示,切出指定包及其子孙包下所有的类中的query方法,要求无参,但返回值类型不限。
例子4:
execution(* com.jony.service..*.query(int,java.lang.String))
该切入点规则表示,切出指定包及其子孙包下所有的类中的query方法,要求参数为int java.langString类型,但返回值类型不限。
例子5:
execution(* com.jony.service..*.query(..))
该切入点规则表示,切出指定包及其子孙包下所有的类中的query方法,参数数量及类型不限,返回值类型不限。
例子6:
execution(* com.jony.service..*.*(..))
该切入点规则表示,切出指定包及其子孙包下所有的类中的任意方法,参数数量及类型不限,返回值类型不限。这种写法等价于within表达式的功能。
例子7:
execution(* com.jony.service..*.del*(..))
该切入点表示,切入指定包下的所有子孙包下的所有类中以del开头的方法,参数数量及类型不限,返回类型不限
1-2-3、@annotation
@annotation可以通过注解来设置切入点表达式,比如我们给UserServiceImpl里面的所有@Logger注解设置切入点
@annotation(jdk.nashorn.internal.runtime.logging.Logger)
需要注意的是,里面需要填入注解的完整限定名
那如果我们使用@Override是否可以完成切入点呢,答案是否定的,因为我们进入@Override的类,如下
RetentionPolicy.SOURCE 代表只有在java文件下才有作用,等我们java文件编译成class文件的时候,这个注解就不起作用了,所有如果我们切点是@Override是无效的。
我们再看@Logger
RetentionPolicy.RUNTIME 代表运行时起作用,因此这个注解可以被作为切入点。
1-2-4、合并切点表达式
可以使用 &&、|| 和 !等符号进行合并操作。也可以通过名字来指向切点表达式。
&& 需要多个表达式都满足,|| 满足一个 ,!只不满足,示例如下:
execution(void com.jony.service.UserServiceImpl.addUser(java.lang.String)) && @annotation(jdk.nashorn.internal.runtime.logging.Logger)
二、通知方法的执行顺序
spring4.0版本
1、正常执行:@Before>@After>@AfterReturning
2、异常执行:@Before>@After>@AfterThrowing
我这边本地使用spring5.2.x(这个需要特别注意一下)
1、正常执行:@Before>@AfterReturning>@After
2、异常执行:@Before>@AfterThrowing>@After
三、获取方法的详细信息
在上篇文章的案例中 手把手教你spring aop的使用,一下子明明白白的(其一)我们并没有获取Method的详细信息,例如方法名、参数列表等信息,想要获取的话其实非常简单,只需要添加JoinPoint参数即可。
这样方法名、参数等相关信息就可以取到了
@Before("execution(* com.jony.proxy.service..*.*(..))")
public static void before(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//重新输出结果
System.out.println(name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args)));
}
3-1、获取执行结果信息
刚刚只是获取了方法的信息,但是如果需要获取结果,还需要添加另外一个方法参数,并且告诉spring使用哪个参数来进行结果接收
通过上图,可以看到我们之前的表达式,改为了value="xxx",这是因为如果就只有一个的时候value我们省略了,但是因为我们需要接收参数所以value必须写上,另外 表达式后面我们加上了 && args(result),这个代表接收的参数,并且参数只有一个,而且这个参数result,必须和方法内参数名一致,否则会报错,如下:
//后置返回通知
@AfterReturning(value = "execution(* com.jony.proxy.service..*.*(..))",returning = "result")
public static void afterEnd(JoinPoint joinPoint,Object result){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println(name+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args))+",结果是:"+result);
}
执行结果
需要注意的是,获取结果需要加在@Afterruning中
3-2、获取异常信息
也可以通过相同的方式来获取异常的信息,在注解后面添加throwing="ex",然后再参数中添加Exception ex,同样注解的throwing的值需要和参数Exception 的名称一致。
//后置异常通知
@AfterThrowing(value="execution(* com.jony.proxy.service..*.*(..))",throwing = "ex")
public static void afterException(JoinPoint joinPoint,Exception ex){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+ex);
}
执行结果
那如果我们想捕获下面所有的异常信息,就需要添加如下代码中捕获错误信息的代码即可(固定写法)
//后置异常通知
@AfterThrowing(value="execution(* com.jony.proxy.service..*.*(..))",throwing = "ex")
public static void afterException(JoinPoint joinPoint,Exception ex){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//捕获错误信息--begin
StringWriter stringWriter=new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter,true));
//捕获错误信息--end
System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+stringWriter.getBuffer().toString());
}
执行结果
这样就方便我们以后根据错误信息,跟踪到错误的代码了,是不是很方便。
四、spring对通过方法的要求
spring对于通知方法的要求并不是很高,可以任意改变方法的返回值和方法的访问修饰符,但是唯一不能修改的就是方法的参数,会出现参数绑定的错误,原因在于通知方法是spring利用反射调用的,每次方法调用得确定这个方法的参数的值。
//后置通知
@After(value="execution(* com.jony.proxy.service..*.*())")
public static void after(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println("after---"+name+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
}
如上面方法,我们可以把public 改为private或者protect, 返回值也可以改为任意类型,这样都不会收到影响。
五、表达式的抽取@Pointcut
上面我们有LogUtils切面类,然后各个通知的表达式都一样,如果修改表达式,就得同时修改多个地方,这无疑就增加了工作量,甚至不小心还可能出错,因此我们可以使用@Pointcut来进行处理
我们声明了一个point的方法,然后加上@Pointcu注解,然后把表达式写里面,通知就可以使用point()代替原表达式了。
package com.jony.proxy.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
@Aspect//设置切面
@Component //将类注册到IOC容器中,让spring管理,只要这样切面才会生效
public class LogUtils{
//可以使用切点的方式,让其他类通用,重用性更强
@Pointcut("execution(* com.jony.proxy.service..*.*(..))")
public void point(){}
//前置通知
//设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
@Before(value="point()")
public static void before(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//重新输出结果
System.out.println("before---"+name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args)));
}
//后置通知
@After(value="point()")
public static void after(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println("after---"+name+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
}
//后置异常通知
@AfterThrowing(value="point()",throwing = "ex")
public static void afterException(JoinPoint joinPoint,Exception ex){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//捕获错误信息--begin
StringWriter stringWriter=new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter,true));
//捕获错误信息--end
System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+stringWriter.getBuffer().toString());
}
//后置返回通知
@AfterReturning(value = "point()",returning = "result")
public static void afterEnd(JoinPoint joinPoint,Object result){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println("AfterReturning---"+name+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args))+",结果是:"+result);
}
}
六、获取注解中的值
AOP在切面中的通知中,我们也可以获得注解中的值,
6-1、首先我们先创建一个注解
package com.jony.proxy;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
//设置在运行时起作用
@Retention(RetentionPolicy.RUNTIME)
public @interface myannotation {
public String name() default "";
}
6-2、给我们需要切的点添加我们自定义注解,并设置值
我们给UserServiceImpl 中的 get方法添加注解,并设置值为jony
@MyAnnotation(name="jony")
public User get(Integer id) {
System.out.println("查询User");
return userDao.get(id);
}
6-3、修改我们的切面通知
//前置通知
//设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
@Before("point() && @annotation(myAnnotaion)")
public static void before(JoinPoint joinPoint, myannotation myAnnotaion){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//重新输出结果
System.out.println("before---"+name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args))+"注解值为:"+myAnnotaion);
}
需要注意的是,如下图
6-4、测试
如下图,已经成功获得注解对象和name的值,如果想获取具体的值,只需要
//myAnnotation为传入的参数对象
String val=myAnnotaion.name;
6-5、总结
这种使用通过注解获取值,在项目中也会需要到,比如我们切面通知,在处理的时候,需要根据不同注解的值进行不同的判断。
举例:假如我们有两个方法都加了自定义注解,如果注解传入值为a,我们就走A逻辑,如果传入值为b,我们就走B逻辑,就好像我们去机场乘机,普通乘客和VIP乘客都有机票(自定义注解),虽然最终都是一架飞机,但是VIP就能享受不同的乘机待遇(根据票的类型,走不同的逻辑)。
七、环绕通知@Around
通过给方法添加@Around,将方法设置为环绕通知,环绕通知中,可以监控前置、返回、异常、后置通知
//环绕通知
@Around("point()")
public static void around(ProceedingJoinPoint joinPoint){
//获取参数
Object[] args = joinPoint.getArgs();
//获取方法名
String name = joinPoint.getSignature().getName();
//设置返回值
Object obj=null;
//执行方法
try {
System.out.println("环绕前置通知-"+name+"方法开始,参数是:"+Arrays.asList(args));
obj=joinPoint.proceed(args);
System.out.println("环绕返回通知-"+name+"方法开始,返回值是:"+obj);
} catch (Throwable e) {
e.printStackTrace();
System.out.println("环绕异常通知-"+name+"方法出现异常,返回值是:"+e);
}finally {
System.out.println("环绕后置通知-"+name+"方法结束");
}
}
测试结果
可以发现,之前设置的通知和环绕通知都同时执行了,我这边使用的spring版本为5.2.19.RELEASE。其实不同的版本之间执行的顺序是不一样的
7-1、环绕通知顺序
Spring4.0
正常情况:环绕前置>目标方法执行>环绕返回>环绕最终
异常情况:环绕前置>目标方法执行>环绕异常>环绕最终
Spring5.2.19
正常情况:环绕前置>目标方法执行>环绕返回>环绕最终
异常情况:环绕前置>目标方法执行>环绕异常>环绕最终
7-2、混合通知顺序
Spring4.0
正常情况:环绕前置>@Before>目标方法执行>环绕返回>环绕最终>After>AfterReturning
异常情况:环绕前置>@Before>目标方法执行>环绕异常>环绕最终>@After>@AfterThrowing
Spring5.2.19
正常情况:环绕前置>@Before>目标方法执>@AfterReturning>@After>环绕返回>环绕最终
异常情况:环绕前置>@Before>目标方法执行>@AfterThrowing>@After>环绕异常>环绕最终
八、基于Schema的方式实现AOP
上面主要基于注解的AOP配置方式,下面我们开始看一下基于xml的配置方式
8-1、为了顺利,我们先把之前配置的信息去掉
8-1-1、去掉spring.xml里面的 aop配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--扫描包:扫描类中所有注解,不扫描注解不是生效-->
<context:component-scan base-package="com.jony" >
</context:component-scan>
<!--因为我们使用的是注解方式的AOP,所以要开启注解AOP功能-->
<!-- <aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->
</beans>
8-1-2、复制一个切面,并去掉里面的注解
LogUtilsSecond.java
package com.jony.proxy.aspect;
import com.jony.proxy.myannotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
//@Aspect//设置切面
//@Component //将类注册到IOC容器中,让spring管理,只要这样切面才会生效
public class LogUtilsSecond {
//前置通知
//设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
// @Before("point() && @annotation(myAnnotaion)")
public static void before(JoinPoint joinPoint, myannotation myAnnotaion){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//重新输出结果
System.out.println("before---"+name+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args))+"注解值为:"+myAnnotaion+"最终值:"+myAnnotaion.name());
}
//后置通知
// @After(value="point()")
public static void after(JoinPoint joinPoint){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println("after---"+name+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
}
//后置异常通知
// @AfterThrowing(value="point()",throwing = "ex")
public static void afterException(JoinPoint joinPoint,Exception ex){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
//捕获错误信息--begin
StringWriter stringWriter=new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter,true));
//捕获错误信息--end
System.out.println("AfterThrowing---"+name+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args))+stringWriter.getBuffer().toString());
}
//后置返回通知
// @AfterReturning(value = "point()",returning = "result")
public static void afterEnd(JoinPoint joinPoint,Object result){
//获取方法名
String name = joinPoint.getSignature().getName();
//获取所有参数
Object[] args = joinPoint.getArgs();
System.out.println("AfterReturning---"+name+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args))+",结果是:"+result);
}
//环绕通知
// @Around("point()")
public static void around(ProceedingJoinPoint joinPoint){
//获取参数
Object[] args = joinPoint.getArgs();
//获取方法名
String name = joinPoint.getSignature().getName();
//设置返回值
Object obj=null;
//执行方法
try {
System.out.println("环绕前置通知-"+name+"方法开始,参数是:"+Arrays.asList(args));
obj=joinPoint.proceed(args);
System.out.println("环绕返回通知-"+name+"方法开始,返回值是:"+obj);
} catch (Throwable e) {
e.printStackTrace();
System.out.println("环绕异常通知-"+name+"方法出现异常,返回值是:"+e);
}finally {
System.out.println("环绕后置通知-"+name+"方法结束");
}
}
}
8-1-3、再次执行测试代码
AOP的相关数据已经没有了
8-2、使用xml配置aop
8-2-1、开启aop相关设置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--扫描包:扫描类中所有注解,不扫描注解不是生效-->
<context:component-scan base-package="com.jony" >
</context:component-scan>
<!--因为我们使用的是注解方式的AOP,所以要开启注解AOP功能-->
<!-- <aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->
<aop:config>
<!--设置切面类,可以在上面设置一个bean,也可以给类添加@Component注解,我这边选择后者-->
<aop:aspect ref="logUtilsSecond">
<!--设置切面表达式-->
<aop:pointcut id="myPointCut" expression="execution(* com.jony.proxy.service.impl.*.*(..))" />
<!--设置前置通知 如果多个表达式,需要将所有表达式写在pointcut中,并且&符号需要转义使用& 同时注解中的参数也需要和实际方法中的一致-->
<aop:before method="before" pointcut="execution(* com.jony.proxy.service.impl.*.*(..)) && @annotation(myAnnotaion)"/>
<aop:after method="after" pointcut-ref="myPointCut"/>
<!--方法返回通知,可以使用returning 设置返回参数,这个参数仍然需要和方法中的参数名一致-->
<aop:after-returning method="afterEnd" pointcut-ref="myPointCut" returning="result"/>
<!--方法异常通知,可以使用throwing 设置返回异常信息,这个ex 也需要和方法中的参数名一致-->
<aop:after-throwing method="afterException" pointcut-ref="myPointCut" throwing="ex"/>
<aop:around method="around" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
8-2-2、测试结果
总结
1、AOP底层使用的是动态代理的方式实现的,如果有接口则是JDK代理,如果没有接口则是CGlib代理
2、在后置返回通知(@AfterReturning)中可以使用returning获取返回值,在异常通知中(@AfterThrowing)中可以使用throwing获得异常信息
3、使用@excution设置表达式,需要记住表达式的语法
4、使用@annotation设置注解传值
5、使用环绕通知,并知道环绕通知和另外四个通知的执行顺序
6、掌握两种实现AOP的方式:
- 1)给切面类添加注解@Aspect 和@Component,同时spring.xml配置
<aop:aspectj-autoproxy></aop:aspectj-autoproxy> -
- 使用xml方式配置AOP