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 框架十几年演进后得出的经验结论,而不是技术能力不足。
如果你在面试中被问到这个问题,推荐的回答结构是:
- 先说默认行为:ApplicationContext 默认 eager 实例化所有 singleton Bean
- 核心原因:Fail Fast + 单例唯一性 + 循环依赖处理 + AOP 等增强统一生效
- 权衡:启动时间 vs 运行时稳定性
- 最后补充:支持 @Lazy,但生产慎用全局懒加载
有具体场景想讨论懒加载的利弊、或者想看循环依赖在启动阶段是怎么被解决的,可以继续问~