☰
Spring IOC容器初始化源码解析:AnnotationConfigApplicationContext与refresh()全拆解
2026/10/15 21:47:41 网站建设 项目流程

Spring 的 IOC 容器,在注解驱动的时代,入口几乎都收敛到了AnnotationConfigApplicationContext这一个类上。不管你是写new AnnotationConfigApplicationContext(AppConfig.class),还是用AnnotationConfigApplicationContext("com.example")指定包路径,最后都会落到这个类的初始化逻辑上。很多人用 Spring 用了好几年,@Component、@Bean、@Configuration这些注解张口就来,但真要问一句“容器启动时这些 BeanDefinition 是怎么被收集起来的,refresh()里那十几个步骤到底干了什么”,能讲清楚的人并不多。

这篇文章就专门把AnnotationConfigApplicationContext的初始化链路拆开看。从构造函数开始,到register()注册配置类,再到refresh()里最核心的几大步,我会结合源码一行行说明白每一步为什么这么做、有哪些坑、以及调试时可以怎么验证。适合已经能熟练使用 Spring 但想真正走进源码层面的开发者,也适合准备面试时被问到“Spring IOC 容器初始化流程”这类问题的人。

1. 初始化前先搞懂设计骨架:继承体系藏着哪些设计意图

1.1 AnnotationConfigApplicationContext 到底继承了什么

先看类声明,这是理解一切的起点:

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry

GenericApplicationContext是 Spring 4.0 之后才引入的中间层,它本身继承了AbstractApplicationContext。这里要说清楚一件事:大多数文章会告诉你“IOC 容器就是DefaultListableBeanFactory”,这句话对,但不完整。DefaultListableBeanFactory是真正干活的BeanFactory实现,里面维护着beanDefinitionMap、singletonObjects这些核心数据结构。而GenericApplicationContext做的事,是在它内部持有并对外暴露这个DefaultListableBeanFactory,同时把BeanFactory的生命周期和ApplicationContext的生命周期绑定在一起。

为什么不让AnnotationConfigApplicationContext直接继承AbstractApplicationContext?因为AbstractApplicationContext里的refresh()流程是可复用的骨架,但每一种上下文获取BeanFactory的姿势不一样。比如ClassPathXmlApplicationContext是从 XML 里解析出 BeanDefinition 再注册,而注解方式是想让用户直接注册配置类、靠扫描机制收集 BeanDefinition。GenericApplicationContext在中间起了一个“适配”作用:它维护DefaultListableBeanFactory,至于 BeanDefinition 怎么进来,交给子类自己决定。

从类声明还能看到,这个类实现了AnnotationConfigRegistry接口,这个接口只声明了两个方法:register(Class<?>...)和scan(String...)。这个设计很妙,它把“注解配置上下文”对外暴露的能力收敛到了两个动作上:要么直接注册配置类,要么扫描指定包路径。后面所有复杂的初始化逻辑,都藏在这两个动作的背后。

1.2 构造器里两个成员变量的分工

打开这个类的字段定义,你会看到两个极其重要的成员:

private final AnnotatedBeanDefinitionReader reader; private final ClassPathBeanDefinitionScanner scanner;

reader负责把“注解标注的类”转换成BeanDefinition并注册到容器,它是配置类进入容器的第一道门。scanner负责扫描指定包路径下的所有类,找到符合条件的候选组件后,再走reader的逻辑完成注册。这两个对象在无参构造里被一起初始化,但这个“初始化”只是创建对象和注册一批内置后置处理器,并不立即扫描任何类。

这里有一个特别容易被忽略的细节:scanner在无参构造里被创建,但它默认的scan(String...)方法并不会在构造阶段自动执行。Spring 容器启动时,真正承担包扫描任务的是另外一个角色——ConfigurationClassPostProcessor,它会在refresh()阶段通过解析@ComponentScan注解来触发扫描,而不是直接用scanner。刚学源码的人经常在这里绕晕:明明创建了一个 scanner,结果扫描不是它干的?严格说,scanner是给编程式使用场景准备的,比如你写context.scan("com.example")时它才出场;而@ComponentScan注解驱动的扫描,走的是配置类解析流程,两者最终都会把扫描到的候选类变成 BeanDefinition,但触发机制完全不同。

1.3 为什么还要单独抽出 AnnotationConfigUtils

在AnnotatedBeanDefinitionReader的构造方法里,有一行关键的调用:

AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

AnnotationConfigUtils是一个静态工具类,它做的事情非常朴素:把 Spring 容器运行所必需的一批“基础设施 Bean”提前注册进容器。这些基础设施包括ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、EventListenerMethodProcessor、DefaultEventListenerFactory等。它们不是业务 Bean,而是让容器支持@Configuration、@Autowired、@EventListener这些注解能力的内置组件。

把这段逻辑抽到一个独立的工具类里,而不是写在 reader 内部,背后的考虑是复用。XML 时代的容器也需要注册这些后置处理器,在 XML 配置里你需要手写<context:annotation-config/>标签来触发注册;注解驱动时代,这一步被整合进了AnnotationConfigApplicationContext的构造过程。抽出工具类后,不管是注解入口还是 XML 入口,最终都能调用同一个registerAnnotationConfigProcessors方法,保证注册逻辑一致。

2. 构造函数链路:从 new 到 reader 就绪的全部细节

2.1 无参构造都做了什么

AnnotationConfigApplicationContext有多个重载构造方法,先看最核心的无参构造:

public AnnotationConfigApplicationContext() { StartStep step = new StartStep(this, ApplicationStartup.DEFAULT); this.reader = new AnnotatedBeanDefinitionReader(this); this.scanner = new ClassPathBeanDefinitionScanner(this); step.end(); }

两个关键点。第一,AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner的构造参数都传了this,也就是当前上下文自身。为什么要传自身?因为这两个工具类最终要把解析出来的 BeanDefinition 注册到一个BeanDefinitionRegistry上,而GenericApplicationContext本身就是一个BeanDefinitionRegistry的实现,它内部委托给持有的DefaultListableBeanFactory完成注册。

第二,这里会间接触发一次DefaultListableBeanFactory的创建。GenericApplicationContext的构造函数里执行了this.beanFactory = new DefaultListableBeanFactory()。所以当你new AnnotationConfigApplicationContext()的那一刻,一个全新的、空的BeanFactory就已经诞生了,只是里面还没有任何业务 BeanDefinition。

2.2 AnnotatedBeanDefinitionReader 内部四件事

AnnotatedBeanDefinitionReader的构造方法里其实藏了四层逻辑,我按执行顺序理一遍:

  1. 校验并保存BeanDefinitionRegistry引用。
  2. 获取或创建Environment对象,用于后续条件评估和属性解析。
  3. 创建ConditionEvaluator,这是处理@Conditional注解的核心评估器。
  4. 调用AnnotationConfigUtils.registerAnnotationConfigProcessors(registry),注册内置后置处理器。

第三点值得多说几句。ConditionEvaluator的存在意味着注解配置在“注册阶段”就会做一次条件判断。比如你在某个配置类上标注了@Conditional(SomeCondition.class),当doRegisterBean()执行到条件评估这一步时,它会读取@Conditional注解里声明的所有条件类,实例化并执行matches()方法;如果条件不满足,当前这个 BeanDefinition 直接丢弃,连注册都不会发生。这和@Profile的处理机制其实是同一套体系:@Profile本身就是@Conditional的一个具体实现。

2.3 ClassPathBeanDefinitionScanner 被创建的用意

这个 scanner 的构造过程比 reader 稍复杂一点,它要传入useDefaultFilters参数,默认值是true,也就是启用默认的扫描过滤器。默认过滤器匹配的是@Component注解及其派生注解,包括@Repository、@Service、@Controller、@Configuration。

我之前提过,@ComponentScan驱动的扫描并不直接用这个 scanner 对象,但编程式调用context.scan("com.example.pkg")时,走的就是它的scan()方法。这个方法内部会创建一个ClassPathScanningCandidateComponentProvider,按包路径找到所有 class 文件,加载为AnnotatedGenericBeanDefinition,再通过checkCandidate()方法判断是否已经注册、是否允许覆盖,最后注册进容器。

这里有一个实用经验:如果你在单元测试里想手动加载某个包下的组件,不一定要先定义配置类,直接new AnnotationConfigApplicationContext()然后context.scan("com.example")就能完成。但要注意,这种方式和@ComponentScan注解方式有个细微差别:编程式 scan 不会自动处理@Configuration类里的@Bean方法,因为这些解析动作是由ConfigurationClassPostProcessor在refresh()阶段完成的,而编程式 scan 只是把扫描到的类注册成了普通 BeanDefinition。你仍然可以手动调用refresh()触发后续处理,但入口场景不同,执行路径就有差异。

2.4 registerAnnotationConfigProcessors 注册了哪些关键组件

这一步注册的组件,几乎决定了注解驱动容器的所有核心能力。看代码片段最有说服力:

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors( BeanDefinitionRegistry registry, @Nullable Object source) { DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry); if (beanFactory != null) { if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) { beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); } if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) { beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver()); } } Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8); if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class); def.setSource(source); beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)); } // AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor... // EventListenerMethodProcessor、DefaultEventListenerFactory... }

ConfigurationClassPostProcessor是这里面最重要的一位。它是一个BeanDefinitionRegistryPostProcessor,在refresh()阶段的invokeBeanFactoryPostProcessors()中被调用,负责解析所有标注了@Configuration的类,处理@ComponentScan、@Import、@Bean方法等,是整个注解驱动 IOC 的核心引擎。

另外它还设置了两个基础设施:把DependencyComparator换成AnnotationAwareOrderComparator,这样@Order注解和Ordered接口就能影响自动注入时的候选顺序;把AutowireCandidateResolver换成ContextAnnotationAutowireCandidateResolver,这样@Lazy、@Value这些注解才能在字段注入、构造器注入时被正确解析。

3. register() 注册配置类:BeanDefinition 是怎么进来的

3.1 register 调用链与包裹逻辑

来看register方法的源码:

@Override public void register(Class<?>... componentClasses) { Assert.notEmpty(componentClasses, "At least one component class must be specified"); this.reader.register(componentClasses); // 版本差异:部分版本会在 register 后触发 early 初始化, // 但核心逻辑都是交给 reader.register 去处理 }

它做的事情非常纯粹:参数校验后,原样委托给AnnotatedBeanDefinitionReader.register()。reader.register()又做了一层包装:

public void register(Class<?>... componentClasses) { for (Class<?> componentClass : componentClasses) { registerBean(componentClass); } } public <T> void registerBean(Class<T> beanClass) { doRegisterBean(beanClass, null, null, null, null); }

到这里,还没有真正生成 BeanDefinition,只是把Class对象传给了doRegisterBean()。所有的处理和注册都发生在那个方法里。

3.2 AnnotatedGenericBeanDefinition 与元数据的装载

doRegisterBean()的第一行代码:

AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);

这是一个非常关键的类型。它和 XML 时代常用的GenericBeanDefinition不同,AnnotatedGenericBeanDefinition不仅保存了 Bean 的类信息,还持有AnnotationMetadata。所谓AnnotationMetadata,就是通过 ASM 读取 class 字节码得到的“注解元数据”,它可以在不实例化类的情况下,查询目标类上标注了哪些注解、注解的属性值是什么、类的访问修饰符是什么。

这里有个值得一提的细节:为什么用 ASM 而不是反射?因为 Spring 在判断一个类是不是配置类、有没有@Component注解时,可能涉及几十上百个类的扫描,如果用反射把每个类都加载进 JVM,会产生巨大的类加载成本和额外的内存开销。ASM 直接读字节码,效率高得多,而且有些候选类可能因为条件不满足最终根本不会被实例化。

3.3 doRegisterBean 完整执行流程

继续往下追踪,完整版本的doRegisterBean方法结构如下:

private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] beanDefinitionCustomizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } abd.setInstanceSupplier(supplier); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); if (qualifiers != null) { for (Class<? extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } } if (beanDefinitionCustomizers != null) { for (BeanDefinitionCustomizer customizer : beanDefinitionCustomizers) { customizer.customize(abd); } } BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); }

逐个环节说明。

先执行条件评估。如果类上的@Conditional条件不满足,整个注册直接中止。这里说的“不满足”指的是shouldSkip()返回true,注意条件是“shouldSkip”,它的语义是:条件评估不通过,就跳过这个 Bean。如果类上没有@Conditional注解,默认不跳过。

然后是设置实例供应商。supplier允许你传入一个Supplier<T>回调,替代常规的反射实例化。这是 Spring 5.0 引入的函数式 BeanDefinition 注册方式,在AnnotationConfigApplicationContext.registerBean()的重载中可以传入 Supplier,比如context.registerBean(MyService.class, () -> new MyService("xxx"))。这种方式最大的好处是绕过了反射构造器调用,对于需要自定义构造参数或做复杂初始化的场景非常方便。

接下来是解析作用域。scopeMetadataResolver默认实现是AnnotationScopeMetadataResolver,它会扫描类上的@Scope注解,读取 value 属性决定是singleton、prototype还是自定义 scope。如果没标注@Scope,默认是singleton。这里要注意,@Scope不只影响单例或多例,它还支持scopedProxyMode,也就是代理模式。如果你设置proxyMode = ScopedProxyMode.TARGET_CLASS,Spring 会为这个 Bean 生成一个代理对象注册到容器里,实际依赖注入的是代理。AnnotationConfigUtils.applyScopedProxyMode()就是处理这个的。

接着是生成 BeanName。默认的beanNameGenerator是AnnotationBeanNameGenerator。它的规则可以概括为:如果类上有@Component("name")等显式指定名称的注解,就用注解里的 value 值;否则把类名首字母小写作为默认 beanName。有一个细节容易踩坑:如果你注册的配置类没有显式命名,默认 beanName 就是配置类的简单类名首字母小写,但这个 beanName 只影响配置类自身作为 Bean 的名称,不影响它内部@Bean方法的名称。

然后是补全通用注解的元数据:

AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);

这个方法会检查类上的@Lazy、@Primary、@DependsOn、@Role、@Description等注解,并把对应的属性写入 BeanDefinition。比如类上标了@Primary,这个 Bean 在多个候选注入时会被优先选择。

最后是注册动作。BeanDefinitionReaderUtils.registerBeanDefinition()内部会调用DefaultListableBeanFactory.registerBeanDefinition(),把BeanDefinition存进beanDefinitionMap里,同时维护beanDefinitionNames这个有序列表。这里有一个关于覆盖行为的细节:如果容器里已经存在同名的 BeanDefinition,会走allowBeanDefinitionOverriding的判断逻辑,默认允许覆盖,只是会打印一条日志。在某些严格场景下(比如外部化配置不允许重复定义),可以把这个开关关掉。

3.4 ConditionEvaluator 的条件装配评估

条件评估是注解驱动容器一个很有代表性的能力,值得单独拆开理解。ConditionEvaluator.shouldSkip()的大致逻辑是:

public boolean shouldSkip(AnnotatedTypeMetadata metadata) { return shouldSkip(metadata, null); } public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } // 收集 @Conditional 注解中声明的所有 Condition 类 // 排序后依次实例化并执行 matches() // 如果其中一个返回 false,则 shouldSkip 返回 true }

Spring Boot 的@ConditionalOnClass、@ConditionalOnProperty等注解,本质上就是在@Conditional的基础上做了一层封装,最终走的都是这同一套ConditionEvaluator评估链路。理解这一点,你就会明白为什么配置类上的条件判断是在 BeanDefinition 注册阶段就生效的——它根本没给你创建 Bean 的机会,条件不满足时连 BeanDefinition 都不会进入容器。

4. refresh() 才是真正的主角:12步初始化全拆解

4.1 prepareRefresh + obtainFreshBeanFactory

回到AbstractApplicationContext.refresh()。如果你之前只学过概念,没有真正对照过源码,可以先把整段结构看一遍:

@Override public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // 准备刷新,设置启动时间、活跃状态、初始化属性源 prepareRefresh(); // 获取刷新后的 BeanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 给 BeanFactory 补充容器级基础设施 prepareBeanFactory(beanFactory); try { // 子类扩展点:允许在 BeanFactory 已准备但 Bean 创建前修改 postProcessBeanFactory(beanFactory); // 执行 BeanDefinitionRegistryPostProcessor 和 BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(beanFactory); // 注册 BeanPostProcessor,等待 Bean 创建时发挥作用 registerBeanPostProcessors(beanFactory); // 初始化 MessageSource,支持国际化 initMessageSource(); // 初始化事件广播器 initApplicationEventMulticaster(); // 子类扩展点:刷新时初始化其他特殊 Bean onRefresh(); // 注册事件监听器 registerListeners(); // 实例化所有非懒加载的单例 Bean finishBeanFactoryInitialization(beanFactory); // 完成刷新,发布 ContextRefreshedEvent 事件 finishRefresh(); } catch (BeansException ex) { destroyBeans(); cancelRefresh(ex); throw ex; } finally { resetCommonCaches(); } } }

prepareRefresh()做的事情比较轻量:设置容器的启动时间、激活状态、关闭状态,初始化属性源(PropertySource),并准备一批用于审计的早期事件。它不会创建任何 Bean,但它是整个刷新过程中的“状态开关”。

obtainFreshBeanFactory()在GenericApplicationContext里的实现几乎等于一个空操作,因为它继承的GenericApplicationContext在构造函数里已经创建好DefaultListableBeanFactory了,这里只是把它包装一层返回。但如果你是跟着ClassPathXmlApplicationContext的路径走,这个方法会重新创建 BeanFactory 并加载 XML 中的所有 BeanDefinition。两种上下文在这里的分叉,正好验证了前面说的“GenericApplicationContext适配不同 BeanDefinition 来源”的设计意图。

4.2 prepareBeanFactory 里那些容易被忽略的配置

prepareBeanFactory是给刚创建的 BeanFactory 做“基础环境配置”的步骤,它做的事足够冷门但非常重要。核心代码大致如下:

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { beanFactory.setBeanClassLoader(getClassLoader()); beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver()); beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); beanFactory.ignoreDependencyInterface(EnvironmentAware.class); beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); beanFactory.ignoreDependencyInterface(MessageSourceAware.class); beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); beanFactory.registerResolvableDependency(ApplicationContext.class, this); beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); // ... 其他组件 }

这里面有一个面试高频考点:为什么注册ApplicationContextAwareProcessor的同时要ignoreDependencyInterface一堆 Aware 接口?原因是 Spring 在完成@Autowired字段注入时,会自动寻找所有可注入的依赖类型;如果不排除那些 Aware 接口,那么一个实现ApplicationContextAware的 Bean 在提取注入点时,就可能被误判为“需要注入 ApplicationContext 类型的依赖”。这会造成重复处理。正确路径是:ApplicationContextAwareProcessor作为一个BeanPostProcessor,在 Bean 实例化后、初始化前专门负责回调 Aware 方法的赋值逻辑,而普通依赖注入阶段则跳过这些接口。

还有一行容易被忽略:

beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);

这行代码的效果是:当你的业务类里声明了@Autowired private BeanFactory beanFactory;,Spring 能直接注入当前容器内部的DefaultListableBeanFactory。类似的,ApplicationContext、ResourceLoader、ApplicationEventPublisher也能被直接注入,因为AnnotationConfigApplicationContext本身就是这些接口的实现类。

4.3 invokeBeanFactoryPostProcessors 与 ConfigurationClassPostProcessor 的高光时刻

这一步是整个注解驱动初始化流程中最精彩、最复杂的一段。invokeBeanFactoryPostProcessors的逻辑需要区分两种处理器:

  • BeanDefinitionRegistryPostProcessor:它本身也是BeanFactoryPostProcessor,但多了一个postProcessBeanDefinitionRegistry()方法,可以有意识地修改 BeanDefinition 注册表。
  • 普通BeanFactoryPostProcessor:只提供postProcessBeanFactory()方法,可以在 Bean 实例化前修改 BeanFactory 的配置。

执行顺序有讲究:先执行所有BeanDefinitionRegistryPostProcessor,再执行普通BeanFactoryPostProcessor。而在BeanDefinitionRegistryPostProcessor内部,又分成“先执行实现了PriorityOrdered的,再执行实现了Ordered的,最后执行剩余的”。

ConfigurationClassPostProcessor就是一个优先级极高的BeanDefinitionRegistryPostProcessor,它实现了PriorityOrdered,所以在这一轮会被最先执行。它内部最关键的方法是processConfigBeanDefinitions():

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { List<BeanDefinitionHolder> configCandidates = new ArrayList<>(); String[] candidateNames = registry.getBeanDefinitionNames(); // 遍历所有 BeanDefinition,找出配置类候选 for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (ConfigurationClassUtils.isFullConfigurationCandidate(beanDef.getMetadata()) || ConfigurationClassUtils.isLiteConfigurationCandidate(beanDef.getMetadata())) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } // 没有配置类候选则直接返回 if (configCandidates.isEmpty()) { return; } // 排序,初始化 ConfigurationClassParser // 解析每个配置类:处理 @ComponentScan、@Import、@ImportResource、@Bean 方法 // 每解析出一个新的 BeanDefinition,加入容器后继续迭代解析 // 直到没有新的配置类产生 }

ConfigurationClassParser是真正干活的组件。它会遍历每个配置类上的所有注解,如果发现@ComponentScan,就调用扫描逻辑扫描指定包路径下的候选类;如果发现@Import,把导入的类也当作配置或普通组件处理;如果类中有@Bean方法,为每个@Bean方法生成一个BeanMethod对应的 BeanDefinition。解析过程中产生的所有新 BeanDefinition 都会被注册进容器,然后继续迭代解析,直到没有新的配置类产生。

这里给想调试源码的人一个建议:在ConfigurationClassParser.doProcessConfigurationClass()方法里打断点,你能亲眼看到每一个@ComponentScan是如何被展开成成百上千个 BeanDefinition 的。对应关系非常直观。

4.4 registerBeanPostProcessors 与后置处理器注册

注意这一步和上一步的名称区别:上一步是在“调用”BeanFactoryPostProcessor,这一步是在“注册”BeanPostProcessor。BeanPostProcessor负责的是 Bean 实例化之后、初始化前后的拦截逻辑,典型代表是AutowiredAnnotationBeanPostProcessor——@Autowired的注入就是它完成的。

Spring 会把容器中所有实现了BeanPostProcessor接口的 Bean 先实例化出来,再按优先级注册进beanFactory的beanPostProcessors列表。这里有个先后顺序的陷阱:BeanPostProcessor必须在普通 Bean 创建之前注册完毕,否则它对 Bean 生命周期的拦截能力就会失效。这也是为什么registerBeanPostProcessors被放在finishBeanFactoryInitialization之前。

还有一个冷知识:如果你自己定义了一个类实现了BeanPostProcessor,但同时又依赖了其他普通 Bean 来完成初始化,这时要格外小心——BeanPostProcessor会被提前实例化,它依赖的那些 Bean 也会被提前创建,这可能会打破你原有的初始化顺序预期,甚至引发循环依赖。源码注释里其实明确写了“BeanPostProcessor 不应该依赖于未初始化的 Bean”。

4.5 finishBeanFactoryInitialization:单例预实例化与循环依赖

这一步的名场面是“预实例化所有非懒加载的单例 Bean”。DefaultListableBeanFactory.preInstantiateSingletons()的流程大致是:

public void preInstantiateSingletons() throws BeansException { List<String> beanNames = new ArrayList<>(this.beanDefinitionNames); for (String beanName : beanNames) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { if (isFactoryBean(beanName)) { // 先处理 FactoryBean 本体的创建 } else { getBean(beanName); } } } // 所有单例 Bean 创建完成后,触发 SmartInitializingSingleton 回调 for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { ((SmartInitializingSingleton) singletonInstance).afterSingletonsInstantiated(); } } }

getBean()之后会进入doGetBean(),然后经过createBean()→doCreateBean()的漫长链路。在这个链路里,有一个核心机制值得单独说:循环依赖的三级缓存。

DefaultSingletonBeanRegistry维护了三个Map:

  • singletonObjects:一级缓存,保存已经完成初始化的单例 Bean。
  • earlySingletonObjects:二级缓存,保存早期暴露的 Bean 引用,这个 Bean 已经实例化但还没完成属性填充。
  • singletonFactories:三级缓存,保存ObjectFactory<?>,可以在需要时提前生成早期引用。

处理循环依赖的思路可以这样理解:A 依赖 B,A 先创建,发现需要 B;B 创建过程中又依赖 A;此时容器把 A 刚实例化完的“半成品”提前暴露出来给 B 用,等 B 创建完成后 A 再拿到完整的 B。三级缓存存在的必要性,是为了处理“属性填充发生在循环依赖场景下”的时序问题。

这里必须强调一个限制:Spring 的循环依赖解决机制只对“单例 + 非构造器注入”的场景有效。如果是构造器注入的循环依赖,直接抱团失败;如果是原型作用域的循环依赖,也没有缓存可用,直接报错。网上很多“Spring 支持循环依赖”的说法其实过于绝对,准确的说法是“Spring 通过三级缓存可以解决部分单例作用域下的循环依赖”。

4.6 finishRefresh:上下文刷新收尾的收尾

要确保对初始化流程的认知完整,finishRefresh()这一步也值得理解。它主要做了四件事:

  • 清空ResourceLoader缓存,确保后续资源解析拿到的是最新数据。
  • 初始化生命周期处理器LifecycleProcessor。
  • 调用getLifecycleProcessor().onRefresh(),通知所有SmartLifecycle组件启动。
  • 发布ContextRefreshedEvent事件。

最后这一条在业务中的意义很大。如果你在代码里监听ContextRefreshedEvent,它会在容器完整刷新后触发,适合做一些“所有 Bean 都就绪后”的启动后置动作,比如缓存预热、数据初始化、定时任务注册等。

5. 注解方式与 XML 方式的路程差异

5.1 入口不同但终点一致

很多从 XML 时代过来的开发者总觉得注解方式和 XML 方式是完全不同的两套实现。实际上,两种方式共用了同一个AbstractApplicationContext.refresh()骨架,最后的 BeanFactory 都是DefaultListableBeanFactory,所有 Bean 生命周期机制(BeanPostProcessor、BeanFactoryPostProcessor、事件发布)完全相同。不同的只是 BeanDefinition 的收集来源:

  • XML 方式:XmlBeanDefinitionReader读取 XML 文件,遇到<bean>标签就构建一个GenericBeanDefinition注册进容器。
  • 注解方式:AnnotatedBeanDefinitionReader注册配置类,再由ConfigurationClassPostProcessor递归解析出所有 BeanDefinition。

两者注册进容器之后的命运完全相同:都要经过invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization这三大阶段。

5.2 注解驱动多出来的一层“配置解析”

注解方式比 XML 方式多出来的核心环节,就是“配置类解析”。XML 里你显式写了每一个 Bean,信息是完整的、静态的;而注解方式里你只给了容器一个配置类入口,容器需要自己去扫注解、找@Bean方法、处理@Import和@Conditional,这是一个动态的、递归的解析过程。ConfigurationClassPostProcessor和ConfigurationClassParser就是为这一层解析而生的。

这也是为什么注解驱动的启动过程通常比 XML 略慢——不是慢在反射创建 Bean,而是慢在配置解析阶段的扫描和注解元数据读取。在 Bean 数量足够大时,这个差异会更明显。

6. 常见问题与排查技巧实录

6.1 配置类没有被代理

一个常见的怪问题:明明在配置类里定义了两个@Bean,其中一个调用了另一个@Bean方法,结果每次调用都 new 出了新对象,单例失效。

原因很可能是配置类只加了@Component而不是@Configuration。@Configuration类在解析时被标记为“full 配置类”,Spring 会为它生成 CGLIB 代理,@Bean方法之间的调用走的是代理对象,从而保证单例语义;而“lite 配置类”(比如标注了@Component、@Import但没标@Configuration的类)不会被代理,方法调用就是普通 Java 调用,每次都会执行方法体,导致@Bean方法内的对象重复创建。

排查方法很简单:在ConfigurationClassUtils.isFullConfigurationCandidate()和isLiteConfigurationCandidate()两个方法上打断点,观察你的配置类走的是哪条判断分支。

6.2 包扫描不生效

使用context.scan("com.example.pkg")时扫描不到任何 Bean,大概率是包路径写错,但有时候包路径完全正确还是不生效。这时候先确认你的pom.xml或构建配置里有没有引入spring-context依赖;再确认目标包里的类是不是真的标了@Component或其派生注解。还有一个很隐蔽的原因:类被编译后的目录中不包含对应注解的CLASS文件,比如被某些插件过滤了。

经验建议:真调试时在ClassPathScanningCandidateComponentProvider.findCandidateComponents()里打断点,看它扫描的Resource列表到底覆盖了哪些路径,可以在classpath*相关的加载逻辑处直接看到结果。

6.3 单例容器里的循环依赖

项目里一旦出现BeanCurrentlyInCreationException,说明循环依赖没被解决。先不要急着加@Lazy,先判断是构造器注入还是 setter/属性注入。如果是构造器注入的循环依赖,最靠谱的方案是重构设计,把依赖关系改成 setter 注入或使用@Lazy打破循环。如果一定要保留构造器注入,@Lazy会让 Spring 先注入一个代理对象,等真正调用时才去获取目标 Bean,但这个做法会增加一层隐式延迟,别滥用。

另外,Spring Boot 2.6 之后默认关闭了循环依赖支持,如果你在 Boot 项目中遇到循环依赖异常,还需要检查配置项spring.main.allow-circular-references有没有被显式打开。

6.4 调试源码的三个建议

最后分享三个我实际调试 Spring 源码时觉得很有用的习惯。

第一,不要直接在refresh()的入口点一路单步走到底,那样会迷失在成千上万的调用栈里。更好的方式是在关键节点打断点,比如ConfigurationClassParser.doProcessConfigurationClass()、doCreateBean()、invokeBeanFactoryPostProcessors(),然后结合beanDefinitionMap.size()的变化来观察容器状态。

第二,利用beanDefinitionNames列表观察注册顺序。在DefaultListableBeanFactory.registerBeanDefinition()方法里打断点,看一眼beanDefinitionNames的顺序,你会直观感受到配置类解析的先后顺序。很多“为什么我的 Bean 先执行后执行”的困惑,看这个列表基本就能解开。

第三,注意 Spring 版本的差异。不同版本里AnnotationConfigApplicationContext内部代码有出入,比如很多版本会在构造阶段调用StartStep做启动性能记录,但在 5.0 之前的版本根本没有这些逻辑。调试时以你本地依赖的源码版本为准,理论上相近版本之间核心流程差异不大。

我自己在阅读这套源码时,最大的体会是:不要把 IOC 容器想象成什么高深莫测的东西,它本质上就是一个强化版的Map<String, Object>,只不过这个 Map 在创建对象的过程中加入了复杂的生命周期管理。AnnotationConfigApplicationContext初始化链路的每一步,都是在往这个 Map 里填充数据或增强未来填充数据的能力。搞懂了这条链路,Spring 的大半个世界基本就打开了一半。剩下的那些 AOP、事务、事件机制,都建立在这条初始化链路的框架之上,理解起来会顺畅得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询