为什么不推荐你在使用 Spring 时主动从容器中获取 Bean?

内容分享20小时前发布
0 1 0

下面是一段使用 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 环境运行

© 版权声明

相关文章

1 条评论

  • 头像
    微微草堂 读者

    收藏了,感谢分享

    无记录
    回复