【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如何寻找构造器(三)------多参数构造器的匹配(待发布)

相关推荐
吨~吨~吨~3 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟3 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日3 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du3 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea
C澒3 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
东东5163 小时前
高校智能排课系统 (ssm+vue)
java·开发语言
余瑜鱼鱼鱼3 小时前
HashTable, HashMap, ConcurrentHashMap 之间的区别
java·开发语言
SunnyDays10113 小时前
使用 Java 自动设置 PDF 文档属性
java·pdf文档属性