SpringAOP、连接点、通知类型、通知顺序、切入点表达式
一. AOP入门
1. AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),可简单的理解为面向特定方法编程
2. 场景:部分业务方法运行较慢,定位执行耗时较长的接口,此时需要统计每一个业务的执行耗时,传统模式:在每一个业务方法中都需要计算耗时,AOP模式:可以统一管理计算(如记录系统的操作日志、事务管理、权限控制等等);
3. 优势
(1) 减少重复代码
(2) 代码无侵入
(3) 提高开发效率
(4) 维护方便
4. AOP是一种思想,而在Spring框架中对这种思想进行的实现就是Spring AOP
二. 入门程序
例如统计所有业务层方法的执行耗时
1. 导入依赖:在pom.xml文件中引入AOP的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>2. 编写AOP程序:针对于特定的方法根据业务需要进行编程
/*
* 入门程序 如统计所有业务层方法的执行耗时
* */
@Slf4j
@Aspect // 标识当前是一个AOP类/切面类
@Component
public class RecordTimeAspect {@Around("execution(* com.wyyzs.service.impl.*.*(..))") // com.wyyzs.service.impl 针对包下所有类所有方法生效public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {// 1. 记录方法运行的开始时间Long startTime = System.currentTimeMillis();// 2. 执行原始方法Object result = joinPoint.proceed();// 3. 记录方法运行的结束时间 记录耗时Long endTime = System.currentTimeMillis();log.info(joinPoint.getSignature() + "方法执行耗时"+(endTime-startTime));return result;}
}
3. 执行业务层方法结果


三. AOP核心概念
1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
2. 通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
3. 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用( 切入点一定是连接点,连接点不一定是切入点)
4. 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
5. 目标对象:Target,通知所应用的对象


5. AOP执行流程
四. 通知类型
1. 根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
(1) @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(需要自己调用ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行,环绕方法有返回值,必须指定为Object,来接受原始方法的返回值 )
(2) @Before:前置通知,此注解标注的通知方法在目标方法前被执行
(3) @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
(4) @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
(5) @AfterThrowing:异常后通知,此注解标注的方法发生异常后执行
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 AspectTest1 {/** 前置通知,目标方法运行之前运行* */@Before("execution(* com.wyyzs.service.impl.*.*(..))")public void before(){log.info("before 前置通知执行了");}/** 环绕通知 - 目标方法执行前、后运行* */@Around("execution(* com.wyyzs.service.impl.*.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("around 环绕通知 - 目标方法执行前执行了");// 1. 记录方法运行的开始时间Long startTime = System.currentTimeMillis();// 2. 执行原始方法Object result = joinPoint.proceed();// 3. 记录方法运行的结束时间 记录耗时Long endTime = System.currentTimeMillis();log.info(joinPoint.getSignature() + "方法执行耗时"+(endTime-startTime));log.info("around环绕通知 - 目标方法执行后执行了");return result;}/** 后置通知 无论是否有异常都会执行* */@After("execution(* com.wyyzs.service.impl.*.*(..))")public void after(){log.info("after 后置通知执行了");}/** 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行* */@AfterReturning("execution(* com.wyyzs.service.impl.*.*(..))")public void afterReturning(){log.info("AfterReturning返回后通知执行了");}/** 异常后通知,此注解标注的方法发生异常后执行* */@AfterThrowing("execution(* com.wyyzs.service.impl.*.*(..))")public void afterThrowing(){log.info("afterThrowing异常后通知执行了");}}

2. @PointCut
(1)该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可
(2)private 修饰:仅能在当前切面类中引用该表达式;public修饰:在其他外部的切面类中也可以引用该表达式
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 AspectTest1 {@Pointcut("execution(* com.wyyzs.service.impl.*.*(..))")private void pc(){}/** 前置通知,目标方法运行之前运行* */@Before("pc()")public void before(){log.info("before 前置通知执行了");}/** 环绕通知 - 目标方法执行前、后运行* */@Around("pc()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {log.info("around 环绕通知 - 目标方法执行前执行了");// 1. 记录方法运行的开始时间Long startTime = System.currentTimeMillis();// 2. 执行原始方法Object result = joinPoint.proceed();// 3. 记录方法运行的结束时间 记录耗时Long endTime = System.currentTimeMillis();log.info(joinPoint.getSignature() + "方法执行耗时"+(endTime-startTime));log.info("around环绕通知 - 目标方法执行后执行了");return result;}/** 后置通知 无论是否有异常都会执行* */@After("pc()")public void after(){log.info("after 后置通知执行了");}/** 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行* */@AfterReturning("pc()")public void afterReturning(){log.info("AfterReturning返回后通知执行了");}/** 异常后通知,此注解标注的方法发生异常后执行* */@AfterThrowing("pc()")public void afterThrowing(){log.info("afterThrowing异常后通知执行了");}}
五. 通知顺序
1. 当有多个切面的切入点都匹配到了目标方法,在目标方法运行时多个通知方法都会被执行
2. 执行顺序:不同切面类中,默认按照切面类的类字母排序
(1) 目标方法前的通知方法:字母排名靠前的先执行
(2) 目标方法后的通知方法:字母排名靠前的后执行
@Slf4j
@Aspect
@Component
public class AspectTest2 {@Pointcut("execution(* com.wyyzs.service.impl.*.*(..))")private void pc(){}/** 前置通知,目标方法运行之前运行* */@Before("pc()")public void before(){log.info("AspectTest2 前置通知执行了");}/** 后置通知 无论是否有异常都会执行* */@After("pc()")public void after(){log.info("AspectTest2 后置通知执行了");
}同理 AspectTest3 AspectTest4

3. 用@Order(数字)加在切面类上来控制顺序
(1) 目标方法前的通知方法:数字小的先执行
(2) 目标方法后的通知方法:数字小的后执行




六. 切入点表达式
介绍:描述切入点方法的一种表达式
作用:用来决定项目中哪些方法需要加入通知
常见形式:execution(.....) :根据方法的签名来匹配;@annotation(....):根据注解匹配
1. execution
(1) execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
① ? 表示可以省略的部分
② 访问修饰符:可省略(如 public、protected)
③ 包名.类名:可省略(不建议省略,如果省略则全项目扫描方法)
④ throws 异常:可省略(方法上声明抛出的异常,不是实际抛出的异常)
(2) 可以使用通配符描述切入点
① * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 (execution(* com.*.service.*.update*(*)))
② .. :多个连续的任意符号,可以通配任意层级的包、或任意类型任意个数的参数(execution(* com.wyyzs..DeptService.*(..)))
③:根据业务需要,可以使用 且(&&)、或(||)、非(!)来组合比较复杂的切入点表达式
@Slf4j
@Aspect
@Component
public class AspectTest5 {/** 前置通知,目标方法运行之前运行* */// @Before("execution(public void com.wyyzs.service.impl.DeptServiceImpl.delete(java.lang.Integer))") // 完整等表达式//@Before("execution(void com.wyyzs.service.impl.DeptServiceImpl.delete(java.lang.Integer))") // 省略访问修饰符//@Before("execution(void delete(java.lang.Integer))") // 省略包名.类名 不建议省略//@Before("execution(* com.wyyzs.service.impl.DeptServiceImpl.delete(java.lang.Integer))") //返回值任意//@Before("execution(* com.*.service.impl.DeptServiceImpl.delete(java.lang.Integer))") //com包下的任意一级包中的.service.impl.DeptServiceImpl.delete(java.lang.Integer))方法// @Before("execution(* com.wyyzs.service.impl.*.delete(java.lang.Integer))")// com.wyyzs.service.impl 包下所有类中的 delete(java.lang.Integer)方法// @Before("execution(* com.wyyzs.service.impl.*.*(java.lang.Integer))") //com.wyyzs.service.impl 包下所有类中的 形参是Integer 所有方法//@Before("execution(* com.wyyzs.service.impl.*.*(*))") //com.wyyzs.service.impl 包下所有类中的 单个任意类型形参的 所有方法// @Before("execution(* com.wyyzs.service.impl.*.del*(*))") // com.wyyzs.service.impl 包下所有类中的 以 del开头的 单个任意类型形参的 所有方法//@Before("execution(* com.wyyzs.service.impl.*.*e(*))") // com.wyyzs.service.impl 包下所有类中的 以 e结尾 单个任意类型形参的 所有方法// @Before("execution(* com..service.impl.DeptServiceImpl.delete(java.lang.Integer))") //com包下的任意层包// @Before("execution(* com..service.impl.*.*(..))") // 任意类型 任意个数 形参方法// @Before("execution(* com.wyyzs.service.*.*(..))") // com.wyyzs.service下所有接口的所有方法// 匹配 findAll 与delete方法 可以通过逻辑运算符 || && !@Before("execution(* com.wyyzs.service.impl.DeptServiceImpl.findAll(..)) || execution(* com.wyyzs.service.impl.DeptServiceImpl.delete(..))")public void before(){log.info("AspectTest4 前置通知执行了");}}
(3) 书写建议
① 所有业务方法在命名时尽量规范,方便切入点表达式快速匹配(如findXxx、deleteXxx)
② 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
③ 在满足业务需求的前提下,尽量缩小切入点的匹配范围,如包名尽量不使用..,使用*匹配单个包
2. @annotation
(1) @annotation 切入点表达式,用于匹配标识有特定注解的方法
/*
* 定义注解
* */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTest {
}
@Slf4j
@Aspect
@Component
public class AspectTest6 {@Before("@annotation(com.wyyzs.anno.LogTest)") // 例如 使用LogTest注解的方法public void before(){log.info("AspectTest4 前置通知执行了");}}
七. 连接点JoinPoint
1. 在Spring中使用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
(1) 对于@Around通知,获取连接点信息只能使用 ProceedingJoinPoint
(2) 对于其他四种通知,获取连接点信息只能使用JoinPoint,他是ProceedingJoinPoint的父类型
@Slf4j
@Aspect
@Component
public class AspectTest7 {@Before("execution(* com.wyyzs.service.impl.DeptServiceImpl.*(..))")public void before(JoinPoint joinPoint){log.info("before 前置通知执行了");//1. 获取目标对象Object target=joinPoint.getTarget();log.info("获取目标对象{}" , target);// 2. 获取目标类String className = target.getClass().getName();log.info("获取目标类{}" , className);// 3.获取目标方法String methodName = joinPoint.getSignature().getName();log.info("获取目标方法{}" , methodName);// 4. 获取目标方法参数Object[] args = joinPoint.getArgs();log.info("获取目标方法参数{}" , Arrays.toString(args));}// @Around("execution(* com.wyyzs.service.impl.DeptServiceImpl.*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable{log.info("around before 执行了");//1. 获取目标对象Object target=joinPoint.getTarget();log.info("around获取目标对象{}" , target);// 2. 获取目标类String className = target.getClass().getName();log.info("around获取目标类{}" , className);// 获取目标方法签名Signature signature = joinPoint.getSignature();log.info("around获取目标方法签名{}" , signature);// 3.获取目标方法String methodName = joinPoint.getSignature().getName();log.info("around获取目标方法{}" , methodName);// 4. 获取目标方法参数Object[] args = joinPoint.getArgs();log.info("around获取目标方法参数{}" , Arrays.toString(args));return joinPoint.proceed();}
}


