Spring AOP:面向切面编程的优雅解耦之道
在企业开发中,日志记录、事务管理、权限校验等如果直接写在业务代码中,会导致逻辑混杂、重复冗余、难以维护。
Spring AOP(面向切面编程) 正是为了解决这一问题而生。它通过"在不修改源码的前提下,动态织入通用逻辑"的方式,实现关注点分离,让核心业务更纯粹。
今天,我们就从思想本质、生活类比、实战用法到最佳实践,带你彻底掌握 Spring AOP!
一、生活类比:智能家居中的"自动化场景"
想象你家里装了一套智能家居系统:
-
你每天晚上 10 点躺到床上,并不需要手动去关灯、拉窗帘、调低空调温度;
-
而是提前设置了一个 "睡眠模式"自动化场景:
- 当你对语音助手说"我要睡觉了",
- 系统自动执行一系列操作:关主灯、拉上窗帘、空调调至 26℃、关闭电视......
✅ 这些操作(关灯、调温等)并不是你"主动写进每个夜晚流程"的,而是通过一个独立的规则(切面) ,在特定触发点(你说"睡觉")动态插入执行。
二、什么是 AOP?
AOP 是一种编程范式,用于将与核心业务无关但又广泛使用的功能(如日志、事务)模块化为"切面(Aspect)",并在运行时动态织入目标方法。
| 概念 | 说明 |
|---|---|
| Aspect(切面) | 横切关注点的模块化,如日志切面、事务切面 |
| Join Point(连接点) | 程序执行过程中的某个点,如方法调用、异常抛出(Spring AOP 仅支持方法级) |
| Pointcut(切入点) | 匹配哪些连接点要被拦截(通过表达式定义) |
| Advice(通知) | 切面在特定连接点上执行的动作(前置、后置、环绕等) |
| Weaving(织入) | 将切面应用到目标对象的过程(Spring 采用运行时动态代理) |
✅ 本质:通过代理模式 + 表达式匹配,在方法调用前后插入通用逻辑
三、Spring AOP 实战五步走
第一步:pom 中新增依赖
xml
<!-- Spring AOP 支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.2.12</version>
</dependency>
<!-- 若使用注解驱动 AOP,还需确保 spring-context 已引入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.12</version>
</dependency>
💡 注意:
spring-aspects依赖了 AspectJ 的注解和运行时库,但 Spring AOP 本身不使用编译时织入,而是基于 JDK 动态代理或 CGLIB。
第二步:启用 AOP(配置类)
kotlin
package org.example.spring.aop.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy // 启用 @AspectJ 注解风格的 AOP
@ComponentScan(basePackages = "org.example.spring.aop")
public class AopConfig {
// 空配置类,仅用于开启 AOP
}
✅
@EnableAspectJAutoProxy是关键!它告诉 Spring 自动识别@Aspect并创建代理。
第三步:定义业务服务
typescript
package org.example.spring.aop.service;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
public void placeOrder(String orderId) {
System.out.println("Processing order: " + orderId);
}
public String getOrderStatus(String orderId) {
return "ORDER_CONFIRMED";
}
}
第四步:编写切面(日志示例)
typescript
package org.example.spring.aop.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 定义切入点:所有 service 包下的 public 方法
@Pointcut("execution(public * org.example.spring.aop.service..*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[AOP] Before method: " + methodName + ", args: " + java.util.Arrays.toString(args));
}
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP] After method: " + methodName + ", returned: " + result);
}
}
第五步:启动容器并测试
arduino
package org.example.spring.aop;
import org.example.spring.aop.config.AopConfig;
import org.example.spring.aop.service.OrderService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AopConfig.class);
OrderService orderService = context.getBean(OrderService.class);
orderService.placeOrder("ORD-12345");
System.out.println("---");
orderService.getOrderStatus("ORD-12345");
context.close();
}
}
输出:
yaml
[AOP] Before method: placeOrder, args: [ORD-12345]
Processing order: ORD-12345
[AOP] After method: placeOrder, returned: null
---
[AOP] Before method: getOrderStatus, args: [ORD-12345]
[AOP] After method: getOrderStatus, returned: ORDER_CONFIRMED
四、一张表总结 Spring AOP 核心要点
| 要素 | 注解 / 配置 | 说明 |
|---|---|---|
| 启用 AOP | @EnableAspectJAutoProxy |
必须在配置类上声明 |
| 定义切面 | @Aspect + @Component |
切面本身也是 Spring Bean |
| 定义切入点 | @Pointcut("execution(...)") |
使用 AspectJ 表达式 |
| 通知类型 | @Before, @After, @AfterReturning, @AfterThrowing, @Around |
环绕通知最强大 |
| 代理机制 | JDK 动态代理(接口) / CGLIB(类) | Spring 自动选择 |
⚠️ 注意:只有通过 Spring 容器获取的 Bean 才会被代理! 直接
new的对象不会触发 AOP。
五、思考题
以下说法是否正确?
"在同一个类中,方法 A 调用本类的方法 B(带
@Transactional),AOP 会生效。"
💡 答案:不会生效!原因:
- Spring AOP 基于代理对象,只有外部调用才会经过代理;
- 类内部的自调用(this.methodB())绕过了代理,直接调用原始对象方法;
- 解决方案:通过
AopContext.currentProxy()获取当前代理,或重构为两个 Bean 互相调用。
📌 关注我 ,每天5分钟,带你从 Java 小白变身编程高手!
👉 点赞 + 关注,让更多小伙伴一起进步!