AOP进阶
通知类型
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
代码测试:
~~~java
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
~~~
重新启动SpringBoot服务,进行测试:
1. 没有异常情况下:
使用postman测试查询所有部门数据
查看idea中控制台日志输出
程序没有发生异常的情况下,@AfterThrowing标识的通知方法不会执行。
2. 出现异常情况下:
修改DeptServiceImpl业务实现类中的代码: 添加异常
@Override
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
int num = 10/0;
return deptList;
}
重新启动SpringBoot服务,测试发生异常情况下通知的执行:
查看idea中控制台日志输出
程序发生异常的情况下:
- @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
- @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)
在使用通知时的注意事项:
- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
在代码测试的过程中可以看到每一个注解里面都指定了切入点表达式,而且这些切入点表达式都一模一样。此时我们的代码当中就存在了大量的重复性的切入点表达式,假如此时切入点表达式需要变动,就需要将所有的切入点表达式一个一个的来改动,就变得非常繁琐了。
~~~java
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
~~~
可以采用抽取方式解决切入点表达式重复的问题
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
~~~java
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){
}
//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
~~~
需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:
全类名.方法名(),具体形式如下:
~~~java
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.itheima.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
}
通知顺序
当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。
那么这多个通知方法到底哪个先运行,哪个后运行? 下面通过程序来验证(定义两种类型的通知进行测试,一种是前置通知@Before,一种是后置通知@After)
定义多个切面类:
重新启动SpringBoot服务,测试通知的执行顺序:
使用postman测试查询所有部门数据
查看idea中控制台日志输出
通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
如果我们想控制通知的执行顺序有两种方式:
1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)
2. 使用Spring提供的@Order注解
使用@Order注解,控制通知的执行顺序:
~~~java
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
~~~
~~~java
@Slf4j
@Component
@Aspect
@Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect3 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
~~~
~~~java
@Slf4j
@Component
@Aspect
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect4 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect4 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect4 -> after ...");
}
}
~~~
重新启动SpringBoot服务,测试通知执行顺序:
通知的执行顺序大家主要知道两点即可:
1. 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
2. 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序