← 返回题目列表

代理模式和 Spring AOP 是什么关系?

高频 中等 第 4 / 25 题 更新于 2026/07/28
代理模式Spring AOP动态代理横切逻辑

简化版

Spring AOP 的核心实现思想就是代理模式:Spring 为目标 Bean 创建代理对象,调用方法时先经过代理,再执行事务、日志、权限等横切逻辑。Spring AOP 常用 JDK 动态代理或 CGLIB 生成代理对象。

详细版

Spring AOP 用代理模式解决横切逻辑复用问题。例如:

  • @Transactional:方法前开启事务,方法后提交或回滚;
  • 日志切面:方法前后记录日志;
  • 权限切面:方法执行前检查权限;
  • 监控切面:统计耗时和异常;
  • 缓存切面:命中缓存时直接返回。

调用链可以理解为:

客户端 -> Spring 代理对象 -> AOP 增强逻辑 -> 目标对象方法

如果目标对象实现了接口,Spring 可以使用 JDK 动态代理;如果没有接口,Spring 可以使用 CGLIB 创建子类代理。

重要的是:Spring AOP 是基于代理对象生效的。如果某个调用没有经过代理对象,例如同一个类内部方法自调用,就可能导致切面不生效。

示例:

public class OrderService {
    public void outer() {
        inner(); // 可能绕过代理
    }

    @Transactional
    public void inner() {
        // 事务逻辑
    }
}

因为 outer() 内部直接调用 this.inner(),没有经过 Spring 代理对象,所以 inner() 上的事务增强可能不会按预期生效。

完整版教学

一、AOP 为什么需要代理

AOP 解决的是横切关注点问题。

横切关注点指很多业务方法都需要,但又不是业务本身的逻辑,例如:

  • 事务;
  • 日志;
  • 权限;
  • 缓存;
  • 监控;
  • 异常处理。

如果每个方法都手写这些逻辑,代码会重复且难维护。

代理模式可以把这些逻辑放到代理对象里:

业务方法只写业务
代理对象负责增强

这就是 Spring AOP 和代理模式的连接点。

二、Spring AOP 的基本调用链

假设有一个 Bean:

class OrderService {
    public void createOrder() {
        System.out.println("创建订单");
    }
}

如果它被事务、日志等切面增强,Spring 容器中暴露给调用方的可能不是原始 OrderService,而是代理对象。

调用链类似:

controller 调用 orderService.createOrder()

实际进入代理对象

执行事务拦截器、日志拦截器等

调用目标对象 createOrder()

执行后置增强

所以 Spring AOP 的本质不是“修改原方法源码”,而是“让调用先经过代理”。

三、JDK 动态代理和 CGLIB 在 Spring 中的选择

Spring 可以使用两类代理技术:

JDK 动态代理:

  • 基于接口;
  • 代理对象实现目标接口;
  • 调用进入 InvocationHandler 或框架封装的拦截链。

CGLIB:

  • 基于继承;
  • 代理对象是目标类子类;
  • 方法调用进入方法拦截器。

常见选择逻辑:

  • 目标类实现接口:可以使用 JDK 动态代理;
  • 目标类没有接口:使用 CGLIB;
  • 配置强制类代理:使用 CGLIB。

不同 Spring 版本和配置细节可能不同,但这个理解在面试中足够抓住主线。

四、为什么自调用会失效

自调用是 Spring AOP 最经典的坑。

例如:

public void outer() {
    inner();
}

这行 inner() 本质上是:

this.inner();

它是当前对象内部调用,没有从外部经过代理对象。既然没有经过代理,代理上的事务、日志、权限等增强就没有机会执行。

解决思路包括:

  • 把被增强方法拆到另一个 Bean,通过 Spring 注入调用;
  • 从容器中获取当前代理对象再调用;
  • 调整事务边界,避免依赖内部自调用;
  • 使用 AspectJ 编织等非代理机制,但复杂度更高。

最推荐的是重新设计服务边界,让需要增强的方法由外部通过代理调用。

五、Spring AOP 的边界

Spring AOP 主要是方法级代理。它不像完整 AspectJ 那样能对字段访问、构造器调用等更多连接点做编织。

同时,因为它基于代理,所以会受到代理机制限制:

  • JDK 动态代理要求接口;
  • CGLIB 受 final 限制;
  • 自调用可能失效;
  • 非 Spring 管理的对象不会被 Spring AOP 增强;
  • private 方法通常无法作为代理增强入口。

理解这些限制,比只会写 @Aspect 更重要。

六、Spring AOP 的代理入口

外部调用 bean.pay() 经过代理可触发事务;同一对象内 this.pay() 只发生普通 Java 调用,经过代理的次数从 1 变为 0。

client -> AOP proxy -> interceptor chain -> target; target -> this.method (bypass proxy)

这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。

七、边界、代价与验证

检查维度应确认的内容
正确性注解写在方法上只是元数据,是否生效取决于调用有没有穿过容器创建的代理。
适用边界Spring AOP 主要拦截 Spring Bean 的方法执行连接点,不等同于 AspectJ 能覆盖的全部连接点。
测试证据覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序
运行成本记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量

模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。

易错点:注解写在方法上只是元数据,是否生效取决于调用有没有穿过容器创建的代理。

八、常见误区与追问

  • 误区:只要方法上有 @Transactional,事务就必然生效。 非 Spring 管理对象、不可代理方法或自调用都可能让代理拦截链无法进入。
  • 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
  • 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
  • 追问:自调用失效有哪些稳妥解决方式? 优先拆分 Bean 让调用经过代理;也可重构事务边界,避免依赖暴露代理等侵入方案。
  • 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
  • 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。

九、加强记忆

Spring AOP 可以理解成代理模式的工程化落地:容器给 Bean 套代理,代理负责事务、日志、权限等横切逻辑。只要记住“必须经过代理才生效”,很多 AOP 坑就能解释清楚。