3. AOP进阶
AOP的基础知识学习完之后,下面我们对AOP当中的各个细节进行详细的学习。主要分为4个部分:
- 通知类型
- 通知顺序
- 切入点表达式
- 连接点
我们先来学习第一部分通知类型。
3.1 通知类型
在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。
@Around("execution(* com.itheima.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { long begin = System.currentTimeMillis(); Object result = pjp.proceed(); long end = System.currentTimeMillis(); log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin); return result; }
|
只要我们在通知方法上加上了@Around注解,就代表当前通知是一个环绕通知。
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
下面我们通过代码演示,来加深对于不同通知类型的理解:
@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. 没有异常情况下:
程序没有发生异常的情况下,@AfterThrowing标识的通知方法不会执行。
2. 出现异常情况下:
修改DeptServiceImpl业务实现类中的代码: 添加异常
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper;
@Override public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
int num = 10/0;
return deptList; } }
|
重新启动SpringBoot服务,测试发生异常情况下通知的执行:
程序发生异常的情况下:
在使用通知时的注意事项:
- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
五种常见的通知类型,我们已经测试完毕了,此时我们再来看一下刚才所编写的代码,有什么问题吗?
@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注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
@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,而在引用的时候,具体的语法为:
全类名.方法名(),具体形式如下:
@Slf4j @Component @Aspect public class MyAspect2 { @Before("com.itheima.aspect.MyAspect1.pt()") public void before(){ log.info("MyAspect2 -> before ..."); } }
|
3.2 通知顺序
讲解完了Spring中AOP所支持的5种通知类型之后,接下来我们再来研究通知的执行顺序。
当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。
此时我们就有一个疑问,这多个通知方法到底哪个先运行,哪个后运行? 下面我们通过程序来验证(这里呢,我们就定义两种类型的通知进行测试,一种是前置通知@Before,一种是后置通知@After)
定义多个切面类:
@Slf4j @Component @Aspect 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 ..."); } }
|
@Slf4j @Component @Aspect 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 ..."); } }
|
@Slf4j @Component @Aspect 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服务,测试通知的执行顺序:
备注:
把DeptServiceImpl实现类中模拟异常的代码删除或注释掉。
注释掉其他切面类(把@Aspect注释即可),仅保留MyAspect2、MyAspect3、MyAspect4 ,这样就可以清晰看到执行的结果,而不被其他切面类干扰。
通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
如果我们想控制通知的执行顺序有两种方式:
- 修改切面类的类名(这种方式非常繁琐、而且不便管理)
- 使用Spring提供的@Order注解
使用@Order注解,控制通知的执行顺序:
@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 ..."); } }
|
@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 ..."); } }
|
@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服务,测试通知执行顺序:
通知的执行顺序大家主要知道两点即可:
- 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
- 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序
package com.playing.aop;
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component;
@Slf4j @Aspect @Component public class AspectClass { @Pointcut("execution(* com.playing.service.Imp.UserServiceImp.*(..))") public void pt(){}
@Before("pt()") public void before(){ 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(){ log.info("after ..."); }
@AfterReturning("pt()") public void afterReturning(){ log.info("afterReturning ..."); }
@AfterThrowing("pt()") public void afterThrowing(){ log.info("afterThrowing ..."); } }
|