【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景

createBean如何寻找构造器(二)------单参数构造器的场景

代码仓库Gitee 仓库链接

本文档的所有示例代码都可以在代码仓库中找到,建议结合代码一起阅读。


⚠️ 重要提醒

请大家学习这一段代码时,保持平静。

Spring 的构造器选择机制涉及复杂的逻辑和大量的代码,学习过程中可能会遇到:

  • 代码调用链很长
  • 方法嵌套很深
  • 逻辑判断复杂
  • 参数匹配算法繁琐

这些都是正常的。请保持耐心,采用抽丝剥茧、层层深入的方式,一步一步理解,不要急于求成。


1. 回顾上一章节

在上一篇文章《createBean如何寻找构造器(一)------无参构造器的场景》中,我们深入分析了 Spring 处理无参构造器的完整流程:

  • 核心流程createBeanInstance → 条件判断 → instantiateBeanInstantiationStrategy.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 需要:

  1. 识别构造器参数:从 Bean 定义或调用参数中获取构造器参数值
  2. 匹配构造器:根据参数类型和数量匹配对应的构造器
  3. 类型转换:将配置的参数值转换为构造器参数所需的类型
  4. 实例化对象:使用匹配的构造器创建 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 的加载过程前半部分与上一个例子(无参构造器)几乎一致:

  1. 进入 createBeanInstance 方法
  2. 检查 instanceSupplierfactoryMethodName(都没有,走基础分支)
  3. 检查缓存的构造器(第一次创建,没有缓存)
  4. 通过 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 的构造器选择优先级:

  1. 优先选择 public 构造器(更符合 Java 规范,访问性更好)
  2. 优先选择参数多的构造器(更精确的匹配,信息更完整)

步骤六:从候选构造器中选择合适的构造器

遍历候选构造器,选择最合适的构造器:

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);
    }
}

🔍 关键步骤解析

  1. 提前退出条件

    • 如果已经找到合适的构造器(constructorToUse != null),且参数数量满足要求,则结束选择
    • 这避免了不必要的循环
  2. 参数数量过滤

    • 如果候选构造器的参数数量小于最少参数个数(minNrOfArgs),则跳过
    • 只有参数数量足够的构造器才会进入后续的匹配流程
  3. 参数名称解析

    • 通过 ParameterNameDiscoverer 解析出构造器参数名称
    • 用于后续的参数匹配和类型转换
  4. 创建 ArgumentsHolder

    • createArgumentArray 方法会解析构造器参数值,进行类型转换,得到 ArgumentsHolder
    • 由于我们的示例只有一个构造器,这部分逻辑相对简单,我们会在后续多构造器场景中详细展开
  5. 注册依赖关系

    • 将构造器依赖的 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 的完整流程是:

  1. 解析配置 :解析 <constructor-arg> 配置的参数值
  2. 获取候选构造器:根据访问权限获取候选构造器
  3. 排序构造器:对候选构造器进行排序(public 在前,参数多的在前)
  4. 匹配构造器:匹配构造器的参数类型和数量
  5. 类型转换:将字符串转换为构造器参数所需的类型
  6. 计算权重:计算匹配权重(多构造器场景)
  7. 缓存结果:缓存构造器和参数到 Bean 定义中
  8. 实例化对象:使用匹配的构造器创建 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 关键发现

  1. 构造器选择的分支判断

    • hasConstructorArgumentValues()true 时,会进入 autowireConstructor 方法
    • 这是单参数构造器场景与无参构造器场景的关键分歧点
  2. 候选构造器的获取策略

    • 根据 Bean 定义是否允许非 public 访问,决定获取全部构造器还是只获取 public 构造器
    • 体现了 Spring 对访问权限的灵活控制
  3. 构造器排序策略

    • public 构造器优先于非 public 构造器(更符合 Java 规范)
    • 参数数量多的构造器优先于参数数量少的构造器(更精确的匹配)
  4. 构造器缓存机制

    • Spring 会将选择的构造器和解析后的参数缓存到 Bean 定义中
    • 避免重复解析,提高性能
  5. 代码坏味道

    • resolveConstructorArguments 方法违反了单一职责原则
    • 一个方法既返回参数数量,又通过引用传递修改了入参(解析构造器参数值)

5.3 设计思想

Spring 在单参数构造器场景中体现了以下设计思想:

  • 分层处理:从简单到复杂,逐步处理不同场景(无参 → 单参 → 多参)
  • 缓存优化:通过缓存机制避免重复计算,提高性能
  • 策略模式:通过排序策略和权重计算来选择最佳构造器
  • 职责分离:虽然存在代码坏味道,但整体上还是将构造器选择、参数解析、类型转换等职责分离

5.4 本文的目的

💡 重要说明

本篇博客旨在搞清有参构造器选择的一个大致流程,重点关注:

  • 从无参构造器到单参数构造器的转变
  • 进入 autowireConstructor 的条件判断
  • 构造器选择的核心步骤和流程

对于以下复杂逻辑,我们会在后续文章中进一步展开:

  • ArgumentsHolder 的详细解析:如何将配置的参数值转换为构造器参数
  • 类型转换过程:Spring 的类型转换机制如何工作
  • 匹配权重计算:如何计算构造器的匹配权重,选择最佳构造器
  • 多构造器场景:当存在多个构造器时,Spring 如何选择最合适的

6. 大白话总结

💬 用一句话总结

Spring 处理单参数构造器时,会先检查配置中是否有 <constructor-arg>,如果有就进入 autowireConstructor 方法,然后通过九个步骤完成构造器选择:获取候选构造器、解析参数值、排序构造器、匹配构造器、计算权重、缓存结果,最后使用匹配的构造器创建对象。

💬 更详细的解释

想象一下,你要建造一座房子(创建 Bean),但这次需要指定材料(构造器参数):

  1. 检查材料清单 :Spring 先检查配置中有没有 <constructor-arg>(材料清单)。如果有,就不能用简单方式了,必须走复杂流程(autowireConstructor)。

  2. 准备工具箱 :Spring 准备一个工具箱(BeanWrapperImpl),虽然现在里面还是空的,但后续会用上。

  3. 找候选方案:Spring 根据房子的类型(Bean Class)找所有可能的建造方案(候选构造器)。如果允许用非公开方案,就全部找出来;否则只找公开方案。

  4. 快速通道检查:如果只有一个方案,且没有指定材料,可以直接用。但我们有材料清单,所以不能走快速通道。

  5. 解析材料清单 :Spring 解析材料清单(constructor-arg),看看需要多少种材料(最少参数个数),同时把材料准备好(解析参数值)。这里有个小问题:解析材料数量的同时也在准备材料,一个方法做了两件事(代码坏味道)。

  6. 给方案排序:如果有多个方案,Spring 会排序:公开方案优先,材料多的方案优先。

  7. 匹配方案和材料:Spring 尝试用准备好的材料匹配每个方案,看看哪个方案最合适。如果只有一个方案,就直接用。

  8. 计算匹配度:如果有多个方案,Spring 会计算每个方案的匹配度(权重),选择匹配度最高的(差异最小的)。

  9. 记录方案:Spring 把选好的方案和材料记录下来(缓存),下次建同样的房子就不用重新找了。

  10. 开始建造:最后,Spring 用选好的方案和材料建造房子(实例化 Bean),完成!

这就是 Spring 处理单参数构造器的完整流程。虽然看起来复杂,但每一步都有其目的,确保能够正确匹配构造器和参数。

7. 下一篇文章预告

本文我们完成了第二层的探索:单参数构造器的场景。在下一篇文章中,我们将继续深入探索:

7.1 第三层:多参数构造器的匹配

  • 当存在多个构造器时,Spring 如何选择最合适的?
  • 参数匹配的优先级是什么?
  • 如何处理构造器参数的歧义?

7.2 第四层:参数类型转换与歧义解决

  • Spring 的类型转换机制是如何工作的?
  • 当存在多个匹配的构造器时,如何解决歧义?
  • 构造器参数的类型转换规则是什么?

7.3 第五层:复杂场景------依赖注入

  • 构造器注入是如何工作的?
  • 循环依赖在构造器注入场景下如何处理?
  • 构造器注入与 setter 注入的区别是什么?

🔍 敬请期待 :下一篇文章将继续采用抽丝剥茧、层层深入的方式,逐步揭开 Spring 构造器选择机制的神秘面纱。

8. 思考题

在阅读下一篇文章之前,不妨思考以下问题:

  1. 为什么单参数构造器需要走 autowireConstructor 方法,而不是 instantiateBean(本文已解答)
  2. autowireConstructor 方法的九个步骤分别解决了什么问题?(本文已解答)
  3. 为什么 Spring 要对候选构造器进行排序?排序策略的优先级是什么?(本文已解答)
  4. 构造器缓存机制有什么优势?在什么情况下会使用缓存的构造器?(本文已解答)
  5. createArgumentArray 方法是如何将配置的参数值转换为构造器参数的?(后续文章展开)
  6. 当存在多个构造器时,Spring 如何计算匹配权重并选择最佳构造器?(后续文章展开)

9. 参考资料

9.1 相关源码类和方法

  • AbstractAutowireCapableBeanFactory.autowireConstructor() - 构造器自动装配的入口方法
  • ConstructorResolver - 构造器解析工具类
  • BeanUtils.instantiateClass() - 通过反射调用构造器的工具方法

9.2 相关文章


上一篇createBean如何寻找构造器(一)------无参构造器的场景
下一篇createBean如何寻找构造器(三)------多参数构造器的匹配(待发布)

相关推荐
沉鱼.4434 分钟前
第十二届题目
java·前端·算法
努力的小郑1 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
赫瑞1 小时前
数据结构中的排列组合 —— Java实现
java·开发语言·数据结构
Victor3562 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3562 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁2 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp2 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
周末也要写八哥2 小时前
多进程和多线程的特点和区别
java·开发语言·jvm
惜茶3 小时前
vue+SpringBoot(前后端交互)
java·vue.js·spring boot
宁瑶琴3 小时前
COBOL语言的云计算
开发语言·后端·golang