Spring 容器读取 XML 和创建 Bean 是两个相连、但不能混为一谈的过程:前者把配置转换成 BeanDefinition,后者依据定义查找或创建对象。这篇笔记沿着阅读《Spring 源码深度解析》时关注的两条线展开,方法名以 Spring Framework 5.x 为背景。

从一个小例子找到入口

旧例子使用 XmlBeanFactory。它从 Spring 3.1 起就已弃用,可用 DefaultListableBeanFactory 搭配 XmlBeanDefinitionReader 表达相同的基本关系。XmlBeanFactory API

把下面的 beans.xml 放在应用 classpath 中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="message" class="java.lang.String">
        <constructor-arg value="hello"/>
    </bean>
</beans>

在有 Spring Beans 和 Core 依赖的工程中,入口可以写成:

import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;

public class XmlBeanDemo {
    public static void main(String[] args) {
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions(new ClassPathResource("beans.xml"));
        System.out.println(factory.getBeanDefinitionCount());
        System.out.println(factory.getBean("message", String.class));
        factory.destroySingletons();
    }
}

这是说明调用关系的最小示例。普通 BeanFactory 不会自动拥有 ApplicationContext 的所有扩展处理;例如,不能仅因为 classpath 有某个 BeanPostProcessor,就认为它已自动注册到这个 factory。

XML 如何变成 BeanDefinition

阅读 XmlBeanDefinitionReader 时,可以按下面的层次定位,而不必先记住每一个重载:

层次关注对象要回答的问题
资源Resource、EncodedResource配置来自哪里,是否指定字符集
输入InputStream、InputSource如何把资源交给解析器
文档DocumentLoader、DOM DocumentXML 如何解析,校验在哪里进行
定义BeanDefinitionDocumentReader标签如何转换为容器元数据
注册BeanDefinitionRegistry定义最终放到哪里

EncodedResource 是资源与编码信息的包装,不能理解为“无论原文件是什么编码都自动修复”。XmlBeanDefinitionReader 在加载期间还维护正在处理的资源集合,用于识别循环加载;这与两个 Bean 互相依赖是不同问题。XML 校验也没有因为外层方法变短就消失,仍需继续跟到 doLoadDocument 等实现。5.3.39 XmlBeanDefinitionReader 源码

文档层再区分默认命名空间和自定义命名空间。默认 beans 标签中的 import、alias、bean 以及嵌套 beans 各有处理分支;自定义命名空间则交给对应的 NamespaceHandler。profile 和嵌套 beans 的默认配置还会影响当前解析上下文。遇到不熟悉的标签,先看 namespace,再找处理器,比把所有标签都归给 parseBeanDefinitionElement 更容易跟对路径。

getBean 不是直接 new

getBean 的主体会进入 AbstractBeanFactory.doGetBean。读这段代码时,先区分“命中已有对象”和“需要创建对象”:

  1. 处理别名和名称前缀,得到容器使用的 beanName。
  2. 尝试单例缓存;命中后仍要处理 FactoryBean 的语义。
  3. 本地没有相应定义时,按条件委托父工厂。
  4. 合并定义,检查依赖关系,再按 singleton、prototype 或其他 scope 获取对象。
  5. 最后根据请求的类型处理返回值。

普通 FactoryBean 名称通常取得其产品,带 & 前缀则用于获取工厂本身。BeanDefinition 里的父定义合并与父 BeanFactory 委托也不是同一种“父子关系”。读源码时,把这两对概念分开,后续异常更容易解释。AbstractBeanFactory 源码

从 createBean 进入真正的创建流程

createBean 先处理类和方法覆盖等准备工作,还会给实例化前处理器返回替代对象的机会;因此不是每次都必然执行完整 doCreateBean。

进入 doCreateBean 后,可以用下面这条主线阅读:

createBeanInstance
  → 合并定义后处理
  → 在符合条件时准备早期引用
  → populateBean
  → initializeBean
  → 注册需要的销毁回调

createBeanInstance 也不只有无参构造器:Supplier、工厂方法、构造器自动装配和默认实例化都有自己的分支。BeanWrapper 包装实例与属性访问能力;它不是业务 Bean 的另一个副本。方法注入可能使用子类化策略,但不能据此说“所有 Bean 都是 CGLIB 代理”。

单例、允许循环引用、对象正在创建等条件满足时,容器可加入早期引用工厂。这不是提前创建第二个业务实例,更不意味着所有循环依赖都能解决。构造器循环依赖、prototype 循环依赖等情况不能套用同一结论;面对循环依赖,先改进依赖设计,而不是默认打开容错开关。创建流程源码

属性填充之后,初始化仍有多个阶段

populateBean 负责属性填充及相关扩展点;initializeBean 则涉及 Aware 回调、初始化前处理器、初始化方法、初始化后处理器等。业务对象可能在后处理阶段被包装,因此最终暴露对象不一定与最初实例具有相同的引用。

@PostConstruct 与 InitializingBean、init-method 的关系,另见初始化回调与后处理器顺序。将这些步骤统称为“构造函数执行完”会漏掉依赖注入和扩展处理。

这两条源码链最有用的连接点是 BeanDefinition:XML 阶段回答“容器知道了哪些定义”,getBean 阶段回答“这些定义如何成为可用对象”。排查时先确定错误发生在哪一侧,再定位具体扩展点。