ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring IOC容器构造器选择解析:从getBean到无参构造器

Spring IOC容器构造器选择解析:从getBean到无参构造器 最近在带团队做一次IOC容器的代码走读同事问我的第一个问题就是Spring 到底是怎么确定用哪个构造器来创建 Bean 的。这个问题看起来基础但真钻进createBean的源码里会发现它比想象中复杂得多。从 BeanPostProcessor 的提前干预到 FactoryBean 的绕行再到 CGLIB 子类策略每个分支都有自己的存在理由绝不是简单的一句“反射调用无参构造器”能概括的。这一篇只聚焦最基础的无参构造器场景把整条链路一步步讲扎实从getBean一路走到ctor.newInstance()看 Spring 如何确定“没有明确指定任何构造器时就用无参构造器”。我会把调用栈、关键源码片段、可复现的验证 Demo 都放出来保证你能在本地跑起来亲眼看到这个过程。这篇文章适合两类人一类是刚开始啃 Spring 源码、想从 Bean 创建这条线入手的读者另一类是搭过 Spring Boot 项目、但对“为什么有时不写Autowired也能注入”感到困惑的人。看完你会发现构造器选择这件事的答案早就写在createBeanInstance的几个分支里了。1. createBean 调用链先搞清楚 Spring 是在哪一步决定构造器的1.1 从 getBean 到 createBeanIOC 容器创建 Bean 的完整主线很多读者一上来就盯着createBean方法看结果被doCreateBean里的一堆逻辑绕晕。我的建议是先退一步把整条调用链摆在面前知道createBean在什么时间点被触发后面才不会被细节淹没。以AnnotationConfigApplicationContext为例容器启动时会执行refresh()其中调用finishBeanFactoryInitialization()这个方法会遍历容器里所有非懒加载的单例 Bean 定义逐个调用getBean(beanName)。getBean内部走的是AbstractBeanFactory.doGetBean()先尝试从三级缓存里拿单例拿不到就进入创建流程。单例 Bean 的创建会走到getSingleton(beanName, () - createBean(beanName, mbd, args))也就是说createBean是在“缓存里没有需要现场制造”的时候才被调用的。这里有个容易被忽略的点createBean的返回值不一定直接作为最终的单例对象返回它还要经过getObjectForBeanInstance()的加工。当 BeanDefinition 声明的是一个FactoryBean时createBean创建出来的是 FactoryBean 本身而真正暴露给使用方的是FactoryBean.getObject()的产物。这个机制和 Spring 里各种“代理工厂”的实现密不可分后面我会单独展开。1.2 createBean 方法的三板斧解析 Class、校验 MethodOverride、预留代理机会createBean在AbstractAutowireCapableBeanFactory中实现它是创建 Bean 的模板方法任何 Bean 的实例化都要经过这里。我把源码做了简化核心逻辑是这样的protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) { RootBeanDefinition mbdToUse mbd; // 1. 解析 Bean 的 Class可能触发类加载 Class? resolvedClass resolveBeanClass(mbd, beanName); if (resolvedClass ! null !mbd.hasBeanClass() mbd.getBeanClassName() ! null) { mbdToUse new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); } // 2. 校验 lookup-method / replace-method 配置 try { mbdToUse.prepareMethodOverrides(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(...); } // 3. 给 InstantiationAwareBeanPostProcessor 一个提前返回代理对象的机会 try { Object bean resolveBeforeInstantiation(beanName, mbdToUse); if (bean ! null) { return bean; } } catch (Throwable ex) { throw new BeanCreationException(...); } // 4. 真正走常规创建流程 try { Object beanInstance doCreateBean(beanName, mbdToUse, args); return beanInstance; } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException(...); } }第一步解析 Class 比较简单本质是把beanClassName字符串交给类加载器加载成Class对象。第二步prepareMethodOverrides()很多人没注意到它校验 BeanDefinition 里配置的lookup-method和replace-method是否存在不存在会直接报错。第三步resolveBeforeInstantiation()是 AOP 和代理机制的一个重要介入点如果某个InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation()返回了非空对象Spring 就不再走常规的构造器实例化流程直接把这个对象当作 Bean 返回。说白了createBean在真正动构造器之前已经给了扩展点两次“劫持”的机会。这解释了为什么你看到的某些 Bean 根本不是通过构造器创建的而是由代理工厂直接生成的。绝大多数普通 Bean 在这两步都不会被拦下于是继续往下走进入doCreateBean而doCreateBean的第一步就是调用createBeanInstance——构造器选择的真正决策中心。2. createBeanInstance构造器选择的核心裁判2.1 五条分岔路每一道都有先后顺序createBeanInstance的职责非常单一给定一个 Bean 的 Class 和 BeanDefinition返回一个包装好实例的BeanWrapper。但它内部的分支判断几乎覆盖了 Spring 实例化 Bean 的所有手段。源码简化后是这样protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { Class? beanClass resolveBeanClass(mbd, beanName); // 分支一Supplier编程式提供实例 Supplier? instanceSupplier mbd.getInstanceSupplier(); if (instanceSupplier ! null) { return obtainFromSupplier(instanceSupplier, beanName, mbd); } // 分支二工厂方法常见于 Bean 注解方法 if (mbd.getFactoryMethodName() ! null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } // 分支三构造器缓存原型 Bean 第二次创建时复用之前的解析结果 boolean resolved false; boolean autowireNecessary false; if (args null) { synchronized (mbd.constructorArgumentLock) { if (mbd.resolvedConstructorOrFactoryMethod ! null) { resolved true; autowireNecessary mbd.constructorArgumentsResolved; } } } if (resolved) { if (autowireNecessary) { return autowireConstructor(beanName, mbd, null, null); } else { return instantiateBean(beanName, mbd); } } // 分支四BeanPostProcessor 返回候选构造器就走自动装配构造器 Constructor?[] ctors determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors ! null || mbd.getResolvedConstructorOrFactoryMethod() ! null) { return autowireConstructor(beanName, mbd, ctors, args); } // 分支五兜底使用无参构造器 return instantiateBean(beanName, mbd); }这五个分支的优先级是硬编码的顺序很重要。Supplier优先级最高因为它是完全编程式的实例化Spring 根本不需要碰构造器其次是Bean方法对应的工厂方法接着是“之前已经解析过的构造器”缓存这里专门用于原型 Bean 的重复创建再下来是询问 BeanPostProcessor 是否有候选构造器最后才是我们这一篇的主角——无参构造器。注意分支四里的determineConstructorsFromBeanPostProcessors()它返回的是一个Constructor?[]数组。如果返回的是nullSpring 才会落到无参构造器那条路。所以“无参构造器场景”的准确前提是没有任何 BeanPostProcessor 认为这个 Bean 需要特殊构造器。看到这里你应该明白为什么 Spring 要单独把这套逻辑抽出来交给后处理器去决策。2.2 determineConstructorsFromBeanPostProcessors为什么构造器决策也要交给后处理器determineConstructorsFromBeanPostProcessors的实现很简洁遍历所有SmartInstantiationAwareBeanPostProcessor一旦某个后处理器返回了非空构造器数组就立即生效protected Constructor?[] determineConstructorsFromBeanPostProcessors(Class? beanClass, String beanName) { if (beanClass ! null (beanClass ! Object.class)) { for (BeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { Constructor?[] ctors ((SmartInstantiationAwareBeanPostProcessor) bp) .determineCandidateConstructors(beanClass, beanName); if (ctors ! null) { return ctors; } } } return null; }容器里默认注册的AutowiredAnnotationBeanPostProcessor就是实现这一接口的关键角色。它内部会遍历beanClass.getDeclaredConstructors()逐个检查构造器上有没有Autowired或Value注解。这里有一个非常重要的细节Spring 4.3 起新增了“唯一构造器推断”逻辑——如果类里只有一个构造器而且这个构造器是有参数的即使没有标Autowired也会被当作候选构造器返回。这就是很多 Spring Boot 项目里不写Autowired也能完成构造器注入的原因。反过来如果类里有多个构造器但都没有标注注解就不会产生候选。最终determineCandidateConstructors()返回nullSpring 认为“你没有表达任何构造器偏好”于是老老实实走无参构造器分支。搞清楚这个逻辑你就能解释很多诡异现象比如一个类写了两个构造器、一个无参一个带参容器启动后居然用的还是无参构造器——因为多构造器场景下 Spring 不会自动帮你猜该用哪个。2.3 别忘了 FactoryBean 这一支为什么你在容器里拿到的 Bean 可能是代理对象前面提到createBean返回后还要经过getObjectForBeanInstance()处理这一节要重点讲 FactoryBean 与代理工厂的关系因为很多读者读源码时会在 FactoryBean 这里卡住。如果一个 BeanDefinition 声明的是FactoryBean比如MyBatis的MapperFactoryBean、Ribbon的RibbonClientFactoryBean那么createBean创建的是 FactoryBean 实例之后会调用它的getObject()返回真正的业务对象。Spring 中大量“代理工厂”就是通过 FactoryBean 实现的FactoryBean 内部持有一个ProxyFactorygetObject()时对目标对象生成代理再返回。也就是说容器里的很多 Bean本质上是“工厂 代理”双重包装的产物。我在实际阅读源码时有很长一段时间把createBean和“最终 Bean”画等号直到排查一个 Bean 类型与预期不符的问题时才注意到getObjectForBeanInstance这条隐藏链路。这也提醒所有读源码的人createBean只是创建阶段类为什么不对先检查它是不是 FactoryBean 的产物。至于无参构造器场景如果你定义的类本身不是 FactoryBean这里就可以暂时放一放把注意力拉回instantiateBean。3. 无参构造器场景的底层实现与实操验证3.1 instantiateBean 和默认实例化策略现在进入正题所有分支都没有命中时Spring 走到instantiateBeanprotected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) { return getInstantiationStrategy().instantiate(mbd, beanName, new BeanFactoryParent(this)); }AbstractAutowireCapableBeanFactory默认的实例化策略是CglibSubclassingInstantiationStrategy这个类名很有迷惑性我第一次看到它时以为每个 Bean 都要走 CGLIB 子类生成。实际上CglibSubclassingInstantiationStrategy继承了SimpleInstantiationStrategy而父类SimpleInstantiationStrategy.instantiate()的逻辑是先判断 BeanDefinition 里有没有methodOverrides没有就直接用反射调构造器只有在存在方法覆盖时才需要 CGLIB 生成子类。public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { // 没有方法覆盖直接反射调用无参构造器 if (!bd.hasMethodOverrides()) { Constructor? constructorToUse; synchronized (bd.constructorArgumentLock) { constructorToUse (Constructor?) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse null) { Class? clazz bd.getBeanClass(); constructorToUse clazz.getDeclaredConstructor(); bd.resolvedConstructorOrFactoryMethod constructorToUse; } } return BeanUtils.instantiateClass(constructorToUse); } else { // 有 lookup-method / replace-method需要 CGLIB 生成子类 return instantiateWithMethodInjection(bd, beanName, owner); } }这里有两个值得注意的细节。第一clazz.getDeclaredConstructor()获取的就是无参构造器如果类里没有显式声明任何构造器JVM 会提供一个默认的无参构造器所以反射依然能拿到。第二解析出来的构造器会被缓存到bd.resolvedConstructorOrFactoryMethod字段里下次再创建同一个原型 Bean 时就不用重新获取这个缓存还加了锁保护虽然简单但有效避免了并发问题。3.2 什么情况下才触发 CGLIBlookup-method / replace-method 与代理子类既然默认策略类名里带着 CGLIB就值得把这条分支讲透。RootBeanDefinition.hasMethodOverrides()返回 true 的典型场景是 XML 中配置了lookup-method和replace-method。前者用于方法级别的依赖注入后者用于替换某个方法的实现体。一旦存在方法覆盖SimpleInstantiationStrategy无法通过普通反射直接创建对象因为 Spring 需要 override 指定方法而 Java 反射并不能修改已加载类的方法体。办法就是CglibSubclassingInstantiationStrategy出场用 CGLIB 的Enhancer为目标类生成一个子类在子类里覆写被覆盖的方法并把方法调用路由到对应的MethodReplacer或BeanFactory的查找逻辑上。从构造器角度看这套 CCGLIB 机制生成的子类在实例化时依然会调用父类的无参构造器所以研究无参构造器场景时CGLIB 子类并不会破坏“无参构造器兜底”的结论。但它提醒我们Spring 里“代理”和“构造器”经常同时出现。一个类即使构造器完全正常只要配置了方法覆盖或 AOP 切面最终容器里存的实际对象就可能是代理子类而不是原始类。这也是为什么设定Transactional的类时断点看到的类型名会带$$后缀。3.3 动手验证在构造器里打断点看完整调用栈理论讲再多不如自己跑一遍。我建议你花十分钟做一个最小 Demo亲眼看看无参构造器是怎么被调到的。第一步新建一个 Maven 工程引入spring-context依赖版本随意5.x 都适用。第二步定义一个普通的 Beanpublic class HelloService { public HelloService() { System.out.println(HelloService 无参构造器被调用); } }第三步用一个最简单的注解配置类启动容器Configuration ComponentScan(com.example) public class AppConfig { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); HelloService helloService context.getBean(HelloService.class); System.out.println(helloService.getClass()); context.close(); } }第四步在HelloService的无参构造器里打上断点以 Debug 模式启动。等断点命中后查看 IDEA 的调用栈面板你会看到类似这样的层级HelloService.init() BeanUtils.instantiateClass(Constructor, Object...) SimpleInstantiationStrategy.instantiate(RootBeanDefinition, String, BeanFactory) AbstractAutowireCapableBeanFactory.instantiateBean(String, RootBeanDefinition) AbstractAutowireCapableBeanFactory.createBeanInstance(String, RootBeanDefinition, Object[]) AbstractAutowireCapableBeanFactory.doCreateBean(String, RootBeanDefinition, Object[]) AbstractAutowireCapableBeanFactory.createBean(String, RootBeanDefinition, Object[]) DefaultSingletonBeanRegistry.getSingleton(String, ObjectFactory) AbstractBeanFactory.doGetBean(String, Class, Object[], boolean) AbstractBeanFactory.getBean(String) DefaultListableBeanFactory.preInstantiateSingletons()这个调用栈非常清晰地展示了从容器启动到构造器执行的完整路径。你可以自己再试一种情况在 HelloService 中声明一个带参构造器但不标Autowired同时再声明一个无参构造器然后启动容器。此时determineCandidateConstructors因为“多个构造器且无注解”返回nullSpring 依然会调用无参构造器。这就能直接验证多构造器场景下的兜底行为。建议你在determineConstructorsFromBeanPostProcessors方法上也加一个断点观察它返回的ctors是不是null。这一步是整个决策链的灵魂我为了确认AutowiredAnnotationBeanPostProcessor的返回结果还专门看了它的源码返回值才彻底想明白无参构造器分支的触发条件。3.4 经典报错分析为什么“没写无参构造器”会翻车和构造器相关的经典报错我在群里见过太多次了。最常见的就是这样的场景一个类写了多个带参构造器但都没标Autowired同时也没有无参构造器。启动时直接报No default constructor found堆栈指向AbstractAutowireCapableBeanFactory.createBeanInstance或SimpleInstantiationStrategy.instantiate。原因现在很清楚了多构造器且无注解时期determineCandidateConstructors返回nullSpring 不会替你选择某个带参构造器而是去clazz.getDeclaredConstructor()找无参构造器。找不到就抛出BeanInstantiationException。这里的处理逻辑很“死板”但正是这种死板保证了规则的确定性和可预期性。另一种让人困惑的情况是类里只有一个带参构造器没写无参构造器也没标Autowired容器却能正常启动。这是 Spring 4.3 之后“唯一构造器”自动装配规则的功劳。AutowiredAnnotationBeanPostProcessor看到唯一构造器且参数个数大于 0 时会把它当作候选返回于是走autowireConstructor分支根本不碰无参构造器。也就是说“没有无参构造器”不一定报错得看你类的构造器数量。为了直观我整理了一个小表格类里的构造器情况determineCandidateConstructors 返回实际走的路径最终效果没有显式构造器默认无参nullinstantiateBean调用隐式无参构造器显式无参构造器nullinstantiateBean调用无参构造器多个构造器无注解nullinstantiateBean调用无参构造器没有则报错唯一带参构造器且参数非空返回该构造器数组autowireConstructor自动按参数类型注入多个构造器其中一个 Autowired返回候选数组autowireConstructor使用标注的构造器这张表基本覆盖了无参构造器场景的所有变体。建议收藏排查问题的时候很有用。4. 常见问题与排查心得4.1 遇到 “No default constructor” 先别急着改代码很多人一看到No default constructor found就条件反射式地往类里补一个无参构造器这当然能解决一部分问题但属于“头痛医头”。我建议先按下面这个顺序排查第一步数一下这个类到底有几个构造器。只有一个带参构造器“唯一构造器自动装配”规则会生效理论上不会报这个错除非你用的是 Spring 4.3 之前的版本或者构造器参数类型在容器里找不到对应的 Bean。第二步看构造器上有没有Autowired或Value。加了注解的构造器会优先走autowireConstructor不会去找无参构造器。如果注解标了required false但容器又无法提供参数可能导致注入失败这个行为和直接找无参构造器不一样别混淆。第三步检查 BeanDefinition 上有没有constructor-arg配置。XML 或编程式配置了构造参数就会强制走autowireConstructor分支即使类里有无参构造器也不影响。第四步确认这个类是不是被某个框架包装成了 FactoryBean 或代理类。有些报错看起来是“目标类没无参构造器”实际上问题出在代理子类或者 Mapper 扫描上。遇到这种情况先看堆栈中抛出异常的是不是CglibSubclassingInstantiationStrategy。这四步走完再决定是加无参构造器还是加Autowired构造器会比直接改代码稳妥得多。4.2 构造器选择问题速查表排查时往往会来回翻源码我把高频场景做成了一张速查表方便你对照现象根因解决方向多个带参构造器且无无参构造器启动报 No default constructordetermineCandidateConstructors 返回 nullSpring 找无参失败补一个无参构造器或给某个带参构造器加 Autowired只有一个带参构造器无需 Autowired 也能注入Spring 4.3 唯一构造器推断无需改动理解即可两个构造器一个无参一个带参且带参标注 Autowired标注构造器被选为候选走 autowireConstructor 逻辑构造器上标了 Value但值解析失败autowireConstructor 处理参数值时出错检查占位符配置或默认值配置了 lookup-method实例类型变成 CGLIB 子类方法覆盖触发子类生成正常现象不需纠正容器返回的 Bean 类型与你定义的类不一致FactoryBean 或 AOP 代理检查 BeanDefinition 是否声明的 FactoryBean这张表是我在实际踩坑中总结的不一定覆盖所有情况但能把大部分构造器相关问题定位到具体分支。4.3 本次阅读源码的踩坑记录与后续预告读 Spring 源码时我犯过的最大错误是过早进入doCreateBean的细节忽略了createBean前三步中resolveBeforeInstantiation和prepareMethodOverrides的拦截作用。实际上构造器选择本身并不复杂复杂的是它身边环绕的各种“提前干预”机制。只看createBeanInstance的代码会觉得 Spring 做了五层判断把前置扩展点也放进来才意识到构造器只是最后一道关口前面已经放行了很多代理和替代方案。另一个容易误解的地方是determineConstructorsFromBeanPostProcessors返回null的含义。它不是“什么都没有”的意思而是“没有候选构造器需要 autowire”。Spring 用 null 表示不需要走特殊构造器走无参兜底用非空数组表示需要 autowire。这个语义区分清楚了后面读autowireConstructor会轻松很多。下一篇我打算接着写Autowired标注在构造器上的场景required 属性的处理、多构造器中的优先级、以及 Spring 怎么推断构造器参数名称。那套逻辑比这一篇要复杂得多先把无参这条链路吃透后面再往上叠加就不会乱了。
返回列表