Spring为什么启动时要实例化几乎所有的Bean?

Spring 在启动时(容器 refresh 阶段)默认会预先实例化(eagerly instantiate)几乎所有单例 Bean(singleton scope,默认 scope),而不是等到第一次被使用时才创建。这是 Spring IoC 容器设计的核心哲学之一,主要有以下几大原因。

1. 尽早暴露问题(Fail Fast & Early Detection)

这是最核心、最被广泛认可的理由。

  • 如果把 Bean 创建推迟到运行时第一次使用:
  • 配置错误(缺少依赖、循环依赖、@Autowired 找不到 Bean、数据库连接失败、第三方服务初始化异常等)会在业务高峰期才暴露
  • 可能导致线上请求突然雪崩、大量 500 错误、甚至整个集群挂掉
  • 而 Spring 默认在 容器启动阶段 就把所有单例 Bean 都创建出来:
  • 任何初始化阶段的异常(构造方法异常、@PostConstruct 异常、依赖注入失败、BeanPostProcessor 异常等)都会在启动时直接抛出
  • 开发/测试/CI 阶段就能发现问题,运维人员在上线后几秒到几十秒内就能看到容器是否健康
  • 这符合 Java 企业级应用“宁可启动失败,也不要运行时炸”的原则

一句话总结:把问题前置到启动阶段,而不是等到凌晨 2 点用户高峰期才暴露

2. 保证单例的线程安全与唯一性(Singleton 语义的严格保证)

  • Spring 的单例 Bean 是全局唯一线程安全共享的(默认情况下由容器保证)。
  • 如果采用“懒加载 + 并发创建”:
  • 高并发场景下,多个线程同时第一次 getBean(“xxx”),可能导致竞争条件(race condition)
  • 极端情况下可能出现多次实例化空指针不一致状态等问题
  • 而在启动阶段(单线程)把所有单例提前创建好,就天然避免了并发创建的复杂性
  • 后续所有 getBean 都只是从单例池(singletonObjects)里拿引用,极快且线程安全

3. 支持完整的依赖注入与循环依赖解析

  • Spring 允许循环依赖(A → B → A),但只支持单例范围内的 setter / 属性注入方式的循环(构造器注入循环依赖 Spring 默认不允许)
  • 循环依赖的解决依赖于“三级缓存”机制,而三级缓存的填充是在创建 Bean 的过程中完成的
  • 如果不提前创建 Bean,就无法在启动阶段完成所有依赖关系的校验和循环依赖的提前暴露
  • 提前实例化能让容器一次性把整个依赖图构建完整,发现问题(如循环依赖类型不兼容、@Lazy 误用等)

4. AOP、BeanPostProcessor 等增强机制需要在启动时全部生效

  • 很多核心功能(@Transactional、@Async、@Cacheable、自定义 BeanPostProcessor、BeanFactoryPostProcessor 等)都需要在 Bean 初始化阶段(甚至实例化前)介入
  • 如果 Bean 延迟创建,这些后处理器就无法在启动时统一应用,导致:
  • 部分 Bean 被代理,部分没被代理
  • 启动时不完整,运行时行为不一致
  • 提前全部实例化,能让所有增强逻辑在容器就绪前执行完毕

5. 启动性能 vs 运行时稳定性的权衡

很多人会问:“启动慢不是问题吗?能不能全部懒加载?”

  • Spring Boot 2.2+ 引入了 spring.main.lazy-initialization=true(全局懒加载),确实能大幅缩短启动时间(有时从 60s → 10s)
  • 但官方和社区强烈不推荐在生产环境默认开启全局懒加载,原因正是上面几点:
  • 问题后移 → 线上爆炸风险大增
  • 部分 Bean 启动时不创建 → 启动日志不完整,难以判断容器是否真的健康
  • 第一次请求慢(cold start) → 用户体验差,尤其在 serverless / 函数计算场景更明显

什么时候可以/应该使用懒加载?

可以,但要精准使用,而不是全局开启:

@Component
@Lazy              // 只懒加载这个 Bean
public class ExpensiveService { ... }

@Bean
@Lazy
public DataSource dataSource() { ... }

// 或者某个配置类整体懒加载
@Configuration(proxyBeanMethods = false)
@Lazy
public class HeavyConfig { ... }

典型适用场景:

  • 启动非常耗时的 Bean(大文件解析、连接远程服务、加载几十 MB 配置)
  • 某些只有特定条件才会用到的功能模块
  • 低峰期才用到的定时任务、批处理组件

总结:一句话记住

Spring 启动时实例化几乎所有单例 Bean,是为了“把所有可能出错的地方提前暴露,把运行时的不确定性降到最低”,牺牲一点启动时间,换取更高的稳定性和可观测性。

这是企业级 Java 框架十几年演进后得出的经验结论,而不是技术能力不足。

如果你在面试中被问到这个问题,推荐的回答结构是:

  1. 先说默认行为:ApplicationContext 默认 eager 实例化所有 singleton Bean
  2. 核心原因:Fail Fast + 单例唯一性 + 循环依赖处理 + AOP 等增强统一生效
  3. 权衡:启动时间 vs 运行时稳定性
  4. 最后补充:支持 @Lazy,但生产慎用全局懒加载

有具体场景想讨论懒加载的利弊、或者想看循环依赖在启动阶段是怎么被解决的,可以继续问~

文章已创建 4232

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部