Spring Bean初始化顺序:依赖类构造函数为何总先执行?
Spring容器启动时预创建所有非懒加载单例Bean,按依赖拓扑排序,无依赖的优先实例化,构造器先执行,随后通过setter方法注入依赖。获取Bean时仅从单例池返回引用,且所有初始化(包括属性赋值、Aware回调等)均在刷新阶段完成。
在Spring开发中,Bean的初始化和依赖注入顺序一直是开发者关注的重点。许多人会问:为什么依赖类构造函数总是先执行?关键在于,Spring容器启动时会预先创建并初始化所有非懒加载的单例Bean——这是容器生命周期机制决定的,与后续调用getBean()的顺序没有直接关系。
具体流程
以你的示例为例,A、B、C三个类都使用@Component标注,默认是单例模式,且未启用@Lazy。当执行new AnnotationConfigApplicationContext(LegendConfig.class)时,Spring容器启动,进入预实例化阶段。容器会扫描所有Bean定义,然后按依赖拓扑排序——先创建那些没有依赖或依赖已经就绪的Bean。
- A不依赖任何其他Bean,因此最先被实例化,调用
A()构造函数; - 接着创建B:调用
B()构造函数,此时B实例已经存在,但其属性a尚未赋值; - 然后执行
B.setA(a)——通过@Autowiredsetter注入,此时a已经是完全初始化好的A实例; - 同理,C的构造函数和
setA()也在容器启动阶段完成,而不是等到调用getBean(C.class)时才触发。
需要特别强调的是:applicationContext.getBean(B.class)并不会创建新实例,它只是从已构建好的单例池中返回引用。真正耗时的构造和注入过程,在上下文刷新(refresh())阶段已经全部完成。因此,你看到的输出顺序一定是:I am A constructor! → I am B constructor! → I am setA of B! → I am C constructor! → I am setA of C!。这印证了“构造先行、注入随后、获取即得”的全流程。
你可以通过以下配置验证这个行为:
@Configuration
@ComponentScan("com.spring.core.trialDI")
class LegendConfig {
@Bean(initMethod = "onInit")
public static class InitHook {
public void onInit() {
System.out.println(" All singleton beans initialized!");
}
}
}
注意事项
- 如果A本身依赖B,且两者都用构造器注入,那么Spring无法解决循环依赖,启动时会报
BeanCurrentlyInCreationException; - Setter注入(如本例中的
@Autowired方式)可以借助Spring的三级缓存机制解耦创建和注入,支持部分循环依赖场景; - 如果希望某个Bean延迟初始化,可以显式加上
@Lazy注解,这样一来它的构造函数会在首次getBean()时才执行。
总之,理解Spring的“启动期批量预创建 + 按需获取”模型,是掌握依赖注入时序逻辑的核心前提。记住,容器的初始化顺序基于依赖拓扑,而不是基于调用getBean()的顺序。
