SpringNotes

第 05 章:AOP

zjc 于 2026-01-05 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 AOP 用于把日志、监控、权限、事务、限流、审计这类横切逻辑从业务代码中抽离。Spring AOP 是运行时代理模型,理解代理边界比记住切点语法更重要。

5.1 核心概念

概念 说明
Joinpoint 程序执行点,Spring 主要是方法执行
Pointcut 匹配哪些 Joinpoint
Advice 匹配后执行的逻辑
Aspect 切面,切点与通知的组合
Target Object 被代理目标
AOP Proxy 容器暴露的代理对象
Weaver 织入者,Spring 在运行时创建代理

常用通知:

注解 时机
@Before 方法前
@AfterReturning 正常返回后
@AfterThrowing 抛异常后
@After finally 语义
@Around 包裹方法,可控制是否执行

5.2 第一个切面

@Aspect
@Component
public class LoggingAspect {
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Pointcut("within(com.example.order.service..*)")
    public void orderServiceMethods() {}

    @Around("orderServiceMethods()")
    public Object logCost(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed();
        } finally {
            long cost = (System.nanoTime() - start) / 1_000_000;
            log.info("{} cost={}ms", pjp.getSignature().toShortString(), cost);
        }
    }
}

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

5.3 切点表达式

execution(* com.example.order.service.OrderService.create(..))
within(com.example.order.service..*)
@annotation(com.example.log.LogCost)
@within(com.example.log.LogModule)
bean(orderService)
target(com.example.order.service.OrderService)

常用表达式:

表达式 说明
execution 按方法签名匹配
within 按类型包范围匹配
@annotation 按方法注解匹配
@within 按类注解匹配
bean 按 Bean 名称匹配
args 按参数类型匹配

工程建议:避免复杂通用表达式,优先使用注解切点,可读性更好。

5.4 自定义注解切面

注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogCost {
    String value() default "";
}

切面:

@Aspect
@Component
public class LogCostAspect {

    @Around("@annotation(logCost)")
    public Object around(ProceedingJoinPoint pjp, LogCost logCost) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            System.out.printf("%s cost %dms%n",
                    logCost.value(), System.currentTimeMillis() - start);
        }
    }
}

使用:

@LogCost("createOrder")
public Order create(CreateOrderCommand command) {
    return repository.save(new Order(command.userId()));
}

5.5 JDK 动态代理与 CGLIB

代理 条件 特点
JDK 动态代理 目标有接口 基于接口反射
CGLIB 无接口或强制使用 生成子类

Spring Boot 默认通常使用 CGLIB 代理。CGLIB 限制:

  1. final 类不能代理;
  2. private 方法不能拦截;
  3. final 方法不能覆盖;
  4. 构造器不能是私有或不可访问;
  5. 代理会引入类型差异;
  6. 自调用问题依然存在。

5.6 自调用失效

@Service
public class OrderService {

    public void create(Order order) {
        this.validate(order);
    }

    @LogCost("validate")
    public void validate(Order order) {
    }
}

外部调用:

Proxy.create()
  -> target.create()
     -> this.validate()  // 直接调用目标对象

this.validate() 绕过代理,所以切面不执行。

解决方式:

  1. 拆分 Bean;
  2. 通过 AopContext.currentProxy()
  3. 自注入代理对象;
  4. 改成外部调用;
  5. 使用 AspectJ 编译期或类加载期织入。

最推荐第 1 种:把被拦截动作放到另一个职责清晰的 Bean。

5.7 参数与返回值

读取参数:

Object[] args = pjp.getArgs();

记录安全日志时必须脱敏:

Object[] args = pjp.getArgs();
for (Object arg : args) {
    if (arg instanceof LoginCommand command) {
        log.info("user={}", command.username());
    }
}

修改返回值:

@Around("...")
public Object hidePhone(ProceedingJoinPoint pjp) throws Throwable {
    Object result = pjp.proceed();
    if (result instanceof UserView view) {
        return new UserView(view.id(), mask(view.phone()));
    }
    return result;
}

不要在切面中随意吞异常:

try {
    return pjp.proceed();
} catch (Exception e) {
    return null; // 破坏调用方错误处理
}

5.8 切面顺序

@Aspect
@Order(1)
public class SecurityAspect {
}

@Aspect
@Order(2)
public class LoggingAspect {
}

简化规则:

order 小的 @Around 先进入
order 小的 @After 后执行

常见顺序:

审计 / 权限
  -> 限流
     -> 日志
        -> 事务
           -> 业务

事务通常在最内层,确保日志先记录请求,但具体顺序要按业务语义设计并写测试验证。

5.9 AOP 与事务的关系

@Transactional 本质是 AOP 拦截:

TransactionInterceptor
  -> 开启事务
  -> 执行业务方法
  -> 正常提交
  -> 异常回滚

因此以下情况会失效:

  1. 同类自调用;
  2. 方法不是 public;
  3. final 或 static 方法;
  4. 异常被内部吞掉;
  5. 默认回滚规则不匹配受检异常;
  6. 多线程内部逻辑不在同一事务上下文;
  7. 数据库引擎不支持事务;
  8. 传播行为设置错误。

5.10 性能与治理

切面不是免费能力:

  1. 反射获取注解有成本;
  2. 过宽切点会拦截大量方法;
  3. 同步日志可能拖慢请求;
  4. 代理对象创建增加启动成本;
  5. 复杂序列化可能泄露敏感数据。

规范:

  1. 切点必须精确;
  2. 高频路径加开关;
  3. 日志异步化;
  4. 参数脱敏;
  5. 异常只观察不吞;
  6. 给切面写单元测试;
  7. 使用 Micrometer 而不是手写指标协议;
  8. 不在切面中写业务规则。

本章小结

Spring AOP 通过运行时代理实现横切逻辑,核心是切点、通知和代理对象。CGLIB 与 JDK 动态代理各有条件,private、final 和自调用都会导致拦截失效。工程上应优先使用自定义注解切点,保持切面职责单一,并通过测试验证顺序和异常行为。

思考题

  1. @Around@After 的执行顺序关系是什么?
  2. 为什么自调用不经过代理?
  3. CGLIB 代理有哪些限制?
  4. @Transactional 为什么本质上也是 AOP?
  5. 如何设计一个安全且高性能的日志切面?