createBean如何寻找构造器(二)------单参数构造器的场景
代码仓库 :Gitee 仓库链接
本文档的所有示例代码都可以在代码仓库中找到,建议结合代码一起阅读。
⚠️ 重要提醒
请大家学习这一段代码时,保持平静。
Spring 的构造器选择机制涉及复杂的逻辑和大量的代码,学习过程中可能会遇到:
- 代码调用链很长
- 方法嵌套很深
- 逻辑判断复杂
- 参数匹配算法繁琐
这些都是正常的。请保持耐心,采用抽丝剥茧、层层深入的方式,一步一步理解,不要急于求成。
1. 回顾上一章节
在上一篇文章《createBean如何寻找构造器(一)------无参构造器的场景》中,我们深入分析了 Spring 处理无参构造器的完整流程:
- 核心流程 :
createBeanInstance→ 条件判断 →instantiateBean→InstantiationStrategy.instantiate() - 关键发现 :构造器缓存机制、模板方法设计模式、进入
instantiateBean的五个条件 - 设计思想:分层设计、缓存优化、模板方法模式
现在,我们将继续深入探索更复杂的场景:当 Bean 只有一个带参数的构造器时,Spring 如何匹配参数并选择构造器?
2. 统一术语
在深入源码之前,让我们先明确本文涉及的关键术语:
| 术语 | 定义 |
|---|---|
| 构造器参数匹配(Constructor Argument Matching) | Spring 根据提供的参数值匹配对应构造器的过程 |
| 参数类型转换(Type Conversion) | Spring 将配置的参数值转换为构造器参数所需类型的过程 |
| autowireConstructor | Spring 用于自动装配构造器的方法,处理带参数的构造器场景 |
| ConstructorResolver | Spring 内部用于解析构造器的工具类 |
| 参数值解析(Argument Value Resolution) | Spring 将配置的参数值解析为实际对象的过程 |
3. 问题场景
在上一篇文章中,我们探索了最简单的场景:无参构造器。现在,让我们考虑更复杂的情况。
💡 核心问题:
当 Bean 类只有一个带参数的构造器时,Spring 需要解决以下问题:
- 如何判断是否需要走构造器选择流程?(本文解答)
- 构造器选择的整体流程是什么?(本文解答)
- 如何根据配置的参数值匹配构造器?(本文了解流程,详细机制后续展开)
- 参数类型转换是如何进行的?(本文了解流程,详细机制后续展开)
- 如果参数类型不匹配,Spring 如何处理?(后续文章展开)
- 当存在多个构造器时,Spring 如何选择最合适的?(后续文章展开)
🔍 本文的聚焦点:
本文聚焦于第二层:单参数构造器的场景,主要目标是:
- 理解从无参构造器到单参数构造器的转变 :什么情况下会进入
autowireConstructor方法? - 了解构造器选择的整体流程 :
autowireConstructor方法的九个步骤分别做了什么? - 建立整体认知:为后续深入理解参数匹配、类型转换、权重计算等细节打下基础
⚠️ 重要说明:
本文主要关注整体流程,对于以下复杂机制,我们会在后续文章中详细展开:
ArgumentsHolder的详细解析过程- 参数类型转换的具体实现
- 匹配权重计算的详细逻辑
- 多构造器场景的选择策略
4. 源码探索
4.1 从无参构造器到单参数构造器的转变
在上一篇文章中,我们看到当满足特定条件时,Spring 会直接调用 instantiateBean(beanName, mbd) 方法,使用无参构造器创建 Bean 实例。
但是,当 Bean 类只有一个带参数的构造器时,情况就不同了。Spring 需要:
- 识别构造器参数:从 Bean 定义或调用参数中获取构造器参数值
- 匹配构造器:根据参数类型和数量匹配对应的构造器
- 类型转换:将配置的参数值转换为构造器参数所需的类型
- 实例化对象:使用匹配的构造器创建 Bean 实例
示例代码:
- Bean 类:
code/spring-basic/src/main/java/com/example/constructor/SingleArgBean.java - XML 配置:
code/spring-basic/src/main/resources/applicationContext-single-arg-constructor.xml - 测试类:
code/spring-basic/src/test/java/com/example/constructor/SingleArgConstructorTest.java
这个示例展示了一个最简单的单参数构造器场景:Bean 类只有一个接受 String 类型参数的构造器,XML 配置中使用 <constructor-arg value="测试名称"/> 来指定构造器参数值。
4.2 进入 autowireConstructor 方法
4.2.1 前半部分流程与无参构造器一致
通过 debug testSingleArgBean_WithConstructorArg() 测试方法,我们发现该 Bean 的加载过程前半部分与上一个例子(无参构造器)几乎一致:
- 进入
createBeanInstance方法 - 检查
instanceSupplier和factoryMethodName(都没有,走基础分支) - 检查缓存的构造器(第一次创建,没有缓存)
- 通过
BeanPostProcessor确定构造器 (没有干预,返回null)
4.2.2 关键分歧点:判断是否走 autowireConstructor
分歧出现在判断是否进入 autowireConstructor 的逻辑处。让我们回顾一下这个判断条件:
java
if (ctors == null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || args != null) {
// 需要构造器注入或存在构造器参数,走构造器选择逻辑
return autowireConstructor(beanName, mbd, ctors, args);
}
// 否则,使用默认的无参构造器
return instantiateBean(beanName, mbd);
🔍 关键发现:
在我们的单参数构造器场景中,由于 Bean 配置中存在 <constructor-arg value="测试名称"/>,所以:
mbd.hasConstructorArgumentValues()返回true- 满足进入
autowireConstructor的条件之一 - 最终运行至
this.autowireConstructor(beanName, mbd, ctors, args);
💡 对比无参构造器场景:
| 场景 | hasConstructorArgumentValues() |
结果 |
|---|---|---|
| 无参构造器 | false(没有 <constructor-arg>) |
走 instantiateBean |
| 单参数构造器 | true(有 <constructor-arg>) |
走 autowireConstructor |
这就是为什么单参数构造器场景会进入 autowireConstructor 方法,而不是 instantiateBean 方法的原因。
4.2.3 autowireConstructor 方法的核心逻辑
进入 autowireConstructor 方法后,就进入了选择构造器的关键逻辑。ConstructorResolver 类提供了 autowireConstructor 方法,让我们逐步分析其执行流程:
步骤一:初始化 BeanWrapper
java
BeanWrapperImpl bw = new BeanWrapperImpl();
this.beanFactory.initBeanWrapper(bw);
Spring 会新建一个 BeanWrapperImpl 对象,并根据 BeanFactory 初始化该对象。
🔍 深入观察 :进入 initBeanWrapper 方法,实际上什么都没做,因为:
- Bean 工厂中的
ConversionService属性我们没有设置 BeanWrapper也没有CustomEditors
💡 理解 :BeanWrapper 主要用于后续的属性操作和类型转换,在这个阶段只是初始化,实际的转换服务会在需要时使用。
步骤二:获取候选构造器
由于没有明确的构造器入参,且首次选择构造器(Bean 定义中没有构造器缓存),所以需要根据 Bean 定义解析的 Class 获取候选构造器:
java
Constructor<?>[] candidates = chosenCtors;
if (candidates == null) {
Class<?> beanClass = mbd.getBeanClass();
candidates = (mbd.isNonPublicAccessAllowed() ?
beanClass.getDeclaredConstructors() :
beanClass.getConstructors());
}
🔍 关键判断:
- 如果 Bean 定义允许使用非 public 的构造器(
mbd.isNonPublicAccessAllowed()),则获取全部构造器(getDeclaredConstructors()) - 否则只获取 public 构造器(
getConstructors())
在我们的示例中,SingleArgBean 的构造器是 public 的,所以会获取到该构造器。
步骤三:快速路径检查
接下来有一个分支判断,用于处理只有一个候选构造器且没有明确参数的场景:
java
if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) {
// 快速路径:只有一个候选构造器,且没有明确的参数
Constructor<?> uniqueCandidate = candidates[0];
if (uniqueCandidate.getParameterCount() == 0) {
// 无参构造器,直接使用
...
}
}
🔍 快速路径的条件(需要同时满足):
- 候选构造器数组数量为 1
- 明确的参数为空(
explicitArgs == null) - Bean 定义中也未定义
constructor-arg(!mbd.hasConstructorArgumentValues())
💡 我们的示例 :由于配置了 <constructor-arg>,mbd.hasConstructorArgumentValues() 返回 true,所以不符合快速路径条件,需要继续走常规流程。
步骤四:解析 constructor-arg 并获取最少参数个数
根据 Bean 定义获取 constructor-arg,并解析出最少参数个数:
java
ConstructorArgumentValues resolvedValues = null;
int minNrOfArgs;
if (explicitArgs != null) {
minNrOfArgs = explicitArgs.length;
} else {
ConstructorArgumentValues rawValues = mbd.getConstructorArgumentValues();
resolvedValues = new ConstructorArgumentValues();
minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw,
rawValues, resolvedValues);
}
⚠️ 代码坏味道:
resolveConstructorArguments 方法违反了单一职责原则:
- 返回了最少参数个数 (
minNrOfArgs) - 同时通过引用传递修改了入参 (
resolvedValues),解析出构造器参数和值
一个方法做了多件事,让方法的功能不够清晰。更好的设计应该是将参数数量计算和参数值解析分离。
步骤五:对候选构造器进行排序
对候选构造器进行排序,确定构造器选择的优先级:
java
AutowireUtils.sortConstructors(candidates);
🔍 排序策略:
- public 构造器优先:public 构造器排在非 public 构造器之前
- 参数多的优先:参数数量多的构造器排在参数数量少的构造器之前
💡 设计思想:这个排序策略体现了 Spring 的构造器选择优先级:
- 优先选择 public 构造器(更符合 Java 规范,访问性更好)
- 优先选择参数多的构造器(更精确的匹配,信息更完整)
步骤六:从候选构造器中选择合适的构造器
遍历候选构造器,选择最合适的构造器:
java
for (Constructor<?> candidate : candidates) {
Class<?>[] paramTypes = candidate.getParameterTypes();
// 如果已经找到合适的构造器,且参数数量满足要求,则结束选择
if (constructorToUse != null && argsToUse != null &&
argsToUse.length > paramTypes.length) {
break;
}
// 参数数量不足,跳过
if (paramTypes.length < minNrOfArgs) {
continue;
}
// 进一步判断是否可用
ArgumentsHolder argsHolder;
String[] paramNames = null;
// 获取参数名称
if (resolvedValues != null) {
paramNames = this.beanFactory.getParameterNameDiscoverer()
.getParameterNames(candidate);
}
// 解析构造器参数值,得到 ArgumentsHolder
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw,
paramTypes, paramNames, candidate, autowiring);
// 注册构造器依赖的 Bean
if (argsHolder != null) {
argsHolder.storeCache(mbd, constructorToUse);
}
}
🔍 关键步骤解析:
-
提前退出条件:
- 如果已经找到合适的构造器(
constructorToUse != null),且参数数量满足要求,则结束选择 - 这避免了不必要的循环
- 如果已经找到合适的构造器(
-
参数数量过滤:
- 如果候选构造器的参数数量小于最少参数个数(
minNrOfArgs),则跳过 - 只有参数数量足够的构造器才会进入后续的匹配流程
- 如果候选构造器的参数数量小于最少参数个数(
-
参数名称解析:
- 通过
ParameterNameDiscoverer解析出构造器参数名称 - 用于后续的参数匹配和类型转换
- 通过
-
创建 ArgumentsHolder:
createArgumentArray方法会解析构造器参数值,进行类型转换,得到ArgumentsHolder- 由于我们的示例只有一个构造器,这部分逻辑相对简单,我们会在后续多构造器场景中详细展开
-
注册依赖关系:
- 将构造器依赖的 Bean 关系加入缓存
- 用于后续的依赖注入和循环依赖处理
步骤七:计算匹配权重并选择最佳构造器
当存在多个候选构造器时,Spring 需要计算每个构造器的匹配权重:
🔍 权重计算机制:
- 匹配权重实际上是差异权重,差异权重低的构造器更匹配
- 选择差异权重最低(即匹配度最高)的构造器
💡 我们的示例:由于只有一个构造器,所以不需要进行权重比较。在多构造器场景中,权重计算会变得非常重要,我们会在后续文章中详细展开。
步骤八:缓存构造器和参数
将解析后适配的构造器和参数缓存至 Bean 定义中,以便下次使用无需重新加载:
java
mbd.resolvedConstructorOrFactoryMethod = constructorToUse;
mbd.constructorArgumentsResolved = true;
mbd.resolvedConstructorArguments = argsToUse;
🔍 缓存字段说明:
resolvedConstructorOrFactoryMethod:缓存选择的构造器constructorArgumentsResolved:标记构造器参数已解析resolvedConstructorArguments:缓存解析后的构造器参数值
💡 性能优化:这样下次创建同一个 Bean 时,就可以直接使用缓存的构造器和参数,避免重复解析,提高性能。
步骤九:实例化 Bean
最后,通过 BeanWrapper 设置 Bean 实例:
java
bw.setBeanInstance(this.instantiate(beanName, mbd, constructorToUse, argsToUse));
🔍 实例化过程:
instantiate方法会使用选择的构造器和解析后的参数值来创建 Bean 实例- 创建完成后,将实例设置到
BeanWrapper中 BeanWrapper会返回给调用方,用于后续的属性注入等操作
4.2.4 完整流程总结
在单参数构造器的场景中,Spring 的完整流程是:
- 解析配置 :解析
<constructor-arg>配置的参数值 - 获取候选构造器:根据访问权限获取候选构造器
- 排序构造器:对候选构造器进行排序(public 在前,参数多的在前)
- 匹配构造器:匹配构造器的参数类型和数量
- 类型转换:将字符串转换为构造器参数所需的类型
- 计算权重:计算匹配权重(多构造器场景)
- 缓存结果:缓存构造器和参数到 Bean 定义中
- 实例化对象:使用匹配的构造器创建 Bean 实例
待补充 :ArgumentsHolder 的详细解析、类型转换过程和匹配权重计算的详细逻辑,我们会在后续文章中深入展开。
5. 本文总结
5.1 核心流程回顾
通过本文的探索,我们完整地跟踪了 Spring 处理单参数构造器场景的流程:
getBean("singleArgBean")
↓
createBeanInstance(beanName, mbd)
↓
检查 instanceSupplier 和 factoryMethodName(都没有,走基础分支)
↓
检查缓存的构造器(第一次创建,没有缓存)
↓
通过 BeanPostProcessor 确定构造器(没有干预,返回 null)
↓
判断是否进入 autowireConstructor(hasConstructorArgumentValues() 为 true)
↓
autowireConstructor(beanName, mbd, ctors, args)
↓
新建 BeanWrapperImpl 对象
↓
获取候选构造器(根据访问权限)
↓
解析 constructor-arg 并获取最少参数个数
↓
对候选构造器进行排序(public 在前,参数多的在前)
↓
从候选构造器中选择合适的构造器
↓
获取匹配权重并选择最佳构造器(多构造器场景)
↓
缓存构造器和参数到 Bean 定义中
↓
使用 instantiate 方法创建 Bean 实例
↓
设置 Bean 实例到 BeanWrapper
5.2 关键发现
-
构造器选择的分支判断:
- 当
hasConstructorArgumentValues()为true时,会进入autowireConstructor方法 - 这是单参数构造器场景与无参构造器场景的关键分歧点
- 当
-
候选构造器的获取策略:
- 根据 Bean 定义是否允许非 public 访问,决定获取全部构造器还是只获取 public 构造器
- 体现了 Spring 对访问权限的灵活控制
-
构造器排序策略:
- public 构造器优先于非 public 构造器(更符合 Java 规范)
- 参数数量多的构造器优先于参数数量少的构造器(更精确的匹配)
-
构造器缓存机制:
- Spring 会将选择的构造器和解析后的参数缓存到 Bean 定义中
- 避免重复解析,提高性能
-
代码坏味道:
resolveConstructorArguments方法违反了单一职责原则- 一个方法既返回参数数量,又通过引用传递修改了入参(解析构造器参数值)
5.3 设计思想
Spring 在单参数构造器场景中体现了以下设计思想:
- 分层处理:从简单到复杂,逐步处理不同场景(无参 → 单参 → 多参)
- 缓存优化:通过缓存机制避免重复计算,提高性能
- 策略模式:通过排序策略和权重计算来选择最佳构造器
- 职责分离:虽然存在代码坏味道,但整体上还是将构造器选择、参数解析、类型转换等职责分离
5.4 本文的目的
💡 重要说明:
本篇博客旨在搞清有参构造器选择的一个大致流程,重点关注:
- 从无参构造器到单参数构造器的转变
- 进入
autowireConstructor的条件判断 - 构造器选择的核心步骤和流程
对于以下复杂逻辑,我们会在后续文章中进一步展开:
- ArgumentsHolder 的详细解析:如何将配置的参数值转换为构造器参数
- 类型转换过程:Spring 的类型转换机制如何工作
- 匹配权重计算:如何计算构造器的匹配权重,选择最佳构造器
- 多构造器场景:当存在多个构造器时,Spring 如何选择最合适的
6. 大白话总结
💬 用一句话总结:
Spring 处理单参数构造器时,会先检查配置中是否有 <constructor-arg>,如果有就进入 autowireConstructor 方法,然后通过九个步骤完成构造器选择:获取候选构造器、解析参数值、排序构造器、匹配构造器、计算权重、缓存结果,最后使用匹配的构造器创建对象。
💬 更详细的解释:
想象一下,你要建造一座房子(创建 Bean),但这次需要指定材料(构造器参数):
-
检查材料清单 :Spring 先检查配置中有没有
<constructor-arg>(材料清单)。如果有,就不能用简单方式了,必须走复杂流程(autowireConstructor)。 -
准备工具箱 :Spring 准备一个工具箱(
BeanWrapperImpl),虽然现在里面还是空的,但后续会用上。 -
找候选方案:Spring 根据房子的类型(Bean Class)找所有可能的建造方案(候选构造器)。如果允许用非公开方案,就全部找出来;否则只找公开方案。
-
快速通道检查:如果只有一个方案,且没有指定材料,可以直接用。但我们有材料清单,所以不能走快速通道。
-
解析材料清单 :Spring 解析材料清单(
constructor-arg),看看需要多少种材料(最少参数个数),同时把材料准备好(解析参数值)。这里有个小问题:解析材料数量的同时也在准备材料,一个方法做了两件事(代码坏味道)。 -
给方案排序:如果有多个方案,Spring 会排序:公开方案优先,材料多的方案优先。
-
匹配方案和材料:Spring 尝试用准备好的材料匹配每个方案,看看哪个方案最合适。如果只有一个方案,就直接用。
-
计算匹配度:如果有多个方案,Spring 会计算每个方案的匹配度(权重),选择匹配度最高的(差异最小的)。
-
记录方案:Spring 把选好的方案和材料记录下来(缓存),下次建同样的房子就不用重新找了。
-
开始建造:最后,Spring 用选好的方案和材料建造房子(实例化 Bean),完成!
这就是 Spring 处理单参数构造器的完整流程。虽然看起来复杂,但每一步都有其目的,确保能够正确匹配构造器和参数。
7. 下一篇文章预告
本文我们完成了第二层的探索:单参数构造器的场景。在下一篇文章中,我们将继续深入探索:
7.1 第三层:多参数构造器的匹配
- 当存在多个构造器时,Spring 如何选择最合适的?
- 参数匹配的优先级是什么?
- 如何处理构造器参数的歧义?
7.2 第四层:参数类型转换与歧义解决
- Spring 的类型转换机制是如何工作的?
- 当存在多个匹配的构造器时,如何解决歧义?
- 构造器参数的类型转换规则是什么?
7.3 第五层:复杂场景------依赖注入
- 构造器注入是如何工作的?
- 循环依赖在构造器注入场景下如何处理?
- 构造器注入与 setter 注入的区别是什么?
🔍 敬请期待 :下一篇文章将继续采用抽丝剥茧、层层深入的方式,逐步揭开 Spring 构造器选择机制的神秘面纱。
8. 思考题
在阅读下一篇文章之前,不妨思考以下问题:
- 为什么单参数构造器需要走
autowireConstructor方法,而不是instantiateBean?(本文已解答) autowireConstructor方法的九个步骤分别解决了什么问题?(本文已解答)- 为什么 Spring 要对候选构造器进行排序?排序策略的优先级是什么?(本文已解答)
- 构造器缓存机制有什么优势?在什么情况下会使用缓存的构造器?(本文已解答)
createArgumentArray方法是如何将配置的参数值转换为构造器参数的?(后续文章展开)- 当存在多个构造器时,Spring 如何计算匹配权重并选择最佳构造器?(后续文章展开)
9. 参考资料
9.1 相关源码类和方法
AbstractAutowireCapableBeanFactory.autowireConstructor()- 构造器自动装配的入口方法ConstructorResolver- 构造器解析工具类BeanUtils.instantiateClass()- 通过反射调用构造器的工具方法
9.2 相关文章
上一篇 :createBean如何寻找构造器(一)------无参构造器的场景
下一篇 :createBean如何寻找构造器(三)------多参数构造器的匹配(待发布)