Spring源码中,createBean()是AbstractAutowireCapableBeanFactory创建Bean的核心方法,而它内部首先要解决的一件事:这个Bean的实例该由哪个构造器new出来。无参构造器,也就是默认构造器,是最常见的场景,也是理解整个构造器解析体系的地基。这篇文章会和你一起走一遍无参构造器场景下createBean寻找构造器的完整过程,顺带把ConstructorResolver、SmartInstantiationAwareBeanPostProcessor、CglibSubclassingInstantiationStrategy这些名字串起来。适合正在啃Spring源码、搞不清doCreateBean和createBeanInstance之间关系,或者想搞懂为什么有时候构造器选择会“不按套路出牌”的同学参考。
1. 从 getBean 到 createBean:构造器选择的入口
1.1 为什么需要“寻找构造器”
JVM创建一个Java对象,最终都要落到某个构造器上。Spring作为IoC容器,不能像普通代码那样直接new一个对象,它必须根据BeanDefinition里的元信息、构造器参数、注解配置,甚至BeanPostProcessor给出的建议,决定到底调用哪个构造器。这个决定过程,就是“寻找构造器”。
很多人以为Spring实例化Bean就是“调无参构造器”,这是对小型项目的直觉,但在复杂场景下明显不够用。当一个Bean定义了多个构造器,或某个构造器标了@Autowired,或配置了constructor-arg,Spring就必须有一套明确的决策规则。否则依赖注入就成了“乱点鸳鸯谱”。
从设计角度看,构造器选择是Bean实例化策略中最核心的分支逻辑。它直接决定了后续属性填充、初始化回调能不能顺利进行。理解它,不光是应付源码面试,更是排查“Bean实例化失败”“多个构造器报错”这类问题的基本功。
1.2 createBean 的调用链:doCreateBean 之前发生了什么
先从入口看起。AbstractAutowireCapableBeanFactory#createBean是getBean之后的真正创建方法,它会做三件大事:
- 解析并处理BeanClass;
- 调用prepareMethodOverrides(),处理lookup-method和replaced-method的覆盖标记;
- 执行resolveBeforeInstantiation(),给BeanPostProcessor一个“提前介入”的机会。
这里的resolveBeforeInstantiation很容易被忽略,其实它和AOP有着直接关系。如果你在BeanPostProcessor的postProcessBeforeInstantiation方法里直接返回了一个代理对象,那么Spring会直接把这个对象作为最终bean返回,后面doCreateBean里的构造器解析流程根本不会执行。这也是“无参构造器寻找”被跳过的第一种情况。
如果一切正常,进入doCreateBean。doCreateBean的第一步就是从factoryBeanInstanceCache里尝试取缓存的包装对象,取不到就调用createBeanInstance(beanName, mbd, args)。createBeanInstance就是寻找构造器的现场。
调用链可以简单记为:
getBean -> doGetBean -> createBean -> doCreateBean -> createBeanInstance建议你第一次看源码时,只盯这条线,别被后面一堆回调带偏。
2. 无参构造器的判定逻辑:ConstructorResolver 的抉择
2.1 createBeanInstance 里的关键分支
createBeanInstance的源码不长,但每一步都有讲究。核心代码可以抽象成下面几步:
Class<?> beanClass = resolveBeanClass(mbd, beanName); Supplier<?> instanceSupplier = mbd.getInstanceSupplier(); if (instanceSupplier != null) { return obtainFromSupplier(instanceSupplier, beanName); } if (mbd.getFactoryMethodName() != null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR) { return autowireConstructor(beanName, mbd, ctors, null); } return instantiateBean(beanName, mbd);这里的顺序很有讲究:Supplier优先级最高,工厂方法第二,然后才轮到构造器解析。如果Supplier存在,哪怕你写了工厂方法和构造器,也用它。对于大多数普通Bean,Supplier和工厂方法都是null,真正决定走哪条路的,是后面两段判断。
另外,createBeanInstance在正式解析之前,还有一段针对构造器的缓存判断。如果args为null,框架会加锁检查RootBeanDefinition的resolvedConstructorOrFactoryMethod是否已经非空。已经解析过就直接复用结果:如果之前判定需要构造器参数则走autowireConstructor,否则走instantiateBean。这也是为什么第二次创建相同Definition时速度会明显更快。
第一段判断里,determineConstructorsFromBeanPostProcessors会遍历所有SmartInstantiationAwareBeanPostProcessor,让它们返回“候选构造器”。如果任何一个PostProcessor返回了候选数组,就说明开发环境里已经明确指定了要用的构造器,此时直接走autowireConstructor去解析参数并实例化。第二段判断是说,如果autowireMode显式配成了构造器自动装配,即使没有候选构造器,也会强制走autowireConstructor。
只有这两段都不满足,才落到方法最下面的instantiateBean——也就是无参构造器场景。
2.2 无参构造器为什么是“兜底”
无参构造器是所有Java类默认携带的构造器。只要类里没有显式定义其他构造器,编译器就会生成一个无参构造器。Spring把这个默认构造器当成“兜底方案”再合适不过:绝大多数POJO、配置类、组件类都没有特殊构造器需求,直接反射调用无参构造器就能得到实例,成本最低,逻辑最简单。
用类比来说,出门前你如果没有指定“要坐奔驰”“需要带两个箱子”,那默认就是腿着走。Spring也一样:没有额外信息时,无参构造器就是最省事的一条路。但别忘了,“腿着走”并不等于“没有选择”,而是默认选择的结果。一旦有候选构造器出现,局面就会变复杂。
另一点值得注意:Spring在instantiateBean里不会检查构造器是否public。它会调用getDeclaredConstructor()拿到构造器,再通过ReflectionUtils.makeAccessible()强制放开访问权限。所以即使你的无参构造器是private,Spring也能创建实例。这跟普通new严格区分开,是反射带来的能力。
2.3 源码走读:determineCandidateConstructors 为什么返回 null
无参构造器场景成立的标志,是determineConstructorsFromBeanPostProcessors返回null。这个方法内部长这样:
protected Constructor<?>[] determineConstructorsFromBeanPostProcessors(@Nullable Class<?> beanClass, String beanName) { if (beanClass != null) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor sbp = (SmartInstantiationAwareBeanPostProcessor) bp; Constructor<?>[] ctors = sbp.determineCandidateConstructors(beanClass, beanName); if (ctors != null) { return ctors; } } } } return null; }真正会返回候选构造器的PostProcessor,最常见的是AutowiredAnnotationBeanPostProcessor。它会扫描类中的构造器,如果某个构造器标了@Autowired,就会作为一个候选。还有一种情况也会被它挑出来:类里只有一个构造器,并且这个构造器不是无参构造器。这种情况下即使没标@Autowired,它也会认为“这个Bean默认应该用带参构造器”,返回候选列表。
如果你的类只有无参构造器,没有其他构造器,也没有任何@Autowired标记,那么AutowiredAnnotationBeanPostProcessor在determineCandidateConstructors里扫了一圈,发现没有值得推荐的构造器,就会返回null。此时createBeanInstance自然进入instantiateBean。
这里有个容易踩坑的点:如果类里同时有无参构造器和带参构造器,但带参构造器没有标@Autowired,结果如何?这取决于多个PostProcessor的组合和AutowiredAnnotationBeanPostProcessor内部的“单一构造器才自动选中”策略。多个构造器存在时,它不会默认把某一个当作唯一候选。这类问题我们放到第4节和第5节后面展开,属于有参构造器系列的内容。
3. 实例化流程:从构造器到 BeanWrapper
3.1 实例化策略:CglibSubclassingInstantiationStrategy 的“伪装”
走到instantiateBean这一步,Spring会调用实例化策略。实例化策略接口是InstantiationStrategy,默认实现是CglibSubclassingInstantiationStrategy。很多人看到Cglib三个字就以为Spring所有Bean都用CGLIB创建,这是误解。
CglibSubclassingInstantiationStrategy继承自SimpleInstantiationStrategy,它重写了instantiate方法:
@Override public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { if (bd.getMethodOverrides().isEmpty()) { return super.instantiate(bd, beanName, owner); } return instantiateWithMethodInjection(bd, beanName, owner); }只有当RootBeanDefinition里的methodOverrides不为空,也就是配置了lookup-method或replaced-method时,默认策略才会换成CGLIB动态生成子类。而绝大多数Bean的methodOverrides都是空的,所以最终会回到父类SimpleInstantiationStrategy的纯反射逻辑。
换句话说,CGLIB在这里是个“备胎策略”,它服务的是方法注入这种特殊需求,而不是常规构造器实例化。看到这个名字时,应该联想的是“lookup-method”,而不是AOP代理。AOP代理的生成由ProxyFactory完成,那是另一条线,后文会再提一句。
3.2 无参构造器场景下的 Bean 创建全过程
在SimpleInstantiationStrategy#instantiate中,如果methodOverrides为空,会执行类似这样的逻辑:
Constructor<?> constructorToUse = bd.getResolvedConstructor(); if (constructorToUse == null) { constructorToUse = clazz.getDeclaredConstructor(); bd.setResolvedConstructor(constructorToUse); } return BeanUtils.instantiateClass(constructorToUse);这里有两个小细节值得学习。
第一是缓存。Spring会先把解析出来的构造器存到bd.setResolvedConstructor里,下次创建同一个BeanDefinition时就不用再走一遍getDeclaredConstructor。对于单例Bean,这个缓存意义不大,因为单例Bean大部分只创建一次;但对prototype Bean来说,每次getBean都会重新创建,缓存能省下一堆反射查找开销。
第二是getDeclaredConstructor与getConstructor的区别。getConstructor()只能拿public构造器,getDeclaredConstructor()能拿到类的所有构造器,包括private。Spring用后者,再配合BeanUtils内部对非public构造器的访问权限处理,才能做到“私有构造器也能实例化”。这对一些工具类、框架内部类很关键。
拿到构造器后,BeanUtils.instantiateClass会new一个实例出来。这个实例还是“裸的”,没有注入任何属性。紧接着instantiateBean方法会把实例包装成BeanWrapperImpl,完成类型转换器等基础配置,返回给doCreateBean。doCreateBean再继续做属性填充(populateBean)和初始化(initializeBean)。
所以无参构造器场景下,createBeanInstance的职责是:找到默认构造器、创建裸对象、包一层BeanWrapper。真正的业务依赖注入是在后面几步完成的。
4. 常见问题与排查技巧实录
4.1 为什么有时候无参构造器没有被选中
这是我在论坛里见过最多的问题。简单总结,原因基本集中在三类:
- 某个构造器被@Autowired标记了。AutowiredAnnotationBeanPostProcessor会把它作为候选,Spring直接走autowireConstructor,无参构造器被跳过。
- 配置了构造函数自动装配。比如XML里的
autowire="constructor",或者通过CustomAutowireConfigurer设置,导致mbd.getResolvedAutowireMode()变成AUTOWIRE_CONSTRUCTOR,Spring强制尝试用构造器注入。 - 自己写了SmartInstantiationAwareBeanPostProcessor,并且在determineCandidateConstructors里返回了非null数组。这种情况相对少见,但一旦出现,优先级极高。
排查时,最直接的方式是在createBeanInstance的这两行打断点,打印两个值:
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); int autowireMode = mbd.getResolvedAutowireMode();只要看到ctors不为null,或者autowireMode大于0,无参构造器被跳过就是正常的。
4.2 如何通过断点验证无参构造器选择过程
要验证“无参构造器场景”,可以准备一个最小Demo。定义一个类:
@Component public class DemoBean { private String name; public DemoBean() { this.name = "default"; } public String getName() { return name; } }然后用AnnotationConfigApplicationContext启动:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.example"); DemoBean bean = ctx.getBean(DemoBean.class); System.out.println(bean.getName());在AbstractAutowireCapableBeanFactory#createBeanInstance方法第一行打断点,观察程序进入后,单步执行到determineConstructorsFromBeanPostProcessors(beanClass, beanName)这一步,查看返回的ctors应该是null。再继续往下,会看到autowireMode为0,最终进入instantiateBean方法。接着在SimpleInstantiationStrategy#instantiate方法打断点,就可以看到getDeclaredConstructor被调用,最后走BeanUtils.instantiateClass。
如果你想更“现场”一点,可以启动时配置debug日志:
logging.level.org.springframework.beans.factory.support=debug日志会输出一些BeanDefinition和实例化相关信息,但要精确到构造器选择过程,断点还是最直观的。
4.3 多个构造器时的决定规则(为后续文章埋个伏笔)
如果类里同时有无参构造器和带参构造器,且没有@Autowired,也没有constructor-arg,Spring会怎么选?这个问题的完整答案在ConstructorResolver#autowireConstructor里。
简单透露一个方向:Spring会先收集所有“合适”的构造器,包括无参构造器。然后根据BeanDefinition里的构造器参数,或者容器中已有的Bean类型,逐一尝试匹配可用的参数。多个候选之间会有优先级判断,优先选择能够完全匹配参数的构造器。如果实在匹配不上,可能抛出异常。
这个规则比无参构造器场景复杂得多,涉及参数解析器、类型转换、循环依赖下的提前实例化。我们放在下一篇专门拆解。这里埋个伏笔:无参构造器并不是所有时候都能“躺赢”,只要候选列表出现,它也可能只是备选之一。
5. 经验总结与影响范围
5.1 理解构造器选择对 Spring 扩展开发的意义
很多人读源码只是为了面试,但构造器选择这件事在真实开发里能派上用场。
你在写自定义SmartInstantiationAwareBeanPostProcessor时,可以通过determineCandidateConstructors直接干预Spring的构造器选择。比如在某些框架里,希望“所有以特定后缀命名的类都用带参构造器”,就可以在这里返回一个指定数组,Spring会乖乖执行。这种扩展点一旦用起来,比在XML里堆constructor-arg灵活得多。
再比如AOP。Spring AOP在大多数情况下,是先通过createBeanInstance把原始Bean创建出来,再在初始化后处理阶段通过ProxyFactory生成代理对象。很多人一看到CglibSubclassingInstantiationStrategy就以为是AOP在作怪,其实两者不在一个阶段。代理生成影响的是最终返回给你的对象,而构造器选择影响的是最原始的那个目标对象。分清这两层,排查代理相关问题时才不会头脑混乱。
5.2 从源码里学到的缓存与反射技巧
我建议你带着“为什么这么设计”的眼光去看这些代码,能学到不少可复用的经验。
比如构造器缓存。SimpleInstantiationStrategy把解析出的构造器存进RootBeanDefinition,避免了重复反射。虽然这只是一个很小的优化,但如果你在做批量创建对象的框架,可以模仿同样的思路:把Class解析结果缓存起来,别每次都new一个Constructor。
再比如非public构造器处理。Spring反射调用私有构造器时会主动设置setAccessible(true),这在普通业务代码里可能不受待见,但在框架层是完全可接受的。如果你的项目里需要兼容外部类,或者想限制用户直接new但允许容器创建,这个思路值得一试。
5.3 系列内容的预告
这篇文章只讲了无参构造器场景,算是把“构造器寻找”的最小Case跑通了。说实话,源码里真正烧脑的地方在于有参构造器的参数匹配,以及多个候选构造器存在时的“择优录取”。下一篇我会继续拆ConstructorResolver的autowireConstructor方法,把参数解析、异常兜底、循环依赖下的构造器选择全部展开。
你在调试无参构造器时如果遇到奇怪问题,先把mbd.getResolvedAutowireMode()和ctors打出来,九成问题都出在这两个值上。这是我实际调试中回报率最高的一步。先把这条诊断路径练熟,再进入有参构造器的复杂世界,你会走得稳很多。