AOP即 Aspect Oriented Programming 面向切面编程。
切面指的是某一类特定的问题,面向切面编程即解决某一类问题,例如前面我们介绍的拦截器,统一数据返回,统一异常处理。
AOP 是一种思想,它的实现方法有很多,如:Spring AOP,AspectJ,CGLIB 等.
1. Spring AOP 快速入门
1.1 引入依赖
要使用Spring AOP 需要先引入对应的依赖,在 pom.xml 文件中添加配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1.2 编写AOP程序
场景需求:现有一项目执行速度过慢,需要找到速度慢的原因,那么就需要统计每个方法的执行时间来分析。
package com.example.book.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.example.book.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//方法执行前时间戳
long begin = System.currentTimeMillis();
//执行方法
Object result = joinPoint.proceed();
//方法执行后时间戳
long end = System.currentTimeMillis();
//方法执行耗时
log.info("执行时间:{}", end - begin + " ms");
return result;
}
}
解释:
- @Aspect:表示这是一个切面类
- @Around:环绕通知,在目标方法的前后都会执行,后面的表示生效的路径
- ProceedingJoinPoint.proceed() 让原始方法执行
2. Spring AOP 核心概念
2.1 切点(Pointcut)
切点(Pointcut)也称为“切入点”
Pointcut 的作用是提供一组规则,告诉程序对哪些方法生效,即@Around方法中的值,称为切点表达式:
2.2 连接点(Join Point)
满足切点表达式规则的方法,就叫连接点
2.3 通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法),比如上面程序中记录消耗时间就是通知:
2.4 切面(Aspect)
切面就是 切点 + 通知,即整个方法
切面所在的类称为切面类
3. 通知类型
Spring 中的 AOP通知类型有以下几种:
- @Around:环绕通知,此注解标注的通知方法在目标方法前后都执行
- @Before:前置通知,此注解标注的方法在目标方法后被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
@Slf4j
@Component
@Aspect
public class AspectDemo1 {
@Before("execution(* com.example.j20240419springaop.controller.*.*(..))")
public void doBefore() {
log.info("doBefore");
}
@After("execution(* com.example.j20240419springaop.controller.*.*(..))")
public void doAfter() {
log.info("doAfter");
}
@AfterReturning("execution(* com.example.j20240419springaop.controller.*.*(..))")
public void doAfterReturning() {
log.info("doAfterReturning");
}
@AfterThrowing("execution(* com.example.j20240419springaop.controller.*.*(..))")
public void doAfterThrowing() {
log.info("doAfterThrowing");
}
@Around("execution(* com.example.j20240419springaop.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("before doAround");
Object result = joinPoint.proceed();
log.info("after doAround");
return result;
}
}
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public void test1() {
log.info("run test1");
}
@RequestMapping("/t2")
public void test2() {
int a = 10 / 0;
log.info("run test2");
}
}
运行程序访问t1:
可以看到 @Around 的优先级最高,应为没有发生异常,所以 @AfterThrowing中的代码没有执行。
访问t2:
可以看到执行了 @AfterThrowing中的代码,@AfterReturning中的代码 和 @Around 中的方法后执行的代码没有执行。
4. @PointCut
在我们上面的代码中,同一个切点表达式被多次引用,显然让我们的代码冗余度过高,我们可以通过Spring提供的 @PointCut注解把切点表达式提取出来方便后续使用:
@Slf4j
@Component
@Aspect
public class AspectDemo1 {
//定义切点
@Pointcut("execution(* com.example.j20240419springaop.controller.*.*(..))")
public void pt() {};
@Before("pt()")
public void doBefore() {
log.info("doBefore");
}
@After("pt()")
public void doAfter() {
log.info("doAfter");
}
@AfterReturning("pt()")
public void doAfterReturning() {
log.info("doAfterReturning");
}
@AfterThrowing("pt()")
public void doAfterThrowing() {
log.info("doAfterThrowing");
}
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("before doAround");
Object result = joinPoint.proceed();
log.info("after doAround");
return result;
}
}
如果定义的切点要在其他类使用,需要把权限设置为 public 并且在其他类中引用时填写全限定名称:
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doBefore() {
log.info("demo2,doBefore");
}
@After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doAfter() {
log.info("demo2,doAfter");
}
}
如果多个切面应用于同一个方法,优先级是按照名称字典序排序:
可以通过 @order 注解定义优先级,括号中的值越小优先级越大:
@Order(100)
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doBefore() {
log.info("demo2,doBefore");
}
@After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doAfter() {
log.info("demo2,doAfter");
}
}
@Order(10)
@Slf4j
@Component
@Aspect
public class AspectDemo3 {
@Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doBefore() {
log.info("demo3,doBefore");
}
@After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doAfter() {
log.info("demo3,doAfter");
}
}
@Order(1)
@Slf4j
@Component
@Aspect
public class AspectDemo4 {
@Before("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doBefore() {
log.info("demo4,doBefore");
}
@After("com.example.j20240419springaop.aspect.AspectDemo1.pt()")
public void doAfter() {
log.info("demo4,doAfter");
}
}
5. 切点表达式
5.1 execution表达式
execution()是最常用的切点表达式,用来匹配方法,语法为:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中访问修饰符和异常可省略
注意:
- * 匹配任意一个元素(返回类型,包,类名,方法,方法参数)
- .. 匹配多个连续的任意符号, 如包,类型,参数,不能匹配方法名
5.2 @annotation
我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点
实现步骤:
- 编写自定义注解
- 使用 @annotation 表达式描述切点
- 在连接点的方法上添加自定义注解
//限定注解的使用类型和生命周期
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
@Slf4j
@Component
@Aspect
public class AspectDemo5 {
@Around("@annotation(com.example.j20240419springaop.config.MyAspect)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("before ");
Object result = joinPoint.proceed();
log.info("after " );
return result;
}
}
只要添加上@MyAspect 注解的方法,就会应用到 doAround 方法。
6. Spring AOP 的实现方式(常见面试题)
- 基于注解 @Aspect
- 基于自定义注解
- 基于Spring API(通过xml配置的⽅式,⾃从SpringBoot⼴泛使⽤之后,这种⽅法⼏乎看不到了)
- 基于代理实现