Skip to content

第55课:Spring Core - AOP

🎯 学习目标

  • 理解 AOP 解决的是横切关注点复用问题,而不是替代业务逻辑。
  • 掌握切面、连接点、切点、通知、目标对象、代理对象等核心术语。
  • 能编写 @Aspect 切面,使用前置、后置、异常、环绕通知。
  • 理解 Spring AOP 基于代理,知道自调用、final 方法、代理类型带来的限制。
  • 能识别日志、事务、权限、限流、审计等适合 AOP 的场景。

📖 一、为什么需要 AOP

业务方法中经常夹杂横切逻辑:

java
public OrderDTO createOrder(OrderCommand command) {
    log.info("create order start");
    long start = System.currentTimeMillis();
    checkPermission();
    try {
        OrderDTO result = doCreate(command);
        audit("CREATE_ORDER", command);
        return result;
    } finally {
        log.info("create order cost={}", System.currentTimeMillis() - start);
    }
}

问题:

text
日志、权限、审计、耗时统计重复出现
业务逻辑被横切逻辑淹没
规则变更需要修改大量方法
难以统一控制顺序和异常处理

AOP(Aspect-Oriented Programming,面向切面编程)把这些横切关注点抽到切面中。


📖 二、AOP 核心术语

术语含义
JoinPoint连接点,程序执行点,Spring AOP 中主要是方法执行
Pointcut切点,匹配哪些连接点
Advice通知,在匹配点执行的增强逻辑
Aspect切面,切点 + 通知的组合
Target目标对象,原始业务对象
Proxy代理对象,调用方实际调用的对象
Weaving织入,把增强逻辑应用到目标对象

Spring AOP 默认基于代理,不会修改你的字节码文件。


📖 三、添加依赖

Spring Boot 项目中:

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

然后就可以使用 @Aspect


📖 四、基础切面示例

java
@Aspect
@Component
public class ServiceLogAspect {

    @Pointcut("execution(* com.example..service..*(..))")
    public void serviceMethods() {
    }

    @Before("serviceMethods()")
    public void before(JoinPoint joinPoint) {
        log.info("method start: {}", joinPoint.getSignature());
    }

    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        log.info("method success: {}", joinPoint.getSignature());
    }

    @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        log.warn("method failed: {}", joinPoint.getSignature(), ex);
    }
}

这个切面会增强 com.example 下 service 包中的方法。


📖 五、环绕通知

环绕通知最强,也最容易写错。

java
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.nanoTime();
    try {
        return joinPoint.proceed();
    } finally {
        long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
        log.info("method={}, costMs={}", joinPoint.getSignature(), costMs);
    }
}

必须调用 joinPoint.proceed(),否则目标方法不会执行。

环绕通知适合:

text
耗时统计
限流
分布式锁
统一日志
动态数据源
幂等控制

不适合把复杂业务逻辑塞进去。


📖 六、切点表达式

常用 execution

java
// 匹配 com.example.service 包下所有类所有方法
execution(* com.example.service.*.*(..))

// 匹配 service 包及子包
execution(* com.example.service..*.*(..))

// 匹配 public 方法
execution(public * *(..))

// 匹配返回值为 UserDTO 的方法
execution(com.example.UserDTO *(..))

按注解匹配:

java
@Around("@annotation(com.example.RateLimited)")
public Object rateLimit(ProceedingJoinPoint pjp) throws Throwable {
    return pjp.proceed();
}

自定义注解:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
    String action();
}

使用:

java
@AuditLog(action = "CREATE_ORDER")
public OrderDTO createOrder(OrderCommand command) {
    return doCreate(command);
}

📖 七、Spring AOP 的代理机制

Spring AOP 通常使用两种代理:

代理条件特点
JDK 动态代理目标类实现接口基于接口代理
CGLIB 代理目标类无接口或强制类代理基于子类代理

调用链:

text
调用方 -> 代理对象 -> 切面逻辑 -> 目标对象方法

这解释了很多限制。


⚠️ 八、常见陷阱

1. 自调用失效

java
@Service
public class OrderService {
    public void outer() {
        inner();
    }

    @Transactional
    public void inner() {
    }
}

outer() 内部直接调用 inner(),没有经过代理对象,AOP 不生效。事务、缓存、异步注解都有类似问题。

2. final 方法无法被 CGLIB 增强

CGLIB 基于子类覆盖方法,final 方法不能被覆盖。

3. 切点范围过大

java
execution(* com.example..*(..))

可能匹配 Controller、Repository、Config、Aspect 自己,导致性能和行为问题。

4. 环绕通知吞异常

错误示例:

java
try {
    return pjp.proceed();
} catch (Exception e) {
    log.error("failed", e);
    return null;
}

这会改变业务语义。除非明确设计降级,否则异常应继续抛出。


🆚 九、Java vs C 对比

维度C 常见方式Spring AOP
横切逻辑宏、函数包装、回调切面 + 代理
权限/日志手动嵌入函数注解或切点统一增强
动态增强函数指针表替换JDK Proxy/CGLIB
限制类型信息少受代理边界影响

AOP 可以看作一种更结构化的函数包装机制,但它依赖 Java 的反射、代理和容器。


💡 十、最佳实践

  • AOP 只处理横切关注点,不写核心业务规则。
  • 优先用注解切点表达显式增强,避免切点范围过大。
  • 环绕通知必须保证 proceed() 调用和异常语义正确。
  • 需要事务、缓存、异步时,要理解代理和自调用限制。
  • 切面执行顺序用 @Order 明确控制。
  • 日志切面不要打印敏感参数和大对象。
  • 高频方法上的切面要注意性能开销。
  • 如果增强逻辑复杂到难以理解,考虑显式代码而不是 AOP。

🧭 十一、适合 AOP 的场景

text
接口访问日志
方法耗时统计
权限校验
审计日志
分布式锁
幂等控制
限流
事务管理
缓存管理
动态数据源

不适合:

text
核心业务流程
复杂状态转换
需要明确阅读顺序的业务规则
强依赖调用上下文的逻辑

🔍 十二、自测问题

text
AOP 解决什么问题?
Aspect、Pointcut、Advice 分别是什么?
@Around 为什么必须调用 proceed?
Spring AOP 为什么有自调用失效问题?
JDK 动态代理和 CGLIB 有什么区别?
为什么 final 方法可能无法增强?
事务注解为什么本质上依赖 AOP?
什么逻辑不应该写进切面?

🧪 十三、实战案例:接口耗时统计切面

一个可落地的耗时切面应该避免打印大参数,并且保留 traceId:

java
@Aspect
@Component
public class ApiCostAspect {

    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object recordCost(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        String method = pjp.getSignature().toShortString();
        try {
            return pjp.proceed();
        } finally {
            long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            log.info("api cost, method={}, costMs={}", method, costMs);
        }
    }
}

如果要记录请求参数,应做限制:

text
不打印密码、Token、身份证、银行卡。
不打印 MultipartFile。
不打印超大集合。
只打印必要业务 ID。

🧭 十四、AOP 排查清单

切面不生效时,按顺序检查:

text
目标类是否是 Spring Bean?
调用是否经过代理对象?
方法是否 public?
方法是否 final?
切点表达式是否匹配?
切面类是否被扫描到?
是否有多个切面顺序冲突?
是否内部自调用绕过代理?

如果是事务、缓存、异步不生效,也可以按这份清单排查,因为它们都依赖 Spring 代理机制。


📌 十五、学习建议

建议用一个小项目分别实现:

text
耗时统计切面
审计日志切面
自定义注解限流切面
故意制造同类自调用失效
故意写错切点表达式再修复

这样能真正理解 AOP 的边界,而不是只会背术语。


📚 十六、关键术语速查

text
JoinPoint:可被增强的位置。
Pointcut:筛选哪些位置要增强。
Advice:增强逻辑本身。
Aspect:切点和通知的组合。
Proxy:调用方实际拿到的代理对象。
Target:被代理的业务对象。
Weaving:把增强应用到目标的过程。

看懂这些术语,后续理解事务、缓存、异步和安全代理会轻松很多。


🎓 小结

AOP 是 Spring 中非常重要的扩展机制。它让日志、事务、权限、审计等横切逻辑从业务代码中分离出来。但 Spring AOP 基于代理,必须理解代理边界,否则会遇到事务失效、自调用失效、切点误伤等问题。