Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
package com.mkyuan.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @Author sygui
* @Date 2023-07-23
**/
@Component
@Slf4j
@Aspect
public class TimeAspect {
//前置通知
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(JoinPoint joinPoint) {
log.info("Before...");
}
//环绕通知
@Around("execution(* com.mkyuan.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around before...");
//调用目标对象的原始方法执行
Object proceed = joinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("Around after..");
return proceed;
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.mkyuan.service.*.*(..))")
public void afterReturn(JoinPoint joinPoint) {
log.info("AfterReturning...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.mkyuan.service.*.*(..))")
public void afterThrow(JoinPoint joinPoint) {
log.info("AfterThrowing...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(JoinPoint joinPoint) {
log.info("After...");
}
}
1. 没有异常情况下:
程序没有发生异常的情况下,@AfterThrowing标识的通知方法不会执行。
2. 出现异常情况下:
修改EmpServiceImpl业务实现类中的代码: 添加异常
/**
* 根据id查询员工
*
* @param id
* @return
*/
@Override
public Emp selectEmp(Integer id) {
empMapper.selectEmpById(id);
int i = 1 / 0;
return empMapper.selectEmpById(id);
}
程序发生异常的情况下:
-
@AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
-
@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)
优化切面类
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
@Component
@Slf4j
@Aspect
public class TimeAspect {
@Pointcut("execution(* com.mkyuan.service.*.*(..))")
private void pt() {
}
//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint) {
log.info("Before...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around before...");
//调用目标对象的原始方法执行
Object proceed = joinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("Around after..");
return proceed;
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturn(JoinPoint joinPoint) {
log.info("AfterReturning...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrow(JoinPoint joinPoint) {
log.info("AfterThrowing...");
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint) {
log.info("After...");
}
}
需要注意的是:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:
@Component
@Slf4j
@Aspect
public class TimeAspect1 {
@Before("com.mkyuan.aop.TimeAspect.pt()")
public void before() {
log.info("TimeAspect -> before ...");
}
}
@annotation
已经学习了execution切入点表达式的语法。那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。而在之前我们是将两个切入点表达式组合在了一起完成的需求,这个是比较繁琐的。
我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。
实现步骤:
-
编写自定义注解
-
在业务类要做为连接点的方法上添加自定义注解
自定义注解:MyLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
业务类:DeptServiceImpl
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
}
@Override
public Dept getById(Integer id) {
return deptMapper.getById(id);
}
@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
}
}
切面类
@Component
@Slf4j
@Aspect
public class MyAspect {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.mkyuan.anno.MyLog)")
public void before(){
log.info("MyAspect -> before ...");
}
//后置通知
@After("@annotation(com.mkyuan.anno.MyLog)")
public void after(){
log.info("MyAspect -> after ...");
}
}
到此我们两种常见的切入点表达式我已经介绍完了。
- execution切入点表达式
- 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
- annotation 切入点表达式
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了
AOP通知的执行顺序
当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。
此时我们就有一个疑问,这多个通知方法到底哪个先运行,哪个后运行? 下面我们通过程序来验证(这里呢,我们就定义两种类型的通知进行测试,一种是前置通知@Before,一种是后置通知@After)
定义多个切面类:
MyAspect1
@Component
@Slf4j
@Aspect
public class MyAspect1 {
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(){
log.info("MyAspect1 -> before ...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(){
log.info("MyAspect1 -> after ...");
}
}
MyAspect2
@Component
@Slf4j
@Aspect
public class MyAspect2 {
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
MyAspect3
@Component
@Slf4j
@Aspect
public class MyAspect3 {
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行
-
目标方法后的通知方法:字母排名靠前的后执行
如果我们想控制通知的执行顺序有两种方式:
-
修改切面类的类名(这种方式非常繁琐、而且不便管理)
-
使用Spring提供的@Order注解
使用@Order注解,控制通知的执行顺序:
MyAspect1
@Component
@Slf4j
@Aspect
@Order(3)
public class MyAspect1 {
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(){
log.info("MyAspect1 -> before ...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(){
log.info("MyAspect1 -> after ...");
}
}
MyAspect2
@Component
@Slf4j
@Aspect
@Order(2)
public class MyAspect2 {
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
MyAspect3
@Component
@Slf4j
@Aspect
@Order(1)
public class MyAspect3 {
@Before("execution(* com.mkyuan.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
}
//后置通知
@After("execution(* com.mkyuan.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
通知的执行顺序大家主要知道两点即可:
- 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
- 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序