Spring Boot AOP 实战
前言
面向切面编程(AOP, Aspect-Oriented Programming)是 Spring 框架的核心特性之一。它通过预编译方式和运行期动态代理实现程序功能的统一维护。在 Spring Boot 生态中,AOP 主要用于处理日志记录、性能统计、安全控制、事务处理、异常处理等横切关注点(Cross-cutting Concerns)。
本文将从 声明式(基于 @Aspect 注解) 和 编程式(基于 Advisor 底层 API) 两个维度,总结 Spring Boot 中 AOP 的实现方式及其适用场景。
一、 基于注解的用法(声明式 AOP)
这是 Spring Boot 业务开发中最常见、最高效的使用方式。它利用 AspectJ 的注解语法(如 @Aspect, @Pointcut, @Around)来定义切面,Spring 容器在启动时会自动解析这些注解并生成代理对象。
1. 核心组件
@Aspect:标记一个类为切面类。@Pointcut:定义切点表达式,确定哪些类的哪些方法需要被拦截。- Advice 注解:定义增强逻辑的执行时机(
@Before,@After,@Around,@AfterReturning,@AfterThrowing)。
2. 实现步骤
2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 定义切面类
@Aspect
@Component
@Slf4j
public class LoggingAspect {
// 定义切点:匹配 com.example.service 包下所有类的所有 public 方法
@Pointcut("execution(public * com.example.service.*.*(..))")
public void serviceLayer() {}
// 定义切点:匹配带有 @MyLog 注解的方法
@Pointcut("@annotation(com.example.annotation.MyLog)")
public void annotationLayer() {}
// 环绕通知:最强大的通知类型,控制目标方法的执行
@Around("serviceLayer() || annotationLayer()")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
log.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
return result;
}
}
3. 技术特性
- 实现机制:Spring AOP 默认使用 CGLIB(针对类)或 JDK 动态代理(针对接口)生成代理对象。
- 自动装配:
AopAutoConfiguration会自动查找容器中标注了@Aspect的 Bean,并将其转换为底层的Advisor列表。 - 适用场景:绝大多数标准业务场景,如统一日志、鉴权、全局异常处理等,逻辑相对静态,切点规则在编译期即可确定。
二、 非基于注解的用法(编程式 AOP / 底层 API)
在中间件开发、框架集成或动态规则场景下,@Aspect 的方式可能显得不够灵活。此时,我们需要直接操作 Spring AOP 的底层接口:Advisor、Pointcut 和 Advice。
这种方式的核心在于:手动构建 Advisor 并注入 Spring 容器。Spring 的 AnnotationAwareAspectJAutoProxyCreator 会自动扫描容器中的所有 Advisor Bean,并将其应用到匹配的 Bean 上。
1. 核心组件
Advice(通知):具体要执行的拦截逻辑,通常实现MethodInterceptor接口。Pointcut(切点):定义类和方法的匹配规则,常用实现如AspectJExpressionPointcut或AnnotationMatchingPointcut。Advisor(顾问):Spring AOP 的基本单元,封装了Pointcut和Advice。最常用实现是DefaultPointcutAdvisor。
2. 实现步骤
2.1 定义拦截逻辑 (MethodInterceptor)
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class PerformanceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Method " + invocation.getMethod().getName() + " starts.");
Object result = invocation.proceed();
System.out.println("Method finishes.");
return result;
}
}
2.2 定义并注册 Advisor (@Configuration)
我们不再使用 @Aspect,而是直接通过 Java Config 组装 Bean。
@Configuration
public class AopConfiguration {
@Bean
public DefaultPointcutAdvisor performanceAdvisor() {
// 1. 定义切点 (Pointcut)
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 依然可以使用 AspectJ 表达式,或者使用其他编程式 Pointcut 实现
pointcut.setExpression("execution(* com.example.service.*.*(..))");
// 2. 定义通知 (Advice)
MethodInterceptor advice = new PerformanceInterceptor();
// 3. 组装 Advisor
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);
// 设置优先级
advisor.setOrder(1);
return advisor;
}
}
3. 进阶:完全自定义 Pointcut
除了使用字符串表达式,编程式 AOP 允许使用强类型的 Pointcut,例如 AnnotationMatchingPointcut(这也是很多开源框架常用的方式)。
@Bean
public DefaultPointcutAdvisor annotationAdvisor() {
// 匹配类上或方法上带有 @MyLog 注解的切点
Pointcut pointcut = new AnnotationMatchingPointcut(null, MyLog.class);
return new DefaultPointcutAdvisor(pointcut, new PerformanceInterceptor());
}
4. 技术特性与适用场景
- 高度定制:可以在运行时动态构建切点规则(例如从数据库读取规则并生成 Pointcut),而不受限于硬编码的注解。
- 无侵入性:不需要在业务代码上加注解,也不需要扫描特定的包路径,适合开发通用的 SDK 或 Starter。
- 底层暴露:这是 Spring 事务 (
@Transactional)、缓存 (@Cacheable) 等功能的底层实现方式。
三、 原理揭秘:注解 (@Aspect) 与底层 (Advisor) 的等效性
在理解了“声明式”和“编程式”两种用法后,我们需要深入 Spring AOP 的底层运行机制。事实上,@Aspect 并非独立于 Advisor 之外的另一套体系,它仅仅是 Spring 提供的一种高级抽象和配置方式。
1. 底层转换机制
在 Spring 容器启动过程中,核心组件 AnnotationAwareAspectJAutoProxyCreator(一种 BeanPostProcessor)承担了关键职责。它的工作流程如下:
- 扫描:它会在容器中寻找所有标注了
@Aspect注解的 Bean。 - 解析:解析该类中所有的通知注解(
@Before,@After,@Around等)。 - 提取与转换:对于每一个通知注解,Spring 都会将其提取出来,并结合
@Pointcut定义的表达式,实例化为一个独立的Advisor对象(具体实现类通常为InstantiationModelAwarePointcutAdvisorImpl)。 - 注册:这些生成的
Advisor对象会被加入到代理工厂的拦截器链中,最终作用于目标 Bean。
结论:一个包含多个通知方法(Advice Methods)的 @Aspect 类,在运行时会被拆解为多个 Advisor 实例。Advisor 始终是 Spring AOP 运行时的原子执行单元。
2. 代码等效性演示
为了验证上述结论,我们将展示一个标准的 @Aspect 切面类,并给出其完全等效的 Advisor 编程式写法。
2.1 源头:基于注解的切面
这是一个典型的业务切面,包含一个切点、一个前置通知和一个环绕通知。
@Aspect
@Component
public class BusinessLogAspect {
// 1. 定义切点:所有 Service 层的 public 方法
@Pointcut("execution(public * com.example.service.*.*(..))")
public void serviceLayer() {}
// 2. 前置通知
@Before("serviceLayer()")
public void doBefore() {
System.out.println("[Aspect] Before method execution");
}
// 3. 环绕通知
@Around("serviceLayer()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
System.out.println("[Aspect] Execution time: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
2.2 等效:基于 Advisor 的手动实现
如果不使用 @Aspect 注解,要达到完全相同的运行时效果,我们需要手动定义 Bean 来组装 Pointcut 和 Advice。
注意:下面的配置类在 Spring 容器看来,与上面的 BusinessLogAspect 产生的效果是等价的。
@Configuration
public class AdvisorConfiguration {
// 1. 定义公共切点 (AspectJExpressionPointcut)
// 对应 @Pointcut
@Bean
public AspectJExpressionPointcut serviceLayerPointcut() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public * com.example.service.*.*(..))");
return pointcut;
}
// 2. 构建前置通知的 Advisor
// 对应 @Before
@Bean
public Advisor beforeAdvisor(AspectJExpressionPointcut pointcut) {
// 定义通知逻辑 (MethodBeforeAdvice)
MethodBeforeAdvice advice = (method, args, target) -> {
System.out.println("[Advisor] Before method execution");
};
// 组装 Pointcut 和 Advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
// 3. 构建环绕通知的 Advisor
// 对应 @Around
@Bean
public Advisor aroundAdvisor(AspectJExpressionPointcut pointcut) {
// 定义通知逻辑 (MethodInterceptor)
// MethodInterceptor 是 Spring AOP 环绕通知的底层接口
MethodInterceptor interceptor = invocation -> {
long start = System.currentTimeMillis();
// 执行目标方法
Object result = invocation.proceed();
System.out.println("[Advisor] Execution time: " + (System.currentTimeMillis() - start) + "ms");
return result;
};
// 组装 Pointcut 和 Advice
// 注意:这里复用了同一个 pointcut 对象,与 @Aspect 中引用同一个 @Pointcut 方法逻辑一致
return new DefaultPointcutAdvisor(pointcut, interceptor);
}
}
3. 对比分析
通过上述代码对比,我们可以清晰地看到映射关系:
- 切点映射:注解中的切点表达式字符串 →\rightarrow→
AspectJExpressionPointcut对象。 - 通知映射:
@Before方法体 →\rightarrow→MethodBeforeAdvice接口实现。@Around方法体 →\rightarrow→MethodInterceptor接口实现。
- 结构映射:
@Aspect类是多个逻辑的聚合体。- Spring 底层将其拆解为多个
DefaultPointcutAdvisor(或其内部实现),每个 Advisor 只负责一个具体的切面逻辑(1 Pointcut + 1 Advice)。
理解这一层转换关系,有助于开发者在遇到复杂的 AOP 失效问题或需要进行底层框架扩展时,能够跳出注解的限制,直接在 Advisor 层级进行调试和控制。
四、 架构视角:Spring AOP 与 Spring MVC Interceptor 的辩证关系
在 Spring 生态中,开发者经常会混淆 切面(AOP) 和 拦截器(Interceptor)。两者在功能上确实存在重叠,都体现了“拦截器模式”或“责任链模式”的设计思想,即在核心逻辑执行前后插入横切逻辑。
然而,在 Spring 的架构分层中,它们处于完全不同的维度。本章将从底层实现、作用范围及执行时序三个方面理清它们的关系。
1. 概念上的同源与异构
从广义的设计模式来看,AOP 的 Advice(通知)本质上也是一种拦截器。
-
同源性:
- Spring AOP 的底层实现:基于
org.aopalliance.intercept.MethodInterceptor接口。当 Spring 代理工厂生成代理对象时,会将 Advice 封装为 MethodInterceptor 组成的拦截器链。 - Spring MVC 的拦截器:基于
org.springframework.web.servlet.HandlerInterceptor接口。
- Spring AOP 的底层实现:基于
-
异构性(关键区别):
- AOP (MethodInterceptor):面向 Java 方法调用。它关注的是类的粒度,通过动态代理(CGLIB/JDK)嵌入到 Bean 的方法执行过程中。
- MVC Interceptor (HandlerInterceptor):面向 HTTP 请求。它关注的是 URL 的粒度,由
DispatcherServlet在分发请求的过程中显式回调。
2. 核心差异对比表
为了更直观地理解,我们可以通过以下技术指标进行对比:
| 维度 | Spring AOP (@Aspect) | Spring MVC Interceptor |
|---|---|---|
| 所属层级 | 业务逻辑层 (Service/Component) | Web 接入层 (Controller) |
| 拦截粒度 | 方法级 (Method) | 请求级 (Request/URL) |
| 核心机制 | 动态代理 (Dynamic Proxy) | 反射回调 (Reflection Callback) |
| 管理容器 | Spring IOC 容器 | DispatcherServlet (Web 上下文) |
| 参数获取 | JoinPoint (参数对象、方法名、返回值) |
HttpServletRequest, HttpServletResponse |
| 适用场景 | 事务、缓存、业务日志、细粒度权限 | 登录校验、跨域设置、通用 HTTP 头处理 |
| 生效条件 | 必须是 Spring 管理的 Bean | 请求必须经过 DispatcherServlet |
3. 执行顺序:洋葱模型
在一个标准的 Spring Boot Web 请求中,Filter、Interceptor 和 AOP 的执行顺序呈现出一种嵌套的“洋葱模型”。
假设一个 HTTP 请求到达 Controller 的方法,AOP套在Controller方法上,其调用栈如下:
- Servlet 容器 (Tomcat) 接收请求。
- Filter (过滤器):
doFilter(进入) - DispatcherServlet:开始分发。
- HandlerInterceptor (拦截器):
preHandle(进入)- 此时请求尚未到达具体的 Controller 方法对象。
- Spring AOP (切面):
@Around/@Before(进入)- 此时进入了 Bean 的代理对象内部。
- Controller Method (目标方法):执行核心业务逻辑。
- Spring AOP (切面):
@AfterReturning/@After(退出) - HandlerInterceptor (拦截器):
postHandle/afterCompletion(退出) - Filter (过滤器):
doFilter(退出)
五、 Spring AOP 的“自调用”隐形陷阱
有许多注解都是基于AOP开发的,比如 @Transactional,@Cacheable。在 Spring 开发中,我们经常遇到一种“灵异现象”:明明在方法上加了 @Transactional 做事务控制,或者加了 @Cacheable 做缓存,但在同一个类中调用该方法时,这些注解却统统失效了,数据库依然被频繁查询,事务回滚也没有发生。
这并非 Spring 的 Bug,而是经典的 “自调用(Self-Invocation)”陷阱。
1. 现象还原:失效的代码
假设我们有一个 ClientService,其中 getClientById 方法开启了缓存,而 validateClient 需要在内部调用它:
@Service
public class ClientService {
// 这是一个内部调用入口
public void validateClient(String id) {
// 【陷阱发生处】直接调用内部方法
// 相当于 this.getClientById(id);
OAuth2ClientDO client = getClientById(id);
if (client == null) {
throw new RuntimeException("Client not found");
}
}
// 这个方法配置了缓存
@Cacheable(value = "clients", key = "#id")
public OAuth2ClientDO getClientById(String id) {
System.out.println("--- 查询数据库 ---"); // 用于测试是否走了缓存
return clientMapper.selectById(id);
}
}
运行结果:当你调用 validateClient 时,你会发现控制台每次都打印 --- 查询数据库 ---,缓存注解 @Cacheable 完全没有生效。
2. 原理深度解析:谁偷走了我的代理?
要理解这个问题,必须通过 Spring AOP 的底层原理——动态代理(Dynamic Proxy) 来看。
当我们使用 Spring 注入一个 Bean 时,Spring 注入给我们的其实不是在这个类本身,而是一个 “代理对象(Proxy)”。这个 Proxy 持有原始对象的引用,并在方法调用前后织入 AOP 逻辑(如:查找缓存、开启事务)。
- 外部调用(External Call):
当 Controller 调用service.getClientById()时,实际上是在调用 Proxy。Proxy 拦截请求,先检查 Redis 缓存,如果有则直接返回,没有才去调用真实对象。这是符合预期的。 - 内部调用(Self-Invocation):
当我们在validateClient方法内部写getClientById(id)时,本质上执行的是this.getClientById(id)。
这里的this指向的是 目标对象(Target Object)本身,而不是 Proxy 类。
结果:代码直接在这个“裸”对象内部跳转,完全绕过了 Proxy 这个“守门员”,AOP 的增强逻辑自然也就无法执行。
3. 解决方案:如何让“自调用”也能走代理?
既然知道了原因(绕过了 Proxy),解决思路就是:强制通过 Proxy 来调用内部方法。
这里介绍三种常见的解决方案,其中“获取自引用”是很多成熟框架(如你看到的 Yudao 源码)采用的方案。
方案 A:获取自引用(getSelf 模式)—— 推荐
通过一个 getSelf() 方法或者 AopContext 获取当前的代理对象。
public void validateClient(String id) {
// 【修正】不直接调,而是通过“代理对象”去调
OAuth2ClientDO client = getSelf().getClientById(id);
}
/**
* 获取当前 Bean 的代理对象
*/
private ClientService getSelf() {
// 方式1:通过 AopContext (需在配置开启 exposeProxy=true)
return (ClientService) AopContext.currentProxy();
// 方式2:或者利用 ApplicationContext.getBean(ClientService.class)
}
方案 B:自我注入(Self-Injection)
利用 Spring 的依赖注入,将自己注入给自己。
@Service
public class ClientService {
// 注入自己(代理对象)
@Resource
@Lazy // 加上 @Lazy 防止循环依赖报错
private ClientService self;
public void validateClient(String id) {
// 通过注入的代理对象调用
self.getClientById(id);
}
}
方案 C:代码重构(拆分 Service)
如果内部调用逻辑过于复杂,最符合设计模式的做法是将 getClientById 抽取到另一个独立的 Service(如 ClientQueryService)中。
CGLIB如何实现
- 在SpringBoot较新版本全量使用CGLIB代理来实现AOP,早期使用JDK动态代理
- CGLIB生成一个代理类的字类,并重写其中的非final方法,在方法中先执行增强逻辑advice,再通过super.method()调用父类原始逻辑
- JDK动态代理的方式利用反射机制,有性能损耗
- CGLIB并不依赖java编译器,并不是“先生成子类再编译为.class”,而是使用FastClass机制代替反射,更快。
更多推荐

所有评论(0)