Skip to content

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
3final无法继承/重写去掉 final
4new 对象不是 Spring Bean用注入
5@PostConstruct代理还没生成用 ApplicationReadyEvent
6多切面顺序默认顺序不确定用 @Order
7异步线程上下文线程绑定调用方开事务