下面是一段使用 DDD 时常见的错误代码示例,你甚至可以在许多 DDD 的分享文章中看到类似用法:
// ❌ 错误示例:聚合根中直接获取领域服务
public class Order {
public void cancel() {
// 尝试获取领域服务发送通知
NotificationService service = ApplicationContext.getBean(NotificationService.class);
service.sendCancelNotification(this.id);
}
}
虽然DDD 不是本文关注的对象,但是这段代码实则本身是违反 DDD 设计原则的:
1.聚合根(领域对象)变成了“主动调用者”,违反了“聚合根不应依赖外部服务”的原则。
2.领域对象不应知道 Spring 容器的存在。
关于这段代码你可能会遇到一个极为常见的问题:在启动或停服的时候出现service 对象获取不到的异常。其缘由也比较好理解:由于 Spring 无法感知到 order 对象对 service 对象的依赖可能导致 运行至第5行时,service 对象已被销毁。
不推荐主动从 Spring 容器中获取 Bean( ApplicationContext.getBean()),应该是一个被广泛强调的规范。本文从将从多个维度 解释为什么不推荐主动从 Spring 容器中获取 Bean。
一、违背 Spring 的核心设计思想
1.1 控制反转(Inversion of Control, IoC)
- Spring 的核心思想是 控制反转:将对象的创建、依赖关系的管理、生命周期控制等“控制权”交给容器,而不是由程序员在代码中显式控制。
- 主动调用 getBean() 相当于 把控制权又拿回自己手里,破坏了 IoC 的初衷。
1.2 依赖注入(Dependency Injection, DI)
- Spring 通过 DI 实现松耦合:组件之间通过接口或抽象类协作,具体实现由容器注入。
- 使用 getBean() 是 “拉取式”依赖获取(Pull),而 DI 是 “推送式”依赖注入(Push)。
- “拉取”模式让组件知道容器的存在,增加了对框架的依赖。
二、破坏代码的可测试性
2.1 单元测试困难
- 如果代码中直接调用 ApplicationContext.getBean(),那么测试该方法时必须提供一个有效的 ApplicationContext。
- 这一般意味着必须启动整个 Spring 上下文(集成测试),而不是轻量级的单元测试。
- 单元测试的目标是 隔离被测代码,而 getBean() 引入了外部依赖(容器),破坏了隔离性。
2.2 Mock 难以实现
- 使用 DI 时,可以轻松用 Mockito 等工具 Mock 依赖。
- 而 getBean() 返回的是容器中真实的 Bean,难以替换为测试替身(Test Double)。
// 难以测试
public void process() {
UserService user = context.getBean(UserService.class);
user.save(...);
}
// 易于测试
public class MyService {
private final UserService userService;
public MyService(UserService userService) { this.userService = userService; }
public void process() { userService.save(...); } // 可直接传入 Mock
}
三、增加代码与 Spring 框架的耦合度
3.1 业务代码强依赖 Spring API
- 一旦你在业务类中引入 ApplicationContext,你的代码就与 Spring 框架绑定。
- 如果未来需要:
- 迁移到其他 IoC 容器(如 Guice、Micronaut)
- 在非 Spring 环境中运行(如 CLI 工具、测试脚本)
- 将模块解耦为独立库
- 都会由于 getBean() 调用而变得困难。
3.2 违反“关注点分离”原则
- 业务逻辑类应该只关注业务,而不应关心“如何获取依赖”。
- getBean() 把“依赖解析”逻辑混入了业务代码,违反了单一职责原则(SRP)。
四、破坏应用的分层架构和设计清晰度
4.1 绕过正常的依赖链
- 在典型的分层架构中(Controller → Service → Repository),依赖通过注入逐层传递。
- 如果某一层直接从容器中获取另一个 Bean,相当于“跳过”了正常的调用链,造成:
- 依赖关系不透明
- 调用路径难以追踪
- 架构混乱
4.2 隐藏真实依赖
- 通过构造函数或字段注入,类的依赖是 显式的(看构造函数就知道需要什么)。
- 而 getBean() 是 隐式的,阅读代码时无法一眼看出该类依赖哪些服务。
例如:一个类看起来没有依赖,但运行时却通过 getBean() 调用了数据库服务,这会让维护者困惑。
五、生命周期与作用域管理问题
5.1 可能获取到未完全初始化的 Bean
- Spring Bean 的初始化包括:
- 实例化
- 属性注入
- @PostConstruct 回调
- InitializingBean.afterPropertiesSet()
- 自定义 init-method
- 如果在容器尚未完成初始化时调用 getBean(),可能拿到一个“半成品” Bean,导致空指针或状态错误。
5.2 作用域(Scope)处理复杂
- 对于非单例 Bean(如 prototype、request、session),getBean() 的行为可能不符合预期。
- prototype:每次 getBean() 都会创建新实例,但你可能误以为是单例。
- request/session:在非 Web 环境下调用会抛异常。
- 而通过 DI 注入时,Spring 会自动处理作用域代理(如 @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS))。
六、性能与线程安全隐患(次要但存在)
6.1 getBean()有轻微性能开销
- 虽然 Spring 对 Bean 查找做了优化(如缓存),但相比直接调用字段或方法,仍有额外开销。
- 在高频调用场景下(如循环内),可能成为瓶颈。
6.2 线程安全问题(罕见)
- 如果错误地在多线程环境中缓存 ApplicationContext 并并发调用 getBean(),而 Bean 本身不是线程安全的(如 prototype 或有状态 Bean),可能引发并发问题。
- 而 DI 注入的单例 Bean 一般设计为无状态,更安全。
七、替代方案:何时可以“安全”使用 getBean()?
虽然不推荐,但在 极少数合理场景 下,可以接受使用 getBean(),但应 严格封装:
|
场景 |
说明 |
推荐做法 |
|
运行时动态选择实现类 |
如根据用户类型选择不同策略 |
使用策略模式 + 工厂类,工厂类通过 DI 获取 ApplicationContext,业务代码不直接调用 |
|
遗留系统或第三方回调 |
如 Servlet Filter、JDBC 回调等无法注入的上下文 |
通过 ApplicationContextAware 获取上下文,但仅限于基础设施层 |
|
工具类需要访问 Spring Bean |
如静态工具方法需调用 Service |
尽量避免;若必须,可持有静态 ApplicationContext 引用(但需注意初始化顺序和内存泄漏) |
✅ 最佳实践:将 getBean() 封装在专用工厂或服务中,业务代码永远不直接接触 ApplicationContext。
八、针对开头的错误示例的正确提议
通在ApplicationService中添加Service依赖注入,替代在聚合根中ApplicationContext.getBean()获取 bean。
这样的逻辑本身也遵守 DDD 原则:
领域对象(如聚合根)不应直接调用服务。
如果需要调用领域服务,应在 应用层 完成协调。
// 应用服务(Application Service)
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final NotificationService notificationService; // 通过 DI 注入
public void cancelOrder(String orderId) {
Order order = orderRepository.findById(orderId);
order.validateCanCancel(); // 领域逻辑在聚合根内
orderRepository.save(order);
notificationService.sendCancelNotification(orderId); // 应用层调用
}
}
优点:
- 依赖显式
- 易于单元测试
- 无框架耦合
- 符合 SOLID 原则
九、总结
核心理念:依赖应被给予,而非主动索取
除非有超级充分的理由,否则应始终坚持使用 依赖注入而非主动获取 Bean,多维度不提议的缘由:
|
维度 |
问题 |
|
设计原则 |
违背 IoC/DI,破坏控制反转 |
|
可测试性 |
难以单元测试,Mock 困难 |
|
耦合度 |
业务代码强依赖 Spring 容器 |
|
架构清晰度 |
隐藏依赖,破坏分层 |
|
生命周期 |
可能获取未初始化或错误作用域的 Bean |
|
可维护性 |
代码难以理解和重构 |
|
可移植性 |
无法脱离 Spring 环境运行 |


收藏了,感谢分享