Java基础|Spring问题研究之bean的属性xml注入List类型不匹配

一、问题描述 今天在Java群里看到“白日梦想家” 的一个提问,很有意思:

Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

为什么 String类型的列表 通过spring的属性注入 可以注入Integer类型的元素呢?

二、问题分析过程 经过一番调试发现了关键所在(Spring 5.0.10.Release版本代码):

AbstractAutowireCapableBeanFactory类中的applyPropertyValues函数将属性值PropertyValues解析到beanName对应的Bean的属性上。

/** * Apply the given property values, resolving any runtime references * to other beans in this bean factory. Must use deep copy, so we * don't permanently modify this property. * @param beanName the bean name passed for better exception information * @param mbd the merged bean definition * @param bw the BeanWrapper wrapping the target object * @param pvs the new property values */ protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { // ① 如果PropertyValues为空,直接返回 if (pvs.isEmpty()) { return; }// ②判断安全管理器 if (System.getSecurityManager() != null && bw instanceof BeanWrapperImpl) { ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); }MutablePropertyValues mpvs = null; List original; // ③ 获取bean的属性集合 // 如果pvs是MutablePropertyValues的实例,MutablePropertyValues是PropertyValues的默认实现 if (pvs instanceof MutablePropertyValues) { // 将pvs转换为MutablePropertyValues对象,并判断mpvs是否已经经过转换 mpvs = (MutablePropertyValues) pvs; if (mpvs.isConverted()) { // Shortcut: use the pre-converted values as-is. // 如果pvs已经转换过,则直接设置属性值无需再次转换 try { bw.setPropertyValues(mpvs); return; } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Error setting property values", ex); } } // 否则获取原始PropertyValue集合 original = mpvs.getPropertyValueList(); } else { original = Arrays.asList(pvs.getPropertyValues()); } // ④ 获取类型转换器 TypeConverter converter = getCustomTypeConverter(); if (converter == null) { converter = bw; } BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); // Create a deep copy, resolving any references for values. // ⑤ 通过深度拷贝,解析值引用 List deepCopy = new ArrayList<>(original.size()); boolean resolveNecessary = false; // 循环解析PropertyValues for (PropertyValue pv : original) { if (pv.isConverted()) { deepCopy.add(pv); } else { //获取属性值 String propertyName = pv.getName(); Object originalValue = https://www.it610.com/article/pv.getValue(); // 解析原始属性值 Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); Object convertedValue = resolvedValue; // isWritableProperty 判断属性是否可写,如果属性不存在返回false // isNestedOrIndexedProperty 判断是否索引属性或者嵌套属性boolean convertible = bw.isWritableProperty(propertyName) && !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); if (convertible) { convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); } // Possibly store converted value in merged bean definition,in order to avoid re-conversion for every created bean instance. // ⑥缓存已经转换过的值,避免再次转换 if (resolvedValue == originalValue) { if (convertible) { pv.setConvertedValue(convertedValue); } deepCopy.add(pv); } else if (convertible && originalValue instanceof TypedStringValue && !((TypedStringValue) originalValue).isDynamic() && !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { pv.setConvertedValue(convertedValue); deepCopy.add(pv); } else { resolveNecessary = true; deepCopy.add(new PropertyValue(pv, convertedValue)); } } } if (mpvs != null && !resolveNecessary) { mpvs.setConverted(); }// Set our (possibly massaged) deep copy. // ⑦设置属性值. try { bw.setPropertyValues(new MutablePropertyValues(deepCopy)); } catch (BeansException ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName,"Error setting property values", ex); } }



最关键的在这行代码(它对List中元素的类型进行类型转换):
boolean convertible = bw.isWritableProperty(propertyName) && !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); if (convertible) { convertedValue = https://www.it610.com/article/convertForProperty(resolvedValue, propertyName, bw, converter); }


【Java基础|Spring问题研究之bean的属性xml注入List类型不匹配】具体转换在convertForProperty的函数调用:
TypeConverterDelegate的convertIfNecessary

public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {// Custom editor for this type? PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); ConversionFailedException conversionAttemptEx = null; // No custom editor but custom ConversionService specified? ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) { TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { try { return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } catch (ConversionFailedException ex) { // fallback to default conversion logic below conversionAttemptEx = ex; } } }Object convertedValue = https://www.it610.com/article/newValue; // Value not of required type? if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && convertedValue instanceof String) { TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor(); if (elementTypeDesc != null) { Class elementType = elementTypeDesc.getType(); if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) { convertedValue = https://www.it610.com/article/StringUtils.commaDelimitedListToStringArray((String) convertedValue); } } } if (editor == null) { editor = findDefaultEditor(requiredType); } convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); }boolean standardConversion = false; if (requiredType != null) { // Try to apply some standard type conversion rules if appropriate.if (convertedValue != null) { if (Object.class == requiredType) { return (T) convertedValue; } else if (requiredType.isArray()) { // Array required -> apply appropriate conversion of elements. if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) { convertedValue = https://www.it610.com/article/StringUtils.commaDelimitedListToStringArray((String) convertedValue); } return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType()); } else if (convertedValue instanceof Collection) { // Convert elements to target type, if determined. convertedValue = convertToTypedCollection( (Collection) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } else if (convertedValue instanceof Map) { // Convert keys and values to respective target type, if determined. convertedValue = https://www.it610.com/article/convertToTypedMap( (Map) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) { convertedValue = https://www.it610.com/article/Array.get(convertedValue, 0); standardConversion = true; } if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) { // We can stringify any primitive value... return (T) convertedValue.toString(); } else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) { if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) { try { Constructor strCtor = requiredType.getConstructor(String.class); return BeanUtils.instantiateClass(strCtor, convertedValue); } catch (NoSuchMethodException ex) { // proceed with field lookup if (logger.isTraceEnabled()) { logger.trace("No String constructor found on type [" + requiredType.getName() + "]", ex); } } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", ex); } } } String trimmedValue = https://www.it610.com/article/((String) convertedValue).trim(); if (requiredType.isEnum() &&"".equals(trimmedValue)) { // It's an empty enum identifier: reset the enum value to null. return null; } convertedValue = https://www.it610.com/article/attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue); standardConversion = true; } else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) { convertedValue = NumberUtils.convertNumberToTargetClass( (Number) convertedValue, (Class) requiredType); standardConversion = true; } } else { // convertedValue =https://www.it610.com/article/= null if (requiredType == Optional.class) { convertedValue = Optional.empty(); } }if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) { if (conversionAttemptEx != null) { // Original exception from former ConversionService call above... throw conversionAttemptEx; } else if (conversionService != null && typeDescriptor != null) { // ConversionService not tried before, probably custom editor found // but editor couldn't produce the required type... TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } }// Definitely doesn't match: throw IllegalArgumentException/IllegalStateException StringBuilder msg = new StringBuilder(); msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue)); msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'"); if (propertyName != null) { msg.append(" for property '").append(propertyName).append("'"); } if (editor != null) { msg.append(": PropertyEditor [").append(editor.getClass().getName()).append( "] returned inappropriate value of type '").append( ClassUtils.getDescriptiveType(convertedValue)).append("'"); throw new IllegalArgumentException(msg.toString()); } else { msg.append(": no matching editors or conversion strategy found"); throw new IllegalStateException(msg.toString()); } } }if (conversionAttemptEx != null) { if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) { throw conversionAttemptEx; } logger.debug("Original ConversionService attempt failed - ignored since " + "PropertyEditor based conversion eventually succeeded", conversionAttemptEx); }return (T) convertedValue; }

的213行处实现转换,转换前(注意观察convertedValue,集合的元素类型),转换前为整型:
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

转换后为字符串类型:
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

其中TypeConverterDelegate的convertToTypedCollection代码如下:
private Collection convertToTypedCollection(Collection original, @Nullable String propertyName, Class requiredType, @Nullable TypeDescriptor typeDescriptor) {if (!Collection.class.isAssignableFrom(requiredType)) { return original; }boolean approximable = CollectionFactory.isApproximableCollectionType(requiredType); if (!approximable && !canCreateCopy(requiredType)) { if (logger.isDebugEnabled()) { logger.debug("Custom Collection type [" + original.getClass().getName() + "] does not allow for creating a copy - injecting original Collection as-is"); } return original; }boolean originalAllowed = requiredType.isInstance(original); TypeDescriptor elementType = (typeDescriptor != null ? typeDescriptor.getElementTypeDescriptor() : null); if (elementType == null && originalAllowed && !this.propertyEditorRegistry.hasCustomEditorForElement(null, propertyName)) { return original; }Iterator it; try { it = original.iterator(); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Cannot access Collection of type [" + original.getClass().getName() + "] - injecting original Collection as-is: " + ex); } return original; }Collection convertedCopy; try { if (approximable) { convertedCopy = CollectionFactory.createApproximateCollection(original, original.size()); } else { convertedCopy = (Collection) ReflectionUtils.accessibleConstructor(requiredType).newInstance(); } } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Cannot create copy of Collection type [" + original.getClass().getName() + "] - injecting original Collection as-is: " + ex); } return original; }int i = 0; for (; it.hasNext(); i++) { Object element = it.next(); String indexedPropertyName = buildIndexedPropertyName(propertyName, i); // 对集合中的每个元素进行转换(如果需要) Object convertedElement = convertIfNecessary(indexedPropertyName, null, element, (elementType != null ? elementType.getType() : null) , elementType); try { convertedCopy.add(convertedElement); } catch (Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Collection type [" + original.getClass().getName() + "] seems to be read-only - injecting original Collection as-is: " + ex); } return original; } originalAllowed = originalAllowed && (element == convertedElement); } return (originalAllowed ? original : convertedCopy); }
其中此处为集合中每个元素进行转换(再次调用convertIfNecessary函数)
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

经过上面分析,我们知道List可以顺利注入到List中的原因了,因为中间经历了属性转换。

另外有一个童鞋提出可以将配置文件中节点的值改为字符串如下图所示:
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

然后注入List的Bean属性中。
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

我们发现会报错:
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

那么说明 字符串无法通过上面的转换函数转成整数吗?
另外我们注意到代码企图利用Integer.valueOf函数将字符串转成整型,按道理说应该是可以的。
public static Integer valueOf(String s) throws NumberFormatException { return Integer.valueOf(parseInt(s, 10)); }

但是我们根据报错如果我们细心可以发现这里并不是字符串1 ("1")而是字符串(""1""), 红色部分表示字符串的实际内容。

那我们再次修改配置文件
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

我们启动项目发现一切正常。
我们打条件断点回到之前的位置查看
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

走过如上代码后字符串类型的集合转成了整数集合
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片


因此如果是可以转换的类型Spring会对属性进行转换,如果是无法转换将会报错。特别要注意这里的
"1" "2"

的含义并不是字符串1和字符串2而是字符串"1"(三个字符组成)和字符串"2"。
为了证明我们的想法,重新打断点:
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

发现的确如此,将字符串 "1"(三个字符)通过Integer.valueOf转成整型显然会报错。
另外我们如果将属性修改如下:
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片

显然这里的字符串s无法转换为整型(字符串1 可以),会报错。

另外我们根据报错可以了解Spring创建Bean的大致步骤(每一部分调用顺序都是从下往上)
Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片


Java基础|Spring问题研究之bean的属性xml注入List类型不匹配
文章图片


三、总结: 遇到问题可以浅尝辄止,也可以借此机会深入了解问题的本源,对熟悉源码加深理解有很大帮助。
建议大家多拉取核心技术栈的源码,遇到问题多分析调试,理解会更好一些。
遇到问题是研究源码的最好的时机,每一次研究对技术的进步都有很大帮助。
另外下载源码后想了解某个类的某个方法的使用方式,可以右键find usages找到对应的单元测试后打断点进行调试,学习的效果非常好。

附:SpringBoot源码环境搭建的正确姿势
https://blog.csdn.net/w605283073/article/details/85106902

    推荐阅读