ARTICLE DETAIL

资讯详情

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

Spring源码解析:构造器注入的类型转换与候选匹配机制

Spring源码解析:构造器注入的类型转换与候选匹配机制 看Spring源码看到createBean这一步很多人会卡在同一个地方类里明明有好几个构造器Spring是怎么决定用哪一个的决定了之后配置里写的是8080这样的字符串构造器参数却是Integer port这中间谁做了转换更麻烦的是一个接口有两个实现类构造器注入时凭什么选A不选B如果你跟完了这个系列的前几篇应该已经见过createBeanInstance和determineCandidateConstructors了但真正让构造器参数被正确填满的是进入autowireConstructor之后那一大段类型转换与匹配权重的逻辑。这篇就把最后这段链路拆开讲透。1. 构造器决策发生在哪createBeanInstance里的候选解析链路1.1 从determineConstructorsFromBeanPostProcessors到autowireConstructor先回到实例化的起点。在AbstractAutowireCapableBeanFactory.createBeanInstance里Spring并不是一上来就盲目调构造器而是先尝试找出候选构造器protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Nullable Object[] args) { Supplier? instanceSupplier mbd.getInstanceSupplier(); if (instanceSupplier ! null) { return obtainFromSupplier(instanceSupplier, beanName, mbd); } if (mbd.getFactoryMethodName() ! null) { return instantiateUsingFactoryMethod(beanName, mbd, args); } // 通过BeanPostProcessor找出候选构造器 Constructor?[] ctors determineConstructorsFromBeanPostProcessors(beanClass, beanName); if (ctors ! null || mbd.getResolvedAutowireMode() RootBeanDefinition.AUTOWIRE_CONSTRUCTOR) { return autowireConstructor(beanName, mbd, ctors, null); } // 没有候选构造器走默认无参构造器 return instantiateBean(beanName, mbd); }注意这里有个关键判断ctors ! null。如果determineConstructorsFromBeanPostProcessors返回了非空数组Spring就会进入autowireConstructor把我们这个系列的主角真正请出来。反之如果返回nullSpring就会认为这个bean没有特别指定构造器然后走默认的instantiateBean。determineConstructorsFromBeanPostProcessors的逻辑不复杂它只是遍历所有SmartInstantiationAwareBeanPostProcessor调用determineCandidateConstructors把第一个非空结果返回。真正干活的通常是AutowiredAnnotationBeanPostProcessor。它的determineCandidateConstructors方法里有几个非常明确的规则如果一个类里有多个构造器都标注了Autowired(required true)直接抛出异常Spring不允许你指鹿为马。如果标注了Autowired(required false)这些构造器会被收集到候选数组中。如果没有找到任何Autowired构造器但类里只有一个带参构造器且不是默认无参构造器Spring 4.3之后也认这个构造器。如果没有候选返回null后续走无参构造器实例化。所以候选构造器的来源就两条路显式加注解或者单构造器自动推断。很多人以为autowireConstructor会把类里所有构造器都拿来比较其实不是。它拿到的是候选数组最多也就两三个。1.2 多个候选构造器时Spring的粗筛逻辑当一个类里存在多个Autowired(required false)构造器时autowireConstructor内部会做一次粗筛。大致的逻辑是遍历候选构造器看参数数量、参数类型是否能被容器中的bean满足能全部满足且满足度最高的那个胜出。参数多并不意味着一定被选中如果参数多的那个构造器有一个参数在容器里找不到任何候选beanSpring就会排除它退而选择参数少但能完全满足的构造器。这种从多到少尝试、失败就降级的策略本质上就是一个匹配权重问题。虽然Spring源码里没有直接定义一个叫weight的字段但选择顺序就是权重参数数量优先、可满足性其次、注解的required再往后排。平时我们最好别构造这种模糊场景多个Autowired(required false)构造器会让代码很难读而且Spring的选择结果不是那么直观。如果必须支持多个构造器建议其中一个用Autowired(required true)锁死比让Spring猜更安全。2. 参数值从哪来resolvePreparedArguments与createArgumentArray的职责划分2.1 值来源只有三类显式字面量、bean引用、嵌套BeanDefinition进入autowireConstructor后Spring要解决的第一件事是构造器参数的值到底从哪拿我最初读源码时在这绕了很久后来才明白ConstructorResolver把构造器参数值的来源分得非常清晰就三类。第一类是显式字面量对应TypedStringValue。比如XML里的constructor-arg value30/或者注解里的Value(${order.timeout:30})。这类值本质上是字符串后面必须经过类型转换才能变成真正的参数类型。第二类是bean引用对应RuntimeBeanReference。比如XML里constructor-arg refpaymentGateway/表示这个参数应该从容器中找出名为paymentGateway的bean。第三类是嵌套的BeanDefinitionHolder也就是在constructor-arg里直接内嵌一个子bean定义。这种现在很少见了Spring Boot时代基本都是注解配置但不代表源码里没有。这三类值会封装在ConstructorArgumentValues.ValueHolder里然后在createArgumentArray中逐个解析。2.2 显式参数与自动装配参数是如何合并的createArgumentArray这个方法的参数很多容易看花眼但核心思路很清晰先把外部传入的argsToUse准备好再对每个构造器参数调用getArgumentValue找到对应的ValueHolder最后解析值。如果RootBeanDefinition里配置了构造器参数值比如XML里写了constructor-arg index0 value30/Spring会按索引精准匹配。如果只有名字没有索引比如constructor-arg nametimeout value30/Spring会把参数名和构造器参数列表做匹配找到对应索引。麻烦的是部分显式参数 剩余自动装配这种混合模式。Spring的处理方法是先把带索引或带名字的显式参数解析掉剩下没有对应ValueHolder的参数按AUTOWIRE_CONSTRUCTOR模式继续解析。自动装配模式下每个参数会被包装成DependencyDescriptor交给beanFactory.resolveDependency去处理。这一步会触发我们后面要讲的匹配权重逻辑而不再是从BeanDefinition里取值了。这里有一个容易踩坑的细节如果你手动指定了某个构造器参数值但指定值的类型和构造器参数类型不匹配Spring不会在这个阶段放弃它会尝试调用类型转换器来兜底。举个例子构造器是OrderService(int timeout)你只给了30这个字符串createArgumentArray会把这个字符串转成Integer转换成功就继续转换失败才抛异常。所以类型转换不是额外步骤而是构造器参数落地过程中绕不开的一环。3. 类型转换TypeConverterDelegate在构造器参数上的强制改型3.1 convertIfNecessary的三级转换顺序先看Spring在哪里做类型转换。在createArgumentArray解析完原始值之后它会调用beanFactory.getTypeConverter().convertIfNecessary(originalValue, paramType, param)最终落到TypeConverterDelegate.convertIfNecessary。这个方法签名很长但做的事情非常直观把originalValue这个原始对象转换成paramType要求的目标类型。转换顺序是有讲究的不是一上来就硬转。第一步Spring会检查有没有为这个目标类型注册了自定义的PropertyEditor。比如你定义了一个OrderCodeEditor继承PropertyEditorSupport专门把字符串转成OrderCode对象那Spring会优先用它。第二步如果当前BeanFactory配置了ConversionServiceSpring会优先交给ConversionService处理。ConversionService是Spring 3.0之后推荐的转换体系比PropertyEditor更简单也更安全因为PropertyEditor本身是有状态的不是线程安全而ConversionService里的Converter是无状态的。第三步如果前面都不满足Spring才会用JDK内置的PropertyEditorManager兜底比如StringToInteger这种默认编辑器。我整理了一个对照表方便理解特性PropertyEditorConversionService / ConverterAPI年龄老JDK自带Spring 3.0后引入线程安全通常有状态不能共享无状态线程安全注册方式PropertyEditorRegistrar/InitBinderConverter实现类注册适用场景旧项目维护、特殊控件绑定现代Spring Boot推荐出错信息相对模糊错误信息更清晰对于构造器注入来说InitBinder是不会生效的因为InitBinder是Spring MVC为Web数据绑定准备的。如果你发现自己注册了InitBinder但构造器参数转换完全不理你别奇怪这是设计如此。所以要让构造器参数支持自定义类型转换正确做法是注册一个Converter并把ConversionService设置到ConfigurableBeanFactory上或者直接注册成Spring Boot的ConverterBean让自动配置帮你接好。3.2 集合泛型与Value占位符的转换细节类型转换里最折磨人的是泛型集合。比如构造器参数是ListLong ids而你给的是1,2,3。Spring拿到这个字符串后会先通过ResolvableType解析出List的元素类型是Long然后交给StringToCollectionConverter把字符串按逗号拆成数组再对每个元素调用StringToNumberConverterFactory最终得到ListLong。这个过程在源码里跨了好几个转换器但Spring的GenericConversionService能把它们串起来。调试时如果发现泛型元素转换失败一定要看异常栈里有没有ConversionFailedException它会告诉你到底是哪个元素转不过去。再来看Value和占位符的嵌套处理。如果你写了public OrderService(Value(${order.timeout:30}) Integer timeout) { ... }Spring在resolveValueIfNecessary阶段会先做一次resolveEmbeddedValue把${order.timeout:30}解析成30然后再对这个字符串做类型转换。这里有两个典型的坑第一个坑占位符解析失败不一定发生在类型转换阶段而是发生在属性解析阶段。如果配置里没有order.timeout而且没写默认值你会看到IllegalArgumentException: Could not resolve placeholder order.timeout in value ${order.timeout}。这时候别去查TypeConverterDelegate先把配置补上。第二个坑Value里写SpEL表达式时转换时机不一样。比如Value(#{systemProperties[user.region]}) Locale localeSpring会先执行SpEL表达式求值拿到结果后再进行类型转换。如果表达式求出来就是Locale对象那转换直接通过如果求出来是字符串zh_CNSpring会尝试用StringToLocale转换器转换。很多人以为Value只是取属性其实它还内置了一个小型表达式引擎属性占位符和SpEL是两条不同的路。集合类型的转换也有一个实战建议不要在构造器参数里用太复杂的泛型嵌套。比如MapString, ListLong这种转换器虽然能处理但一旦出错排查成本非常高。如果配置值结构真的很复杂建议外部先组装成对象再传给构造器别让Spring硬猜。4. 匹配权重doResolveDependency选出构造器参数正主的判决书4.1 determineAutowireCandidate的裁决顺序现在进入标题里的另一个关键词匹配权重。当构造器参数是PaymentGateway paymentGateway这种接口类型时Spring不能直接new一个对象它必须去容器里找出所有类型匹配的bean然后从中选一个。这个选一个的过程就是匹配权重发挥作用的地方。入口在DefaultListableBeanFactory.doResolveDependency。它会先调用findAutowireCandidates把容器里所有类型匹配的bean都找出来放进一个MapString, Object。如果这个Map只有一个候选那不存在竞争直接返回。如果有多个候选就进入determineAutowireCandidate做最终判决。determineAutowireCandidate的裁决顺序可以简化成下面这张表生效顺序判定依据说明1参数上的Qualifier限定在候选收集阶段就过滤掉不匹配的bean2Primary标记被标记的bean在多个候选中优先使用3Priority注解值JSR-250标准数字越小优先级越高4参数名与beanName匹配开启-parameters编译参数后可用5唯一候选兜底如果过滤后只剩一个直接采用注意Qualifier并不是在determineAutowireCandidate里才判断的它更早在findAutowireCandidates阶段就会用QualifierAnnotationAutowireCandidateResolver去过滤候选。如果参数上写了Qualifier(alipay)容器里只有alipayGateway这个名字对应的bean符合那么后面的Primary、Priority根本来不及参与因为候选已经被缩到唯一了。所以Qualifier更像是海选淘汰赛Primary和Priority才是决赛评分。4.2 Priority与参数名匹配经常被忽略的隐性权重Primary大家用得比较多它很好理解多个候选时标记了Primary的那个直接获胜。Priority很多人容易和Primary搞混。Priority来自jakarta.annotation.Priority不是Spring专属。它的含义是给候选bean一个优先级数字数字越小越优先。Spring在determineAutowireCandidate里会遍历候选bean如果某个候选标注了Priority然后对比每个候选的优先级只保留order最小的那个。同一个场景里如果既有Primary又有Priority怎么办源码里是先看Primary也就是说Primary的效力高于Priority。我见过有人两个注解都标结果行为和自己预期不一样就是因为没搞清楚这个先后关系。还有一个特别容易踩的隐性问题参数名匹配。假设构造器参数叫paymentGateway容器里恰好有一个beanName也叫paymentGateway那么即便有多个候选Spring也可能直接选中这个bean不需要Primary。但这里有个前提Java编译时必须开启-parameters参数保留方法参数名。如果你用的是旧项目编译时没开这个参数Spring会拿不到参数名它只能用LocalVariableTableParameterNameDiscoverer去解析class文件里的调试符号。一旦编译时连-g都没开参数名就是arg0、arg1这种匹配权重就会失效Spring只能退回去靠Primary、Qualifier决定。所以在实际项目里我不建议依赖参数名匹配来做构造器注入。它看起来很智能但环境一变就失灵。最稳妥的做法是确定唯一的首选项用Primary需要根据业务场景精确指定用Qualifier不要指望参数名恰好等于beanName这种巧合4.3 多个候选导致NoUniqueBeanDefinitionException的经典场景如果不做任何标记两个实现类都没有Primary构造器参数上也没有QualifierSpring就会抛NoUniqueBeanDefinitionException。异常信息通常长这样No qualifying bean of type com.example.PayGateway available: expected single matching bean but found 2: alipayGateway,wechatPayGateway这个异常很多人见过但没细想过为什么Spring不自动选一个。原因很简单Spring觉得它没有足够信息判断哪个更合适宁愿报错也不猜测避免运行时行为不可预期。排查这个问题我一般会在DefaultListableBeanFactory.doResolveDependency里打一个条件断点条件就是descriptor.getDependencyName()不为空。这样可以清楚看到候选列表和参数名很快就能定位是哪个注入点出的问题。修复方案就三种在其中一个实现类上加Primary告诉Spring默认用它。在构造器参数上加Qualifier(alipay)精确指定bean名。调整参数名和beanName一致并确保编译时开启-parameters。我个人推荐前两种因为它们不依赖编译配置代码里一看就懂。5. 一个完整案例又涉及转换又涉及权重的构造器注入5.1 代码和配置为了把类型转换和匹配权重串起来我写一个典型例子。假设有个支付网关接口两个实现类以及一个OrderService构造器需要注入PayGateway和一个超时时间public interface PayGateway { String pay(); } Component Primary public class AlipayGateway implements PayGateway { Override public String pay() { return alipay; } } Component public class WechatPayGateway implements PayGateway { Override public String pay() { return wechat; } } Component public class OrderService { private final PayGateway payGateway; private final Integer timeout; public OrderService(Value(${order.timeout:30}) Integer timeout, PayGateway payGateway) { this.timeout timeout; this.payGateway payGateway; } }这里的两个参数分别对应两种机制timeout参数先由Value占位符解析再把字符串30转换成Integer。payGateway参数按类型找候选因为两个实现类都匹配最后由Primary决定选AlipayGateway。5.2 断点验证与异常复盘如果你想知道Spring究竟按什么顺序处理可以在两个位置分别打断点。第一个断点打在ConstructorResolver.createArgumentArray。你会看到Spring先处理timeout参数它的原始值是一个TypedStringValue在解析过程中会调用convertIfNecessary。此时断点进入TypeConverterDelegate能看到原始字符串30和目标类型Integer转换结果就是30。第二个断点打在DefaultListableBeanFactory.doResolveDependency。当Spring处理payGateway参数时会进入这个方法。看matchingBeans这个Map里面有alipayGateway和wechatPayGateway两个候选。继续往下走到determineAutowireCandidateSpring会判断AlipayGateway有Primary于是直接选中它。现在我们把配置故意改坏看看异常怎么追。先把order.timeout配置成abc启动时你会看到Failed to convert value of type java.lang.String to required type java.lang.Integer; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value abc异常栈会直接指向TypeConverterDelegate。这时候不要怀疑构造器选择逻辑问题就出在类型转换。再把Primary去掉order.timeout改回30你看到的就是最经典的NoUniqueBeanDefinitionException。从这个异常栈进去能看到findAutowireCandidates返回了两个候选然后determineAutowireCandidate谁都没选直接抛出。最后说一个和这俩机制都相关的实战经验如果你的构造器参数是OptionalPayGateway、PayGateway[]、ListPayGateway这种集合或包装类型Spring不会走单候选的determineAutowireCandidate而是走resolveMultipleBeans把所有匹配的bean都收集起来。这时候Primary反而不起作用因为集合要的是全部候选。很多人把Primary放在集合注入上发现没效果就是这个原因。我自己在排查这类问题时习惯先在TypeConverterDelegate.convertIfNecessary和determineAutowireCandidate两个方法上各打一个断点因为一个管参数值怎么变成想要的类型一个管多个同类型bean里怎么选。这两个断点一开构造器注入的全过程基本就暴露在眼前了。看完这条链路你会发现Spring的构造器推断一点都不玄乎先找候选再准备参数参数类型不对就转换类型对了但候选多就按权重选权重都救不了就老实报错。这就是createBean里构造器选择的最后一环。
返回列表