使用代理模式有哪些常见坑?
简化版
代理模式常见坑包括:绕过代理导致增强不生效、Spring AOP 自调用失效、JDK 动态代理要求接口、CGLIB 受 final 限制、代理层写入过多业务逻辑、缓存代理忽略一致性和并发问题。核心原则是必须经过代理,增强才有机会执行。
详细版
常见问题可以分成几类。
第一,绕过代理直接调用目标对象:
new UserServiceImpl().createUser();
如果增强逻辑在代理里,直接调用目标对象就不会触发日志、权限、事务等逻辑。
第二,Spring AOP 自调用失效:
public void outer() {
inner(); // this.inner(),没有经过代理
}
第三,代理技术选择错误:
- JDK 动态代理需要接口;
- CGLIB 不能代理
final类; - CGLIB 不能增强
final或private方法。
第四,代理对象类型误判。JDK 动态代理生成的是接口代理,按具体实现类注入可能出问题。
第五,代理层承担过多业务逻辑。代理适合做访问控制和横切增强,不适合变成业务流程中心。
第六,缓存代理忽略一致性。缓存不是简单 Map.get() 和 Map.put(),还要考虑过期、失效、并发击穿、空值和权限上下文。
面试时围绕一句话展开:代理模式的增强依赖调用经过代理对象,一旦绕过代理,增强就不会执行。
完整版教学
一、坑一:没有经过代理对象
代理模式的调用链是:
客户端 -> 代理对象 -> 目标对象
增强逻辑写在代理对象里。如果调用链变成:
客户端 -> 目标对象
代理就完全没有机会工作。
这在手写代理里很容易理解:
UserService target = new UserServiceImpl();
target.createUser(); // 不会走 UserServiceProxy
在 Spring 中也一样。如果你手动 new 一个对象,而不是从容器里拿 Bean,这个对象通常不会带 Spring AOP 代理增强。
二、坑二:Spring AOP 自调用
自调用是 Spring AOP 高频坑。
class OrderService {
public void outer() {
inner();
}
@Transactional
public void inner() {
// ...
}
}
outer() 内部调用 inner(),本质是 this.inner()。这个调用没有从外部进入代理对象,因此 inner() 上的事务增强可能不生效。
常见解决方式:
- 把
inner()拆到另一个 Service; - 通过代理对象调用;
- 调整事务标注位置;
- 避免把事务边界设计在内部自调用上。
最干净的方式通常是重新划分服务职责,让需要增强的方法通过 Spring Bean 外部调用进入。
三、坑三:JDK 动态代理和 CGLIB 限制没搞清
JDK 动态代理要求接口。如果类没有接口:
class UserService { }
JDK 动态代理不能直接代理它。
CGLIB 不要求接口,但依赖继承:
final class UserService { }
这种类不能被 CGLIB 继承。
方法也一样:
public final void createUser() {}
private void check() {}
这些方法不能被子类正常重写增强。
所以代理技术没有绝对万能,必须理解底层约束。
四、坑四:代理层业务过重
代理层适合:
- 权限校验;
- 日志;
- 缓存;
- 事务;
- 监控;
- 远程调用转发。
但如果代理层写了大量业务规则:
if (orderStatus == PAID) {
// A 业务流程
} else if (orderStatus == CANCELLED) {
// B 业务流程
}
代理就变味了。这类状态流转更适合放在领域服务、策略模式、状态模式或业务流程编排里。
代理层应该保持“围绕访问过程增强”的定位。
五、坑五:缓存代理看起来简单,实际很容易错
缓存代理常写成:
if (cache.containsKey(key)) {
return cache.get(key);
}
Object result = target.query(id);
cache.put(key, result);
return result;
但真实系统要考虑:
- 数据更新后缓存如何删除;
- 缓存和数据库短暂不一致是否可接受;
- 热点 key 过期时是否会击穿;
- 查询为空时是否缓存空值;
- key 是否包含用户权限和租户;
- 缓存对象是否会被调用方修改。
如果这些问题没处理,缓存代理可能引入比原来更难查的 bug。
六、坑六:类型注入和代理对象认知错误
JDK 动态代理生成的对象通常只实现接口,不是目标实现类本身。
如果你按接口注入:
@Autowired
private UserService userService;
通常没问题。
但如果按实现类注入:
@Autowired
private UserServiceImpl userService;
在某些 JDK 动态代理场景下可能失败,因为代理对象不是 UserServiceImpl 类型。
这也是为什么面向接口编程在代理场景下更稳。
七、用调用链定位代理失效
排查事务失效时依次确认 4 点:对象是否由容器创建、引用是否为代理、方法是否可拦截、调用是否来自代理外部;任一点为否都可能失败。
bean source -> proxy type -> method visibility/final -> call entry -> interceptor
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
八、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 代理问题先找“谁持有谁、调用从哪里进入”,比只盯着注解更有效。 |
| 适用边界 | 不要把缓存一致性、远程幂等或事务传播等业务语义误认为代理框架会自动解决。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:代理问题先找“谁持有谁、调用从哪里进入”,比只盯着注解更有效。
九、常见误区与追问
- 误区:切换成 CGLIB 就能修复所有代理失效。 自调用仍可能绕过代理,private/final 等限制也不会因盲目切换而消失。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:怎样快速证明当前对象是不是代理? 可检查运行时类型和 AOP 工具判断,并用最小调用验证拦截器是否真正执行。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
十、加强记忆
代理模式最核心的坑就一句:增强必须经过代理才会生效。JDK 看接口,CGLIB 看继承,Spring AOP 看代理调用链;绕过代理、误用 final、自调用、缓存不严谨,都是代理模式的高频翻车点。