代理模式和 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 坑就能解释清楚。