文章目录
- 一、什么是AOP
- [二、Spring AOP快速入门](#二、Spring AOP快速入门)
- 三、SpringAOP详解
-
- [3.1 四个核心概念](#3.1 四个核心概念)
- [3.2 五大通知类型](#3.2 五大通知类型)
- [3.3 多切面执行顺序 @Order](#3.3 多切面执行顺序 @Order)
- [3.4 切点表达式详解](#3.4 切点表达式详解)
- 四、SpringAOP底层原理
-
- [4.1 代理模式](#4.1 代理模式)
- [4.2 静态代理](#4.2 静态代理)
- [4.3 动态代理:运行时生成代理class](#4.3 动态代理:运行时生成代理class)
-
- [1. JDK动态代理](#1. JDK动态代理)
- [2. CGLIB动态代理](#2. CGLIB动态代理)
- 区别
- [4.4 Spring AOP怎么选择代理方式](#4.4 Spring AOP怎么选择代理方式)
IoC与AOP是 Spring 两大核心。AOP是编程思想,Spring AOP是这套思想的落地实现,能够在 不修改原有业务代码 的前提下对方法做功能增强,非常适合做日志统计、耗时监控、权限校验、异常处理等通用功能。
一、什么是AOP
AOP(Aspect Oriented Programming)面向切面编程,对一类通用问题做集中处理。AOP思想:无侵入增强;不改动业务源代码,就给方法追加功能。
日常开发中AOP思想的体现:
- 拦截器:做登录校验,针对请求URL拦截
@ControllerAdvice:全局异常处理、数据预处理- Spring AOP:粒度更细,可以按照包、类、方法名、方法注解、参数拦截方法。
拦截器与Spring AOP对比:拦截器作用于URL请求;Spring AOP作用于Java方法,颗粒更细
举个例子:统计项目各个业务方法的执行耗时。
如果不用AOP,要在每一个方法开头、结尾手写获取时间戳,业务代码到处充斥重复统计代码,维护麻烦。
AOP可以抽取通用逻辑,统一增强目标方法。
AOP只是一种思想,由Spring AOP、AspectJ、CGLIB等方法实现
二、Spring AOP快速入门
1、引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<!-- Springboot4将aop改名未aspectj,springboot3还是spring-boot-starter-aop-->
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
2、编写切面类
需求:统计controller包下所有方法的执行耗时
java
@Aspect
@Slf4j
@Component
public class TimeAspect {
//环绕通知,切点表达式匹配controller下所有类所有任意参数方法
@Around("execution( * com.daisy.springbookdemo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录开始时间
long startTime = System.currentTimeMillis();
//执行原始业务方法
Object proceed = pjp.proceed();
//记录结束时间
long endTime = System.currentTimeMillis();
//打印方法签名与耗时
log.info("{} 执行耗时:{} ms",pjp.getSignature(), endTime-startTime);
return proceed;
}
}
运行项目,访问controller接口,控制台就会输出每个方法的执行耗时。
AOP带来的优势:
- 代码无侵入:完全不用改动业务方法
- 消除重复样板代码
- 开发效率高,后续维护只需要修改切面类
三、SpringAOP详解
3.1 四个核心概念
-
切点 Pointcut:筛选规则
通过 execution / @annotation 写表达式,筛选一批符合条件的方法 ,用来匹配哪些方法需要增强
execution(* com.xxx.controller.*.*(..))就是筛选 controller 包下所有方法。 -
连接点 JoinPoint:可以被拦截的点,满足切点表达式的那些方法
SpringAOP 中只有方法执行是连接点,例如
UserController.query()这个方法。 -
通知 Advice:要执行的增强逻辑(一段 Java 方法)
@Before/@Around/@After修饰的方法,就是通知方法。 -
切面 Aspect = 切点 + 通知
切面类里面:定义筛选哪些方法(切点) + 对这些方法做什么增强(通知)。
以上面的执行耗时为例子:

3.2 五大通知类型
SpringAOP一共5种通知,控制增强逻辑在目标方法什么时机执行。
| 注解 | 通知类型 | 执行时机 |
|---|---|---|
@Around |
环绕通知 | 目标方法前后都执行;可以控制原始方法是否执行;最强大 |
@Before |
前置通知 | 目标方法执行之前执行 |
@After |
后置通知 | 目标方法执行之后执行,无论是否抛异常一定会执行 |
@AfterReturning |
返回通知 | 目标方法正常返回后执行;出现异常不会执行 |
@AfterThrowing |
异常通知 | 只有目标方法抛出异常的时候才执行 |
代码示例
java
@Aspect
@Component
@Slf4j
public class AspectDemo1 {
//环绕通知,切点表达式匹配controller下所有类所有任意参数方法
@Around("execution( * com.daisy.aop.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录开始时间
long startTime = System.currentTimeMillis();
log.info("目标方法执行前");
//执行原始业务方法
Object proceed = pjp.proceed();
//记录结束时间
long endTime = System.currentTimeMillis();
log.info("目标方法执行后");
log.info("{} 执行耗时:{} ms", pjp.getSignature(), endTime - startTime);
return proceed;
}
@Before("execution( * com.daisy.aop.controller.*.*(..))")
public void doBefore() {
log.info("doBefore");
}
@After("execution( * com.daisy.aop.controller.*.*(..))")
public void doAfter() {
log.info("doAfter");
}
@AfterReturning("execution( * com.daisy.aop.controller.*.*(..))")
public void doAfterReturning() {
log.info("doAfterReturning");
}
@AfterThrowing("execution( * com.daisy.aop.controller.*.*(..))")
public void doAfterThrowing() {
log.info("doAfterThrowing");
}
}
执行顺序
- 目标方法正常执行无异常

- 目标方法抛出异常

Around的proceed抛出异常,Around后置代码不会执行;AfterReturning不会执行

注意:
@Around环绕通知,必须调用pjp.proceed(),否则原始业务方法不会执行;- 环绕通知返回值必须写
Object,接收原始方法返回结果; - 除了
@Around,其他通知不用手动调用原始方法。 - @Around可以替换其余四个通知类型:
- 目标方法执行前:相当于@Before
- try内部,目标方法执行后:相当于@AfterReturning,没有异常抛出才执行
- catch内部:相当于@AfterThrowing,有异常抛出时执行
- finally内部:相当于@After,无论是否抛出异常都会执行
- finally之后的部分:相当于@AfterReturning,没有异常抛出才执行
java
@Around("execution( * com.daisy.aop.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
log.info("目标方法执行前");
Object result = null;
//执行原始业务方法
try{
result = pjp.proceed();
}catch (Throwable e){
log.error("do around throwing");
}finally{
log.info("收尾工作")
}
log.info("目标方法执行后");
return result;
}
@Pointcut:复用切点表达式
把重复的切点抽取到一个无实现的方法上。
java
@Pointcut("execution( * com.daisy.aop.controller.*.*(..))")
public void pt(){}
@Around("pt()")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//...
}
pt()方法的访问修饰符决定它的作用域,如果设置为private就只能在当前类中使用;
如果设置为public,在AspectDemo2中使用AspectDemo1的pt()方法:
@Around("com.daisy.aop.aspect.AspectDemo1.pt()")
3.3 多切面执行顺序 @Order
当多个切面同时拦截同一个目标方法,就会涉及执行优先级。

默认情况按照切面类的名称排序,但是实际开发中不可能根据顺序改动类名,因此引入了@Order(数字)手动控制优先级
- @Before前置通知:数字越小,越先执行
- @After后置通知:数字越大,越后执行
切面就像一层层同心圆,Order值越小越靠外层,先进后出。

3.4 切点表达式详解
两种最常用写法:execution() 和 @annotation()
1、execution():按方法签名匹配
语法:
execution(<访问修饰符> <返回类型> <包.类.方法(参数)>)
访问修饰符、异常可以省略。
通配符:
*:匹配单个任意元素(返回值、包一层、类名、方法名、单个参数)..:匹配多层;包中代表当前包以及所有子包;参数位置代表任意数量任意类型参数

示例:
java
//controller包下所有类所有任意参数方法
execution(* com.example.demo.controller.*.*(..))
//demo包以及所有子孙包全部方法
execution(* com.example.demo..*(..))
//TestController类下全部无参方法
execution(* com.example.demo.controller.TestController.*())
2、@annotation():自定义注解匹配切点
适合场景:方法分散在不同包不同类,不方便通过包路径匹配,给需要增强的方法打上自定义注解,切面拦截该注解。
实现步骤:
- 自定义运行时注解
java
import java.lang.annotation.*;
@Target(ElementType.METHOD) //只能标注方法
@Retention(RetentionPolicy.RUNTIME) //运行时保留,可以反射读取
public @interface MyAspect {
}
- 切面:使用
@annotation切点
java
@Slf4j
@Aspect
@Component
public class MyAspectDemo {
//拦截标记了@MyAspect注解的所有方法
@Before("@annotation(com.example.demo.aspect.MyAspect)")
public void before(){
log.info("自定义注解切面执行");
}
}
- 在需要增强的业务方法上加注解
java
@MyAspect
@RequestMapping("/t1")
public String t1(){
return "t1";
}
不一定要使用自定义注解,也可以拦截spring的注解,例如@annotation(org.springframework.web.bind.annotation.RequestMapping)可以拦截所有加了RequestMapping注解的方法
四、SpringAOP底层原理
Spring AOP底层基于动态代理实现,分为 JDK动态代理、CGLIB动态代理。
4.1 代理模式
代理模式也称为委托模式,是指为其他对象提供一种代理,以控制这个对象的访问。它的作用就是提供一个代理类,让我们调用目标方法的时候不直接调用目标方法,而是通过代理类间接调用
某些情况下,一个对象不适合或者不能直接引用另一个对象,代理对象就可以在客户端与目标对象之间起到中介的作用
代理模式核心:不修改目标对象,通过代理对象间接调用目标方法,在代理中增加增强逻辑。
使用代理前:

使用代理后:

类比:房屋中介、经销商、助理/秘书都可以认为是代理对象
代理模式中的主要角色
- Subject:业务接口类,可以是抽象类或者接口,非必要存在
- RealSubject:业务实现类,具体的业务执行,也就是被代理对象
- Proxy:代理类,RealSubject的代理
4.2 静态代理
编译期就已经生成代理class文件。
缺点:每一个目标类都要写一套代理类,接口改动代理类也要同步修改,代码冗余。
4.3 动态代理:运行时生成代理class
1. JDK动态代理
前提:目标类必须实现接口,基于接口生成代理类
- 定义业务接口,编写目标实现类(也就是被代理对象,实现该接口)。
- 自定义类实现
InvocationHandler接口,重写invoke()方法;在invoke中编写增强逻辑 ,通过method.invoke(target,args)反射调用目标对象原始方法。 - 调用
Proxy.newProxyInstance(类加载器, 目标实现的接口数组, InvocationHandler实例),运行时动态生成实现了目标接口的代理 Class 字节码,并创建代理对象。 - 调用代理对象的方法,方法请求会转发到
InvocationHandler的invoke方法;先执行自定义增强逻辑,再执行目标方法,之后执行后置增强,返回结果。
- 核心API:
InvocationHandler、Proxy.newProxyInstance() - 缺点:没有实现接口的普通类,JDK代理无法使用。
2. CGLIB动态代理
前提:目标类不需要实现接口,通过继承目标类生成子类作为代理类
- 编写普通目标类,不需要实现接口。
- 自定义类实现
MethodInterceptor接口,重写intercept()方法;在 intercept 中编写增强逻辑,通过methodProxy.invoke(target,objects)调用原始目标方法。 - 使用
Enhancer工具,设置被代理的目标类 Class、注册MethodInterceptor回调;运行时动态生成目标类的子类字节码,创建代理对象。 - 调用代理对象方法,请求进入
intercept;执行自定义增强逻辑,调用父类(原目标)方法,执行后置增强,返回结果。
- 核心API:
MethodInterceptor、Enhancer - 注意:被final修饰的类无法被继承,CGLIB不能代理final类。
区别
- JDK 代理:依赖接口;
InvocationHandler#invoke;Proxy生成代理。 - CGLIB 代理:继承目标类,既可以代理类也可以代理接口;
MethodInterceptor#intercept;Enhancer生成代理。
4.4 Spring AOP怎么选择代理方式
配置属性 proxyTargetClass
| proxyTargetClass | 目标对象类型 | 代理方式 |
|---|---|---|
| false | 实现接口 | JDK动态代理 |
| false | 普通类无接口 | CGLIB |
| true | 无论是否接口 | CGLIB |
Spring AOP 选择规则:目标实现接口默认 JDK;无接口则 CGLIB;设置proxyTargetClass=true强制使用 CGLIB。
大多数情况SpringBoot和Spring保持一致,但是这里存在差异:
- SpringFramework中proxyTargetClass默认值为false,也就是说根据是否有实现接口来选择代理方式
- SpringBoot2.x与上面相同
- SpringBoot3.x以及之后proxyTargetClass默认值为true,也就是说默认使用CGLIB代理。
可以在配置文件修改:
properties
# 使用jdk动态代理
spring.aop.proxy-target-class=false