设计模式 E05:依赖注入容器、动态代理与 AOP
依赖注入容器能把对象创建从业务代码里移出去,动态代理能在入口外包一层横切逻辑。但这两个机制都不是设计原则本身。DIP 仍然看依赖方向,生命周期仍然看作用域,AOP 仍然只拦截真实经过代理的调用。
先对照手工组装
labs/E05 的 ManualGraph 接收一个 Counter,测试断言图里的对象和调用者传入的是同一个引用。随后用 Spring Framework 6.2.11 的 AnnotationConfigApplicationContext 加载 SpringGraph:singletonCounter 两次获取是同一对象,prototypeCounter 两次获取不是同一对象。
1 | |
这说明容器管理的是运行时对象生命周期。手工组装也能表达依赖方向;容器的收益是集中装配、配置 scope、接入大量协作者,而不是让代码自动符合 DIP。若一个类直接依赖低层 SDK,换成 @Bean 创建并不会改变这个依赖方向。
容器是组装机制,不是原则定义。先画清对象图和抽象边界,再决定由手工代码还是容器创建对象。
代理边界不等于方法边界
ProxyFactory 包装 Checkout 接口,拦截器每次执行就递增计数。调用 proxy.inner(100),计数变成 1;调用 proxy.outer(100),计数变成 2。outer 内部调用 this.inner(100),没有再次穿过代理,所以不会得到第 3 次拦截。
1 | |
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。
- 把
Checkout改成无接口类,尝试 CGLIB 代理;先记录新增依赖和代理类型,再验证自调用是否被拦截。 - 给拦截器增加异常包装。先保留负金额异常的旧合同,再决定是否转换成业务异常;不能把失败订单写成成功收据。
参考资料:
- Spring Framework 6.2 Reference:Bean Scopes、AOP Proxies、Proxying Mechanisms。
- 本地实验:
examples/design-patterns/labs/E05/与examples/design-patterns/evidence/E05/RUN.md。

