← 返回题目列表

使用代理模式有哪些常见坑?

高频 困难 第 14 / 25 题 更新于 2026/07/28
代理模式常见误区Spring AOP动态代理

简化版

代理模式常见坑包括:绕过代理导致增强不生效、Spring AOP 自调用失效、JDK 动态代理要求接口、CGLIB 受 final 限制、代理层写入过多业务逻辑、缓存代理忽略一致性和并发问题。核心原则是必须经过代理,增强才有机会执行。

详细版

常见问题可以分成几类。

第一,绕过代理直接调用目标对象:

new UserServiceImpl().createUser();

如果增强逻辑在代理里,直接调用目标对象就不会触发日志、权限、事务等逻辑。

第二,Spring AOP 自调用失效:

public void outer() {
    inner(); // this.inner(),没有经过代理
}

第三,代理技术选择错误:

  • JDK 动态代理需要接口;
  • CGLIB 不能代理 final 类;
  • CGLIB 不能增强 finalprivate 方法。

第四,代理对象类型误判。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、自调用、缓存不严谨,都是代理模式的高频翻车点。