文章目录
Spring AOP
1. 理解AOP
AOP即面向切面编程(Aspect Oriented Programming)
什么是面向切面编程?
切面指的是某一类特定的问题,所以AOP也可以理解为面向特定方法编程
比如对于网页登录,后端实现的登录拦截,就是一类特点问题
其中,登录拦截器就是对"登录校验"这类问题的统一处理
拦截器就是AOP思想的一种应用
同样的,统一数据返回格式和统一异常处理,也是AOP思想的实现
总的来说,AOP是一种思想,是对某一类问题的集中处理
举个例子:
假设这个项目的方法调用链比较复杂,现在有一些业务的执行效率很低,需要我们对耗时比较长的接口进行优化
首先就是确定出执行时间比较长的方法是哪些,就需要去统计每一个业务方法的执行耗时
可以在业务方法的执行前后记录时间,从而算出耗时
但是如果简单粗暴的去给每个方法添加计时代码,不是一件容易的工作
AOP就可以实现在不改动这些代码的基础上,针对特点的方法就进行功能的增强,即无侵入性
使用Spring AOP
我们针对图书管理系统的Controller进行耗时计算:
首先要AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写程序
@Slf4j
@Aspect
@Component
public class TimeRecordAspect {
@Around("execution(* org.jwcb.book.demo.controller.*.*(..))")
public Object timeRecord(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "耗时" + (end - start) + "ms");
return result;
}
}
执行后依次访问每个接口:
通过上面的例子,我们也能简单的感受到AOP的几个优势:
- 代码无侵入
- 减少了重复代码
- 提高了开发效率
- 维护方便
2. Spring AOP详解
2.1 Spring AOP中涉及的几个核心概念
切点(Pointcut)
Pointcut的作用就是提供一组规则,告诉程序对哪些方法来进行功能加强
这里的表达式execution(* org.jwcb.book.demo.controller.*.*(..))
就是切点表达式
连接点(Join Point)
满足上一点所说的切点表达式规则的方法,就是切点
通知(Advice)
通知就是具体要做工作,指那些重复,共性的工作(最终体现为一个方法)
切面
切面 = 切点 + 通知
通过切面就能够知道当前AOP程序需要针对哪些方法,在什么时候执行什么样的操作
切面所在的类,称为切面类(被@Aspect标注的类)
2.2 通知类型
Spring的通知类型有以下几种:
@Around
环绕通知,此注解标注的通知方法在目标方法前后都被执行@Before
前置通知,此注解标注的通知方法在目标方法前被执行@After
后置通知,此标注的通知方法在目标方法后被执行,且无论是否出现异常都会执行@AfterReturning
返回后通知,此注解标注的方法在目标方法执行后被执行,有异常不会执行@AfterThrowing
异常后通知,此注解标注的方法在发生异常后执行
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("Test1")
public String Test1(){
return "t1";
}
@RequestMapping("/Test2")
public String Test2() {
int a = 10 / 0;
return "t2";
}
}
@Slf4j
@Component
@Aspect
public class TestAspect {
@Before("execution(* org.jwcb.je0803.controller.*.*(..))")
public void doBefore() {
log.info("执⾏ Before ⽅法");
}
@After("execution(* org.jwcb.je0803.controller.*.*(..))")
public void doAfter() {
log.info("执⾏ After ⽅法");
}
@AfterReturning("execution(* org.jwcb.je0803.controller.*.*(..))")
public void doAfterReturning() {
log.info("执⾏ AfterReturning ⽅法");
}
@AfterThrowing("execution(* org.jwcb.je0803.controller.*.*(..))")
public void doAfterThrowing() {
log.info("执⾏ doAfterThrowing ⽅法");
}
@Around("execution(* org.jwcb.je0803.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around ⽅法开始执⾏");
Object result = joinPoint.proceed();
log.info("Around ⽅法结束执⾏");
return result;
}
}
正常访问的情况:
此时@AfterThrowing
标识的通知不会执行
同时,@Around
标识的通知方法包含前后两部分,前置的在@Before
标识的通知方法前执行,后置的在@After
标识的方法后执行
抛出异常情况:
此时@AfterReturning
标识的通知⽅法不会执行, @AfterThrowing
标识的通知⽅法执行了
并且@Around
环绕通知中原始方法法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了
2.2 @PointCut
Spring提供了@PointCut
注解,将公共的切点表达式提取出来,需要时引用使用即可
@Slf4j
@Component
@Aspect
public class TestAspect {
@Pointcut("execution(* org.jwcb.je0803.controller.*.*(..))")
public void pointcut() {}
@Before("pointcut()")
public void doBefore() {
log.info("执⾏ Before ⽅法");
}
@After("pointcut()")
public void doAfter() {
log.info("执⾏ After ⽅法");
}
@AfterReturning("pointcut()")
public void doAfterReturning() {
log.info("执⾏ AfterReturning ⽅法");
}
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
log.info("执⾏ doAfterThrowing ⽅法");
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around ⽅法开始执⾏");
Object result = joinPoint.proceed();
log.info("Around ⽅法结束执⾏");
return result;
}
}
2.3 切面优先级@Order
有时候我们在一个项目里面定义了多个切面类的时候,如果这些切面类的多个切点都匹配了同一个方法,那么执行的时候,顺序是什么样的??
测试一下:
新建类TestAspect1,TestAspect2,TestAspect3,内容与TestAspect上面的一样
@Slf4j
@Component
@Aspect
public class TestAspect3 {
@Pointcut("execution(* org.jwcb.je0803.controller.*.*(..))")
public void pointcut() {}
@Before("pointcut()")
public void doBefore() {
log.info("执⾏ Before ⽅法");
}
@After("pointcut()")
public void doAfter() {
log.info("执⾏ After ⽅法");
}
@AfterReturning("pointcut()")
public void doAfterReturning() {
log.info("执⾏ AfterReturning ⽅法");
}
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
log.info("执⾏ doAfterThrowing ⽅法");
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around ⽅法开始执⾏");
Object result = joinPoint.proceed();
log.info("Around ⽅法结束执⾏");
return result;
}
}
根据测试结果,可以知道,当存在多个切面类的时候,默认是按照类名字母的顺序排序
总体特点有点像栈先进后出的
但是,这种方式不太适合管理
Spring提供了一个注解@Order
,可以用来控制通知的执行顺序
使用方式:
执行结果:
可以看出,根据我们定义的优先级,数组越小的先执行
2.4 切点表达式
execution
我们上面使用的execution()是最常用的切点表达式,语法为:
*
表示匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)- 包名使用
*
表示任意包(一层包用一个*) - 类名使用
*
表示任意类 - 返回值使用
*
表示任意返回类型 - 方法名使用
*
表示任意方法 - 参数使用
*
表示一个任意类型的参数
- 包名使用
..
表示匹配多个连续的任意符号,可以调配任意层级的包,或任意类型,任意个数的参数- 使用
..
配置包名,表示次包下的所有子包 - 使用
..
配置方法参数,表示任意个任意类型的参数
- 使用
注意:访问修饰符和异常是可以省略的
@annotation
execution适用于有规则的,当时当我们要匹配多个之间无规则的方法,使用execution就不方便了
我们可以借助自定义注解的方式以及另一种切点表达式@annotation
来描述这一类的切点
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeCost {
}
@Target
表示这个注解可以用在什么地方
TYPE
用于描述类,接口(包括注解类型)或enumMETHOD
:描述方法PARAMETER
:描述参数TYPE_USE
:可以标注任意类型
@Retention
指注解被保留的时间长短,标明注解的生命周期
SOURCE
:表示注解仅存在于源代码中,编译成字节码之后会被丢弃CLASS
:编译时注解,表示注解存在于源代码和字节码中,但是运行时会被丢弃RUNTIME
:运行时注解,表示注解存在于源代码,字节码和运行时中
定义切面类
使用@annotation
切点表达式切点
@Slf4j
@Aspect
@Component
public class CostTimeAspect {
@Around("@annotation(org.jwcb.je0803.aspect.TimeCost)")
public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature().toLongString()+"cost time: {} ms", end - start);
return result;
}
}
这样的话,如果我们需要计算某一个方法的执行耗时,只需要将自定义的注解加在方法上面即可
@RequestMapping("/test")
@RestController
public class TestController {
@TimeCost
@RequestMapping("Test1")
public String Test1(){
return "t1";
}
}
访问: