主题
Spring AOP 指南
1. 基础概念
1.1 什么是 AOP
AOP(面向切面编程):将与业务逻辑无关、但系统多个模块都会用到的功能(如日志、权限校验、性能监控、事务管理)抽取出来,形成独立的"切面",从而避免这些代码散落在业务代码中。
AOP 切面 ≈ 中间件,都是在方法调用前后插入逻辑;但切面靠切点选择性命中,且依赖代理、存在自调用绕过问题。
1.2 什么是代理
Spring AOP 的核心机制只有一句话:注入给调用方的不是原始对象,而是 Spring 在运行期生成的一层包装。 这层包装持有原始对象的引用,拦截每一次方法调用,在调用前后插入额外的逻辑(Advice),再把流程交还给原始方法。
text
外部调用(经过代理包装):
调用方 ──▶ Proxy ──▶ [前置 Advice] ──▶ Target.method() ──▶ [后置 Advice] ──▶ 返回
同类内部自调用(绕过代理):
Proxy ──▶ Target.methodA() ──▶ this.methodB() ← 直接调 this,Advice 不触发!1.3 AOP 术语速览
Spring AOP 有 7 个核心术语,用一张图讲清关系:
text
┌─────────────────────────────────────────────────────────────────┐
│ 切面 Aspect │
│ = 通知 Advice + 切点 Pointcut │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 连接点 Join Point 切点 Pointcut │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ UserService │ │ 匹配规则(例): │ │
│ │ ├─ createUser()│◄─────────│ @annotation(Log) │ │
│ │ ├─ deleteUser()│ 匹配 │ = 标了@Log的 │ │
│ │ ├─ updateUser()│ │ 方法都增强 │ │
│ │ └─ getUser() │ └─────────────────┘ │
│ └─────────────────┘ │
│ ↑ 所有方法都是潜在连接点 │
│ │
│ 通知 Advice(5种时机) │
│ ├─ @Before:方法执行前 │
│ ├─ @After:方法执行后(无论成败) │
│ ├─ @AfterReturning:正常返回后 │
│ ├─ @AfterThrowing:抛出异常后 │
│ └─ @Around:包裹整个方法(最强大) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ 织入 Weaving │
│ Spring启动时,将通知逻辑织入到匹配的方法上: │
│ │
│ 目标 Target → 代理 Proxy │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 原始Bean │ │ 代理Bean │ │
│ │ 业务逻辑 │──►│ +增强逻辑 │ │
│ └─────────────┘ └─────────────┘ │
│ 普通对象 对外暴露的对象,包含额外功能 │
└─────────────────────────────────────────────────────────────────┘术语对照表
| 术语 | 一句话解释 |
|---|---|
| 连接点 Join Point | 程序中可被拦截的"位置"(Spring 中即方法) |
| 切点 Pointcut | 匹配规则,筛选"哪些方法需要增强" |
| 通知 Advice | 在匹配到的方法"做什么" |
| 切面 Aspect | 切点 + 通知,完整的横切逻辑单元 |
| 目标 Target | 原始对象(被增强的那个) |
| 代理 Proxy | 增强后的包装对象,对外替代 Target |
| 织入 Weaving | 把切面应用到目标对象、生成代理的过程 |
核心公式
切面 = 切点(Where)+ 通知(What)切点决定在哪里切入,通知决定做什么。
2. 五种通知类型
Spring AOP 提供 5 种通知,覆盖方法执行的所有时机点。以下 5 个示例的切点都直接写在通知注解里,即内联切点(同一条规则被多处引用时应改用命名切点,见 3.4 节):
text
@Around(环绕,包裹整个方法)
┌─────────────────────────────────────────────────────┐
│ @Before(方法执行前) │
│ ┌─────────────────────────────────────────────┐ │
│ │ │ │
│ │ targetMethod() 目标方法执行 │ │
│ │ │ │
│ │ 正常返回 → 触发 @AfterReturning │ │
│ │ 抛出异常 → 触发 @AfterThrowing │ │
│ │ │ │
│ └─────────────────────────────────────────────┘ │
│ @After(方法执行后,无论成败) │
└─────────────────────────────────────────────────────┘| 通知 | 触发时机 | 能否阻止执行 | 获取返回值 | 获取异常 |
|---|---|---|---|---|
| @Before | 方法执行前 | ✅(抛异常即可) | ❌ | ❌ |
| @AfterReturning | 正常返回后 | ❌ | ✅ | ❌ |
| @AfterThrowing | 抛出异常后 | ❌ | ❌ | ✅ |
| @After | 方法执行后(无论成败) | ❌ | ❌ | ❌ |
| @Around | 包裹整个方法 | ✅(最强控制) | ✅ | ✅ |
2.1 @Before
方法执行前触发。如果通知里抛异常,目标方法不会执行。
适用场景:参数校验、权限前置检查、防重复提交校验。
java
@Aspect
@Component
public class ValidationAspect {
//com.example.service 包下所有类的所有方法,都会执行validateParameters方法
@Before("execution(* com.example.service.*.*(..))")
public void validateParameters(JoinPoint joinPoint) {
// 获取被拦截方法的参数
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg == null) {
throw new IllegalArgumentException(
joinPoint.getSignature().getName() + " 参数不能为 null");
}
}
}
}2.2 @After
方法执行后触发,无论正常返回还是抛异常都会执行。拿不到返回值。
适用场景:审计日志、资源清理(关闭流)、操作计数。
java
@Aspect
@Component
public class AuditAspect {
@After("execution(* com.example.controller.*.*(..))")
public void auditLog(JoinPoint joinPoint) {
String method = joinPoint.getSignature().toShortString();
// 无论请求成功还是失败,都记录审计日志
System.out.println("[审计] " + method + "被调用,操作人:" + getCurrentUser());
}
private String getCurrentUser() {
// 从 SecurityContext 获取当前用户
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}2.3 @AfterReturning
方法正常返回后触发,可以获取和修改返回值。
适用场景:返回值脱敏、返回值包装(统一加 { code: 200, data: ... })、缓存返回值。
java
@Aspect
@Component
public class DataMaskAspect {
@AfterReturning(
pointcut = "execution(* com.example.controller.UserController.*(..))",
returning = "result")
public void maskPhone(Object result) {
// 如果返回值中包含手机号,自动脱敏
if (result instanceof User) {
User user = (User) result;
user.setPhone(maskPhoneNumber(user.getPhone()));
}
}
private String maskPhoneNumber(String phone) {
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
// 13812345678 → 138****5678
}
}2.4 @AfterThrowing
方法抛异常后触发,可以拿到异常对象。
适用场景:异常告警、错误上报 Sentry、自动重试决策。
java
@Aspect
@Component
public class AlertAspect {
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex")
public void sendAlert(JoinPoint joinPoint, Exception ex) {
String method = joinPoint.getSignature().toShortString();
// 发送钉钉/邮件告警
System.err.printf("[告警] %s 异常:%s%n", method, ex.getMessage());
// 实际项目中这里会调用消息推送 SDK
}
}2.5 @Around
包裹目标方法,可以决定是否执行目标方法、在前后插入逻辑、修改参数和返回值。前面的 4 种通知本质上都是 @Around 的特化版本。
适用场景:性能监控、事务管理、缓存决策、分布式锁、重试。
java
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // ⚠️ 必须调用,否则目标方法不会执行
long elapsed = System.currentTimeMillis() - start;
String method = joinPoint.getSignature().toShortString();
if (elapsed > 1000) {
System.err.printf("[慢查询] %s 耗时 %dms%n", method, elapsed);
}
return result;
}
}3. Pointcut 表达式详解
五种通知写好后,关键问题是:如何精准匹配要拦截的方法?
3.1 六种常用表达式
| 表达式 | 用途 | 示例 |
|---|---|---|
execution | 匹配方法签名 | execution(public * com.example.service.*.*(..)) |
@annotation | 匹配带指定注解的方法 | @annotation(org.springframework.transaction.annotation.Transactional) |
within | 匹配指定类的所有方法 | within(com.example.service.UserService) |
@within | 匹配带指定注解的类 | @within(org.springframework.stereotype.Service) |
args | 匹配指定参数类型 | args(java.lang.Long, ..) |
bean | 匹配指定 Bean 名称 | bean(userService) |
3.2 execution 表达式语法
完整结构:
text
execution(修饰符? 返回类型 包。类。方法名 (参数列表) throws 异常?)
│ │ │ │
│ │ │ └── 通常省略
│ │ └── 支持通配符:* 任意名字,.. 任意包/参数
│ └── * 任意返回类型,或写具体类型
└── public/private/... 通常省略(默认匹配所有)常用示例:
java
// 匹配 com.example.service 包下所有类的所有 public 方法
@Pointcut("execution(public * com.example.service.*.*(..))")
// 匹配任意包下所有以 find 开头的方法
@Pointcut("execution(* find*(..))")
// 匹配参数是 (Long, String) 且返回 Product 的方法
@Pointcut("execution(com.example.Product *(java.lang.Long, java.lang.String))")3.3 组合表达式(内联切点)
用 &&(且)、||(或)、!(非)组合:
java
@Around("execution(* com.example.service.*.*(..)) " +
"&& !@annotation(com.example.SkipLog)")
public Object logUnlessSkipped(ProceedingJoinPoint pjp) { ... }3.4 命名切点(多处引用时的推荐做法)
与 3.3 的内联写法相反:把 @Pointcut 单独声明成一个空方法,其他地方按方法名引用,避免重复写表达式:
java
@Aspect
@Component
public class AuditAspect {
@Pointcut("execution(public * com.example.service.*.*(..))")
public void serviceLayer() {} // 空方法,只是给 Pointcut 起个名字
@Pointcut("@annotation(com.example.aop.AuditLog)")
public void auditedMethods() {}
@Before("serviceLayer() && auditedMethods()") // 使用示例:按名字引用
public void checkAudit(JoinPoint jp) { ... }
}为什么空方法体不需要实现:@Pointcut 标注的方法只是一个"名字",给表达式挂个可引用的标识——Spring 解析时只看注解字符串,从不调用这个方法,方法体写什么都不会执行。真正会执行的是通知方法(如 checkAudit):切点方法 = 常量名,通知方法 = 真正的函数。
4. 代理是什么时候生成的?
代理不是和 Bean 一起 new 出来的,而是在 Bean 初始化完成后由 BeanPostProcessor 包装:
text
Spring 容器启动,为每个 Bean 走这条流水线:
实例化(new)
│
▼
属性填充(@Autowired 注入)
│
▼
初始化前处理(BeanPostProcessor.postProcessBeforeInitialization)
│
▼
初始化(@PostConstruct → InitializingBean.afterPropertiesSet → init-method)
│
▼
初始化后处理(BeanPostProcessor.postProcessAfterInitialization)
│ ★ AbstractAutoProxyCreator 在这里介入:
│ 如果这个 Bean 匹配任何 Pointcut,就用代理包一层再返回
▼
放入单例池,供其他 Bean 注入关键推论:@PostConstruct 里通过 this 调用同类方法时,代理还没生成,任何 AOP 注解都失效——这是 5.5(初始化时机)的根源;
5. 注解失效排查清单
遇到 @Transactional / @Cacheable / @Async 不生效时,按这 7 条对照排查:
5.1 自调用(最常见)
症状:同类内部方法互调,注解失效
原因:this.method() 不经过代理
示例:
java
@Service
public class TransferService {
public void transfer(Long from, Long to, Double amount) {
debit(from, amount); // ❌ this.debit(),@Transactional 失效
credit(to, amount); // ❌ this.credit(),@Transactional 失效
}
@Transactional
public void debit(Long id, Double amount) { ... }
@Transactional
public void credit(Long id, Double amount) { ... }
}修复:拆到独立 Bean
java
@Service
public class TransferService {
private final AccountService accountService; // 注入代理
public TransferService(AccountService accountService) {
this.accountService = accountService;
}
public void transfer(Long from, Long to, Double amount) {
accountService.debit(from, amount); // ✅ 经过代理
accountService.credit(to, amount); // ✅ 经过代理
}
}5.2 非 public 方法
症状:private / protected / 包级私有方法上的注解无效
原因:代理只能拦截 public 方法
原理:Spring AOP 的增强只能挂在"代理对象自己的方法"上,两条生成路径各有一道 public 门槛:
- CGLIB 子类:靠重写方法插入逻辑。private 子类看不见、谈不上重写;protected 虽可重写,但 Spring 对
@Transactional/@Cacheable这类方法级事件只认 public(且跨包调用仍走父类版本) - JDK 接口代理:不是重写,而是实现同一接口再持有目标引用;接口方法按语法强制 public,private 方法不在接口契约内,代理没有对应入口
结论:只有 public 方法在两条路径下都保证有"代理入口",这是 5.2 的直接根源,也是 5.3(final)同一机制的另一侧面。
示例:
java
@Service
public class ProductService {
@Transactional // ❌ 无效:private 方法
private void internalUpdate(Long id) { ... }
@Cacheable("product") // ❌ 无效:protected
protected Product loadFromDb(Long id) { ... }
@Transactional // ✅ 生效
public void update(Long id) { ... }
}修复:改为 public
5.3 final 类/方法
症状:final 类启动报错,final 方法注解静默失效
原因:代理通过继承生成子类,无法重写 final
示例:
java
@Service
public final class ProductService { // ❌ 启动报错:Cannot subclass final class
...
}
@Service
public class OrderService {
@Transactional
public final void create() { ... } // ❌ 静默失效:无法重写 final 方法
}修复:去掉 final
5.4 new 出来的对象
症状:手动 new 的对象注解无效
原因:只有 Spring 管理的 Bean 才有代理
示例:
java
@RestController
public class OrderController {
@PostMapping("/orders")
public String create() {
OrderService service = new OrderService(); // ❌ 没有代理
service.create(); // @Transactional 失效
return "ok";
}
// ✅ 正确:注入 Spring Bean
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
}修复:用 @Autowired 或构造器注入
5.5 @PostConstruct 内调用
症状:初始化方法里调用的注解方法失效
原因:@PostConstruct 执行时代理还没生成(见第 4 章)
示例:
java
@Service
public class ReportService {
@PostConstruct
public void init() {
generateReport(); // ❌ this.generateReport(),@Async 失效
}
@Async
public void generateReport() { ... }
}修复:用 ApplicationReadyEvent 代替
java
@Service
public class ReportInitializer {
private final ReportService reportService;
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
reportService.generateReport(); // ✅ 经过代理
}
}5.1 与 5.5 的本质区别:两者代码形态相同(都是 this.method()),失效根源却相反——
| 5.1 自调用 | 5.5 @PostConstruct 内调用 | |
|---|---|---|
| 代理存在吗 | ✅ 早已生成,在单例池里等着 | ❌ 还没被创建(见第 4 章流水线) |
| 失效本质 | 调用走了 this(指向原始对象),绕开了门口站着的代理 | 不是绕开——是想经过也没得经过,代理此刻还没被创建 |
| 修复思路 | 修调用路径:拆独立 Bean / 自注入 / AopContext.currentProxy() | 修调用时机:推迟到 ApplicationReadyEvent,换路径也没用——拿不到还没出生的代理 |
5.6 多切面顺序错乱
症状:事务提交时机不对、日志耗时统计偏差
原因:多个切面默认顺序不确定
示例:
java
@Aspect
@Component
public class LoggingAspect { ... } // 可能先执行或后执行
@Aspect
@Component
public class TransactionAspect { ... } // 顺序不确定修复:用 @Order 显式指定(数值越小越外层)
java
@Aspect
@Component
@Order(1) // 最外层:先进后出
public class LoggingAspect { ... }
@Aspect
@Component
@Order(2) // 中间层
public class SecurityAspect { ... }
@Aspect
@Component
@Order(3) // 最内层:后进先出
public class TransactionAspect { ... }推荐:事务切面放最内层(@Order 最大),这样"提交事务"发生在"记录耗时"之前。
5.7 异步线程中丢失上下文
症状:@Async 方法里拿不到事务上下文、SecurityContext
原因:这些上下文是线程绑定的,跨线程丢失
示例:
java
@Service
public class OrderService {
private final AsyncService asyncService; // 注入代理(跨 Bean 调用,@Async 能生效)
public OrderService(AsyncService asyncService) {
this.asyncService = asyncService;
}
@Transactional
public void createOrder() {
saveOrder(); // 事务还没提交——commit 要等方法正常返回后
asyncService.process(); // @Async 立刻把任务丢进线程池
}
}
@Service
public class AsyncService {
@Async
public void process() {
// ❌ 两个丢失:
// 1. 此刻在主线程的事务提交之前——这里根本看不到订单数据
// 2. 事务上下文、SecurityContext 都是 ThreadLocal 线程绑定的——子线程全部拿不到
}
}修复:不要在异步方法里"依赖调用方的事务",而是把异步动作挂到事务提交事件上——用 @TransactionalEventListener 把时机交给 Spring:
java
// 事件类
public record OrderCreatedEvent(Long orderId) {}
// 主线程:事务内只发布事件,不做跨线程动作
@Transactional
public void createOrder() {
Order order = saveOrder();
publisher.publishEvent(new OrderCreatedEvent(order.getId()));
}
// 异步监听:默认阶段就是 AFTER_COMMIT——事务提交后才被触发,数据已确定可见
@Async
@TransactionalEventListener
public void onOrderCreated(OrderCreatedEvent event) {
sendSms(event.orderId()); // ✅ 此时能查到已提交的订单
}需要把
SecurityContext等 ThreadLocal 上下文真正传递给@Async子线程时,用TaskDecorator在线程池层面复制,不要手写Thread传递。
快速排查表
| 序号 | 场景 | 根因 | 修复 |
|---|---|---|---|
| 1 | 自调用 | this 不经过代理 | 拆到独立 Bean |
| 2 | 非 public | 代理只拦截 public | 改为 public |
| 3 | final | 无法继承/重写 | 去掉 final |
| 4 | new 对象 | 不是 Spring Bean | 用注入 |
| 5 | @PostConstruct | 代理还没生成 | 用 ApplicationReadyEvent |
| 6 | 多切面顺序 | 默认顺序不确定 | 用 @Order |
| 7 | 异步线程 | 上下文线程绑定 | 调用方开事务 |