注册一条定义,是否已经创建一个对象

一个订单服务依赖仓储和计价规则。手动装配时,构造器调用同时决定创建时点与依赖对象;改成 Spring 后,配置可以先记录“服务由哪个类、哪些依赖创建”,等配置处理完成再创建实例。BeanDefinition 承担的正是这段尚未执行的创建描述。

本篇检验一个问题:同一订单业务改用编程注册、XML 和配置类后,注册、实例化与名称解析是否仍是三件不同的事?实验固定 Spring Framework 6.2.11、JDK 21;实现依据固定到 4c134254642d88e058aa004bdaf44168e1be7bb2。同系列第 01 篇讨论谁拥有生命周期,本篇进一步区分对象的描述与对象本身。

三种声明进入定义注册表,创建实例后进入单例缓存;别名单独解析

图据固定版本的 DefaultListableBeanFactory、BeanDefinitionReaderUtils 和 ConfigurationClassBeanDefinitionReader 绘制。图中的单例缓存只表示 singleton 路径,prototype 不保留同样的共享实例。

定义保存创建条件,而非对象字段的快照

BeanDefinition 描述候选类、作用域、依赖关系、构造参数、属性值、初始化与销毁方法等信息。AbstractBeanDefinition 是常用实现的基础。它的构造参数集合可以装入 RuntimeBeanReference:此时保存的是待解析的 Bean 名称,还不是对应的 Java 引用。直到实际创建服务,容器才解析这个依赖并选取构造器。

这个间隔允许注册表后处理器补充定义,也允许工厂后处理器修改属性值。假设配置里先写了一个占位符,而注册动作已经立即创建对象,后处理器便无法在构造器执行前替换它。元数据与实例分离,使“先处理完整配置,再使用配置创建对象”成为可执行的顺序约束。BeanDefinition 契约

信息 描述的内容 不能据此推断的内容
beanClassName 初始类信息,可能是构造目标或静态工厂所在类 getBean 返回值一定具有这个具体类
构造参数 常量、引用或其他待解析值 所有依赖都已经实例化
factoryBeanName、factoryMethodName 实例工厂与创建方法 必须有产品类的 beanClassName
scope、lazyInit 重用规则与主动预实例化策略 单例天然线程安全,或懒加载对象绝不被提前依赖
resourceDescription、source 定义的定位线索 缺少来源描述就代表注册失败

DefaultListableBeanFactory.registerBeanDefinition 把定义保存到以 Bean 名为键的注册表,并维护名称集合与相关缓存。本例的启动前首次注册不会调用订单服务构造器。运行中覆盖既有定义还涉及重置与既有单例处理,不属于这个“首次注册”的实验结论,也不宜用它推导热更新方案。

模式提炼:描述与执行分阶段

参数化表达为 输入 → 描述({创建方式, 参数, 策略}) → 校验/改写 → 执行。

场景 描述 执行结果
Bean 装配 类、依赖、scope 对象及其依赖引用
SQL 处理 解析后的查询与计划 结果行与数据库副作用
构建任务 目标、依赖、命令 生成文件

排查“配置存在但效果没有发生”时,先分别确认描述是否注册、描述是否经过处理、执行是否发生。三个阶段的证据不能互换;注册表里的记录只能回答第一类问题。

三种输入收敛到创建机制,但元数据形态不同

实验的编程注册用 RootBeanDefinition 指定 TrackedOrderService,给两个构造参数填入命名引用,再把 orders 注册为 orderService 的别名。XML 用 class 和 <constructor-arg ref="…"/> 表达相同关系;@Configuration 中的 @Bean 则提供返回订单服务的工厂方法。

XML 读取器解析资源后注册定义。@Bean 的处理多了一层配置类解析:ConfigurationClassBeanDefinitionReader 把实例方法记录为“哪个配置 Bean 的哪个方法”,并标记工厂方法参数需要自动装配。三种方式可以创建行为相同的业务对象,却不应要求它们的 beanClassName、构造参数集合与来源字段逐项相等。工厂方法里的 new 和构造参数不会被 Spring 反编译后还原成另一条构造器定义。工厂方法定义的注册实现

这个区别也解释了来源信息。XML 定义通常能报告资源描述;配置类定义能携带方法与注解元数据;纯编程定义若没有显式设置来源,相关字段可以为空。定位工具应同时记录 Bean 名、创建方式、来源与工厂方法,不能只打印 getBeanClassName() 再把空值判成异常。

可复制实验

从本系列实验工程目录执行,Maven Wrapper 和依赖版本由第 00 篇冻结:

1
2
cd examples/spring-framework-lab
./mvnw -q compile exec:java -Dexec.mainClass=blog.spring.Chapter02

完整入口为 src/main/java/blog/spring/Chapter02.java,XML 为 src/main/resources/chapter02.xml;共用的 OrderService、OrderRepository、PricePolicy 和 Checks 位于同一工程。入口包含正常断言与反例,断言失败会抛出 AssertionError,不能只靠日志里有“创建成功”判断通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package blog.spring;

import java.math.BigDecimal;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.annotation.*;
import static blog.spring.Checks.*;

public class Chapter02 {
public static class TrackedOrderService extends OrderService {
static int created;
public TrackedOrderService(OrderRepository r, PricePolicy p) { super(r, p); created++; }
}
@Configuration public static class Config {
@Bean OrderRepository repository() { return new OrderRepository(); }
@Bean PricePolicy policy() { return new PricePolicy(); }
@Bean(name={"orderService", "orders"}) OrderService orderService(OrderRepository r, PricePolicy p) { return new TrackedOrderService(r, p); }
}
private static void verify(GenericApplicationContext context, String mode) {
context.refresh();
check(context.getBean("orders") == context.getBean("orderService"), "02 " + mode + " alias identity");
check(context.getBean(OrderService.class).place(2).equals(new BigDecimal("25.00")), "02 " + mode + " same order result");
}
public static void main(String[] args) {
TrackedOrderService.created = 0;
try (var programmatic = new GenericApplicationContext()) {
programmatic.registerBean("repository", OrderRepository.class);
programmatic.registerBean("policy", PricePolicy.class);
var definition = new RootBeanDefinition(TrackedOrderService.class);
definition.getConstructorArgumentValues().addIndexedArgumentValue(0, new RuntimeBeanReference("repository"));
definition.getConstructorArgumentValues().addIndexedArgumentValue(1, new RuntimeBeanReference("policy"));
programmatic.registerBeanDefinition("orderService", definition);
programmatic.registerAlias("orderService", "orders");
check(TrackedOrderService.created == 0, "02 definition registration does not instantiate");
check(definition.getConstructorArgumentValues().getArgumentCount() == 2, "02 definition records two constructor references");
var prototype = new RootBeanDefinition(OrderRepository.class);
prototype.setScope("prototype");
programmatic.registerBeanDefinition("prototypeRepository", prototype);
programmatic.registerAlias("prototypeRepository", "freshRepository");
verify(programmatic, "programmatic");
check(programmatic.getBean("prototypeRepository") != programmatic.getBean("freshRepository"), "02 alias preserves prototype scope");
check(definition.getResourceDescription() == null, "02 programmatic definition resource may be null");
}
try (var xml = new GenericApplicationContext()) {
new XmlBeanDefinitionReader(xml).loadBeanDefinitions("classpath:chapter02.xml");
check(TrackedOrderService.created == 1, "02 XML loading registers definitions only");
check(xml.getBeanDefinition("orderService").getResourceDescription().contains("chapter02.xml"), "02 XML definition records resource description");
verify(xml, "XML");
}
try (var config = new AnnotationConfigApplicationContext()) { config.register(Config.class); verify(config, "configuration");
check("orderService".equals(config.getBeanDefinition("orderService").getFactoryMethodName()), "02 Bean definition records factory method"); }
check(TrackedOrderService.created == 3, "02 three containers create three services");
System.out.println("CHAPTER 02 PASS");
}
}

本地重跑使用 Amazon Corretto 21.0.11,框架实际版本为 6.2.11,命令退出码为 0,末行是 CHAPTER 02 PASS。原始输出保存在工程的 evidence/02/local-20261002/run.txt。关键断言的观察结果如下。

场景 二值断言结果 解释
编程首次注册 服务创建计数等于 0 定义注册未调用业务构造器
XML 加载 服务累计计数仍等于 1 读取资源未额外创建服务
三种配置方式 订单结果均等于 25.00,别名身份均相同 在各自容器中得到相同业务行为
prototype 主名与别名 两次获取的引用不相同 别名不覆盖创建策略
来源与工厂元数据 编程来源为空、XML来源含资源名、配置工厂方法名匹配 三种输入保留不同的定位线索

这些断言把“结果相同”拆成金额、引用身份、创建时点和元数据四种观测。金额相同单独不能证明复用了同一个仓储或服务,类名相同也不能证明单例。

实例计数在本次运行开始时归零。加载 XML 前已有编程容器创建的一个服务,因此加载 XML 后计数仍为一,证明的是“本次读取没有额外实例化服务”,不是计数的绝对值必须为零。三个容器各自创建服务后,计数才达到三。跨容器的 singleton 不合并,它们各有自己的对象缓存。

scope 决定重用,alias 只改变查找名称

AbstractBeanDefinition 的默认 scope 字段是空字符串;在尚未继承或合并出其他 scope 时,空值按 singleton 解释。直接打印原始字段而只接受字符串 singleton,会误判默认定义。需要判断语义时使用 isSingleton()、isPrototype(),同时保留原始值用于定位。默认 scope 实现

singleton 的共享边界是相应容器中的这条定义,不是整个 JVM 里的某个 Class。把同一个类注册为两个独立 Bean 名通常会得到两个对象。与此相对,给一条定义增加 alias,只建立另一个名称到原名称的映射。BeanDefinitionReaderUtils 先注册主名称对应的定义,再分别注册 alias,并没有为每个别名复制一份定义。

因此,“主名和别名返回同一对象”还需要 singleton 这个条件。prototype 定义每次请求都走创建流程,通过主名请求一次、别名请求一次,仍可得到不同实例。别名没有覆盖作用域;它只把两个查找入口指向相同的创建规则。名称与别名注册

上游 DefaultListableBeanFactoryTests 的 prototype 测试对照默认单例与 prototype 的引用身份,aliasChaining 测试验证多级别名仍指向同一个单例。本篇阅读了这些断言;本地实验独立运行,不把上游测试文件的存在写成整套上游测试已通过。对应上游测试

模式提炼:名称解析与对象策略分离

参数化表达为 输入名称 → 规范名称 → {scope策略}(定义)。

场景 先解析什么 再决定什么
Bean alias 别名对应的 Bean 名 取缓存还是创建实例
文件路径别名 实际目标路径 打开方式与访问权限
服务逻辑名 路由后的目标 连接复用与请求执行

多个名字不必对应多个对象;相同目标也不保证两次请求得到相同对象。身份验证应在名称解析之后进行,并明确决定重用的那一层策略。

类型查询为何不能只读 class 字段

直接构造普通对象时,定义中的类与运行类经常相同,容易形成错误预期。实例工厂方法的产品类型来自方法返回信息;FactoryBean 的工厂类型与产品类型又不同;后续代理还可能改变容器暴露的实际类型。定义描述创建入口,最终引用经过创建与后处理,二者没有逐字段的一一对应关系。

诊断时优先向 BeanFactory 查询类型。如果需要尽量避免为了判断类型而初始化 FactoryBean,使用 getType(name, false) 并接受可能返回 null;这个参数不是“整个容器绝无任何副作用”的承诺。若业务已经需要该实例,可以对已取得的引用检查 getClass() 与接口,但这回答的是运行对象类型,不再是创建前的元数据问题。BeanFactory 类型查询契约

设计上的代价是:配置阶段保留更多弹性,类型判断就需要处理未知、工厂方法和代理等分支。注册元数据不能证明所有依赖都存在,也不能证明构造器一定能执行成功;这些错误可能直到 refresh 或第一次请求才暴露。第 03 篇将按启动阶段定位这些失败。

练习与解答

练习一。 同一个 RootBeanDefinition 描述的类分别以 ordersA、ordersB 两个主名注册,与注册 ordersA 后设置 ordersB 为别名,有什么不同?

解答:前者有两个名称对应的定义入口,默认会各自维护单例;后者只有一个目标定义,名称解析后共享该定义的 scope 规则。比较数量时同时查看定义名称集合与别名映射,比较实例时使用 ==,不要用类名相同替代身份相同。

练习二。 XML 和配置类实验返回相同订单金额,但配置类定义的构造参数数量不是二,能否判定注册遗漏了依赖?

解答:不能。配置类产品通过工厂方法创建,其参数由工厂方法解析,而不是复制 new TrackedOrderService(r, p) 中的参数到定义的构造参数集合。应检查工厂 Bean 名、工厂方法与创建后的依赖身份,再判断是否遗漏。

模式速查

观察 对应模式 下一项证据
定义存在,构造器尚未执行 描述与执行分阶段 请求与 refresh 是否触发创建
换别名后实例身份变化 名称与对象策略分离 目标定义的 scope
class 字段为空或与运行类不同 创建描述与最终引用分层 工厂方法、类型查询、后处理结果
配置文件不同但业务相同 输入格式收敛到执行协议 实际依赖与业务断言,不比较字段快照

参考资料

实验工程与环境准备见00:容器实验入口。继续阅读03:refresh 如何组织容器启动。