依赖注入容器能把对象创建从业务代码里移出去,动态代理能在入口外包一层横切逻辑。但这两个机制都不是设计原则本身。DIP 仍然看依赖方向,生命周期仍然看作用域,AOP 仍然只拦截真实经过代理的调用。

先对照手工组装

labs/E05 的 ManualGraph 接收一个 Counter,测试断言图里的对象和调用者传入的是同一个引用。随后用 Spring Framework 6.2.11 的 AnnotationConfigApplicationContext 加载 SpringGraph:singletonCounter 两次获取是同一对象,prototypeCounter 两次获取不是同一对象。

1
2
3
4
try (var context = Scenario.context()) {
assertSame(context.getBean("singletonCounter"), context.getBean("singletonCounter"));
assertNotSame(context.getBean("prototypeCounter"), context.getBean("prototypeCounter"));
}

这说明容器管理的是运行时对象生命周期。手工组装也能表达依赖方向;容器的收益是集中装配、配置 scope、接入大量协作者,而不是让代码自动符合 DIP。若一个类直接依赖低层 SDK,换成 @Bean 创建并不会改变这个依赖方向。

容器是组装机制,不是原则定义。先画清对象图和抽象边界,再决定由手工代码还是容器创建对象。

代理边界不等于方法边界

ProxyFactory 包装 Checkout 接口,拦截器每次执行就递增计数。调用 proxy.inner(100),计数变成 1;调用 proxy.outer(100),计数变成 2。outer 内部调用 this.inner(100),没有再次穿过代理,所以不会得到第 3 次拦截。

1
2
3
Scenario.Checkout proxy = Scenario.proxied(new Scenario.CheckoutService(), calls);
proxy.inner(100); // calls = 1
proxy.outer(100); // calls = 2, inner self-invocation is not intercepted
sequenceDiagram
    participant Client
    participant Proxy as JDK Proxy
    participant Target as CheckoutService
    Client->>Proxy: inner(100)
    Proxy->>Target: inner(100)
    Client->>Proxy: outer(100)
    Proxy->>Target: outer(100)
    Target->>Target: this.inner(100)

图里的两次 Client -> Proxy 会触发拦截器,Target -> Target 的自调用不会回到代理。

异常也不会因为 AOP 自动变成成功。proxy.inner(-1) 抛出 IllegalArgumentException,拦截器计数为 1。横切逻辑可以记录、计量、包裹事务,但不能无声吞掉领域失败;否则测试应明确证明这种转换是业务合同。

机制 本章证据 不能推出
Spring scope singleton/prototype 引用身份不同 单例对象线程安全
手工组装 传入对象保持同一身份 手工比容器更好
JDK 动态代理 外部接口调用被拦截 自调用也被拦截
AOP 异常路径 异常继续传播 切面可以替代领域校验

验证与练习

运行 ./mvnw -B -ntp -pl labs/E05 -am test;SpringContainerProxyTest 通过 4 个测试。原始日志在 examples/design-patterns/evidence/E05/targeted.stdout.txt。本章只使用 Spring Framework 6.2.11 的 spring-context 与 spring-aop,Spring Boot、事务管理器、CGLIB、真实数据库事务 NOT_RUN。

  1. 把 Checkout 改成无接口类,尝试 CGLIB 代理;先记录新增依赖和代理类型,再验证自调用是否被拦截。
  2. 给拦截器增加异常包装。先保留负金额异常的旧合同,再决定是否转换成业务异常;不能把失败订单写成成功收据。

参考资料:

上一节:E04 模式不证明线程安全;下一节:E06 从 DIP 到 Ports and Adapters。