查询名称相同,得到的对象却不同

注册一个名为 product 的 FactoryBean<Product> 后,getBean("product") 返回产品,getBean("&product") 返回工厂。工厂可能已经存在,产品却尚未创建;产品被取得一次之后,后续查询又可能直接命中产品缓存。只在 Product 构造器打断点,会漏掉工厂实例的生命周期。

订单应用可能通过构造器创建普通服务,通过静态方法适配第三方对象,通过实例工厂生成业务组件,再用 lazy 或 provider 控制取得时点。这些方式分别改变“如何创建”和“何时请求创建”,两组选择可以组合。工厂方法本身也可以带依赖参数,仍需使用第 05 篇讨论的依赖解析。

本文固定 Spring Framework 6.2.11、JDK 21,源码提交为 4c134254642d88e058aa004bdaf44168e1be7bb2。完整实验在仓库 examples/spring-framework-lab/src/main/java/blog/spring/Chapter06.java。它用实例身份、Product 构造计数和 FactoryBean 调用计数区分以下状态。

场景输入 需要观察的结果
普通构造器、静态工厂、实例工厂各一个 eager singleton refresh 后创建三个产品
普通 singleton FactoryBean 工厂可先创建;取得产品时才调用 getObject
FactoryBean 返回 singleton 产品 容器重复查询返回同一缓存结果
工厂声明产品不单例,且每次 getObject 分配新对象 两次查询得到不同产品
lazy singleton 与 prototype provider 首次请求创建 lazy;prototype 每次取得可新建

定义、工厂实例与产品缓存及其触发关系

创建入口先读取定义采用哪种策略

AbstractAutowireCapableBeanFactory.createBeanInstance() 根据 BeanDefinition 分派实例化方式。如果定义提供实例 supplier 且没有显式实参,先走 supplier;设置了 factory method 则使用工厂方法;否则考虑已缓存的构造器解析结果、后处理器给出的候选构造器、构造自动装配和显式构造参数,最后才是普通无参实例化。创建入口源码

因此,运行时没有进入 ConstructorResolver.autowireConstructor(),并不说明对象没有交给容器管理。已有 supplier 或 factory method 可能改变了入口。容器先决定实例从哪里取得,之后还会继续属性填充和初始化等阶段;构造结束只说明已有实例,不代表整个 Bean 创建流程完成。

构造器候选也并非由 ConstructorResolver 单独发现。前面的 Bean 后处理器可以提供候选,例如注解驱动的构造器选择。分析多构造器类时,需要同时看候选来源和候选之间的实参匹配,不能从类里“参数最多的构造器”直接推导最终结果。

构造器解析如何从签名走到调用

ConstructorResolver.autowireConstructor() 先读取可能缓存的可执行成员与参数信息。未解析时对候选排序,检查最少参数数量,为候选创建实参数组。参数可以来自定义中的显式值,也可以经类型转换或依赖解析取得。某个候选无法满足依赖时会记录原因,并尝试后续适用候选。

能够满足参数的候选仍需比较类型差异权重。源码在已经找到可满足的更“贪心”构造器时,可以跳过参数更少的后续候选;严格解析配置下,不能区分的构造器会导致歧义异常。这比“Spring 永远选择参数最多的构造器”多了候选资格、依赖可满足性与类型匹配三个条件。构造器和工厂方法解析

确定成员后,定义里会缓存 resolvedConstructorOrFactoryMethod 等解析结果。有些参数已经解析,有些保留 prepared 形式,后续创建时再次解析。这解释了 prototype Bean 为什么可以复用构造器选择结果,却仍为每次请求创建新实例、取得相应依赖:可执行成员缓存和实例缓存保存的是不同东西。

上游 DefaultListableBeanFactoryTests 的 autowireWithSatisfiedConstructorDependency、autowireWithTwoMatchesForConstructorDependency 和 autowireWithUnsatisfiedConstructorDependency 分别覆盖依赖可满足、存在歧义和缺失的构造情况。本文阅读了这些测试;本地最小工程执行的是本文列出的路径,并未运行 Spring 的整套上游构建。上游构造与工厂测试

工厂方法与 FactoryBean 是两种机制

实验的静态工厂使用 Product 类的 make() 方法;实例工厂则登记一个 Maker,并把产品定义的 factoryBeanName 设为 maker、factoryMethodName 设为 make。这个字段名中的 factory bean 指“持有实例方法的对象”,不要求 Maker 实现 FactoryBean 接口。

instantiateUsingFactoryMethod() 在实例方法路径先通过容器取得 Maker,并登记依赖关系;静态方法路径使用类,不需要持有者实例。之后按方法名称、静态条件与参数匹配解析可调用的方法,最终通过实例化策略执行。参数解析仍会遇到缺失和歧义,所以改成 @Bean 工厂方法不能自动绕过依赖冲突。

FactoryBean<T> 则改变容器对已取得对象的暴露语义。AbstractBeanFactory.getObjectForBeanInstance() 检查查询名是否有 &:有则要求实际对象是 FactoryBean 并直接返回;没有且实际对象实现 FactoryBean,则转入产品取得。普通对象直接返回。给一个普通 Maker 的名字加 & 不会将它变成这种特殊工厂,而会因为对象不是 FactoryBean 抛出异常。对象暴露实现

模式提炼:分别追踪定义、生产者与产品

状态可以写成 定义 → 工厂实例 → 产品实例,而普通 Bean 的路径可以省去中间工厂。对象身份、创建次数、缓存位置和失败阶段应分别记录。在连接工厂、客户端适配器和代理生产器中,仅观察最终产品都会漏掉生产者初始化带来的成本和副作用。

定义缓存回答采用哪种创建规则;singleton 工厂缓存回答是否复用生产者;FactoryBean 产品缓存回答是否复用暴露结果。把三个缓存合并称为“Bean 已缓存”,无法解释工厂计数为一而产品调用仍为零的状态。

产品缓存由谁负责

FactoryBeanRegistrySupport.getObjectFromFactoryBean() 只有在 factory.isSingleton() 为真且容器已经保存该工厂的 singleton 时,才进入产品缓存分支。首次调用 getObject() 后,容器按适用条件执行产品后处理并写入 factoryBeanObjectCache;下次按相同名称取得产品可以直接复用缓存值。产品缓存源码

这里存在两个不同的单例问题:工厂实例自己的 scope,以及工厂对产品是否单例的声明。工厂 Bean 是 singleton,不自动意味着产品每次相同;产品 isSingleton() 为真,也不能脱离工厂登记状态推断全局唯一。

当产品声明非单例时,容器不会使用这条 singleton 产品缓存,每次请求会调用工厂。返回值是否一定不同,仍取决于自定义 getObject();一个错误或刻意复用对象的工厂完全可能返回同一实例。本文反例使用每次明确 new Product() 的工厂,所以对象身份差异有具体实现支撑。

工厂产品也不能无条件套用普通 Bean 的全部生命周期假设。该缓存路径明确调用产品后处理,但产品如何初始化和释放还涉及 FactoryBean 契约与工厂实现责任。涉及真实客户端资源时,需要另验关闭行为,不能从产品进入缓存推导连接已经正确销毁。

lazy 与 provider 分别推迟哪一步

lazy 定义使容器在常规预实例化阶段不主动创建该 Bean。但 eager Bean 的直接构造依赖仍需要真实参数;解析依赖时取得 lazy Bean,便会触发创建。实验给 EagerConsumer 一个 lazy Product 构造参数,refresh() 返回前 Product 计数已是一。官方 lazy 契约

Provider 则改变依赖取得接口。取得 ObjectProvider<Product> 本身没有要求立即交付 Product;调用 getObject() 才发起解析与取得。Provider 不改变目标 scope:lazy singleton 在首次请求创建后复用;prototype 在每次取得时创建。直接注入 prototype 到一个 singleton 中,只在那个 singleton 创建时取得一次;将依赖声明成 provider,才能在后续业务调用中再次请求实例。

注入点上的 @Lazy 还可以采用代理推迟目标解析,这与 BeanDefinition 的 lazy 标志并非同一处决策。本文的运行计数主要针对定义 lazy 与显式 provider,不把结果外推成所有代理类型、方法调用和 scope 的保证。

模式提炼:请求边决定创建时点

把对象图中的边写成 A → get(B),才能判断 B 在何时必须存在。lazy 调整主动预创建策略,provider 将直接取得改成持有一个未来可调用的入口;只要图中仍有其他立即请求 B 的路径,B 就可能提前创建。插件加载、资源池初始化和远程客户端构建都适合用请求边标注真实触发者。

类型查询为什么也可能执行构造器

为了按类型筛选候选,容器要知道 FactoryBean 生产什么。足够精确的目标类型属性、泛型和工厂方法签名能够提供预测信息。信息不足时,如果允许初始化,getTypeForFactoryBean() 可能先创建工厂并调用 getObjectType();早期实例仍不能回答时还可能尝试完整创建工厂。

这一步通常不需要调用 getObject() 创建产品,但工厂构造器本身已经执行。若工厂在构造器里连接外部服务,“只查类型”也可能出现可见副作用。allowInit=false 可以阻止相应初始化路径,却也可能使尚未知类型的候选无法被发现。上游的非 eager 类型匹配测试分别覆盖未初始化工厂被忽略、已初始化工厂能被找到等情况,避免把该参数理解成零代价且完整的类型查询。

本地反例刻意使用 raw FactoryBean,移除可预测的产品泛型。第一次 getType(name, false) 返回空且工厂构造计数为零;允许初始化的 getType(name) 返回 Product 类型,工厂计数变成一,而产品调用仍为零。真正 getBean(name) 后产品调用才变成一。这个结果不能推广到所有泛型工厂,因为可用元数据不同会改变分支。

本地命令、结果与实验边界

实验工程下载和 JDK 21 环境准备见第 00 篇。执行前通过 java -version 确认当前终端使用 JDK 21;Maven 与应用运行环境应保持一致。

从仓库根目录执行:

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

本地 Spring 6.2.11、Java 21.0.11 的运行输出保存于 examples/spring-framework-lab/evidence/06/local-20261002/run.txt。断言检查三种 eager 创建路径产出的对象总数和 &product 工厂身份。singleton 产品只调用一次工厂,非单例产品调用两次;其余断言比较 provider、类型查询与 eager 依赖触发前后的计数。输出最后为:

1
CHAPTER 06 PASS

普通 FactoryBean 在该实验中已实例化,但首次产品查询前 getObject() 调用数为零。这里没有配置要求提前生产产品的 SmartFactoryBean,也没有其他 eager 消费方直接请求这些产品,所以该结果有明确前提。实验还使用独立 BeanFactory 检查 prototype provider,避免当前上下文中的多个 Product 候选让 scope 实验先被类型歧义中断。

练习与答案

反例题:一个 singleton FactoryBean 的 isSingleton() 返回 false,但 getObject() 总是返回工厂字段保存的同一对象。两次 getBean(name) 是否必然得到不同对象?答案是否。容器会再次调用工厂,工厂仍可复用结果;应分别检查调用次数与对象身份,而不是只看声明。

改动题:将 Chapter06 中 lazy Product 的直接取得改成“一个 eager 消费方通过构造器接收 Product”,与“消费方接收 ObjectProvider<Product> 且不调用”的两个隔离配置对照。前一组 refresh 时就需要创建 Product;后一组只有该依赖边时可以不创建。目标若另被主动查询或取消 lazy,后一组也会提前创建。答案由所有请求路径决定,不能仅检查某一个消费者上的注解。

模式速查表

现象 对应模式 观察位置
&name 和 name 返回类型不同 分别追踪定义、生产者与产品 工厂实例与产品转换
工厂已存在,产品尚未创建 分别追踪定义、生产者与产品 工厂构造计数、getObject 次数
缓存了构造器却仍创建新对象 分别追踪定义、生产者与产品 可执行成员缓存与实例 scope
lazy Bean 在启动时出现 请求边决定创建时点 eager 直接依赖与类型查询
provider 两次调用返回不同对象 请求边决定创建时点 目标 scope 与工厂实现