<mvc:resources />标签新老版本解析不同,是bug还是?

先来说说这个坑爹的问题,其实本来我是没注意到的,因为程序跑起来一切都正常。但是在tomcat启动时飞速打印log时,在中间“隐藏”了一个错误:

2015-02-15 16:03:22 [ catalina-exec-4:2202 ] - [ DEBUG ] [org.springframework.beans.TypeConverterDelegate] Original ConversionService attempt failed - ignored since PropertyEditor based conversion eventually succeeded
org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.util.ArrayList<?> to type java.util.List<org.springframework.core.io.Resource> for value ‘[classpath:/plugin/assets/]‘; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type java.lang.String to type org.springframework.core.io.Resource
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:168)
    at ........此处省略
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type ... 61 more

看这个错误应该是在类型转换的时候报的错。也就是将

<mvc:resources location="" mapping="" />

中的location转换成List<Resource>的时候报错的。Spring3中使用的是ConversionService来对外提供转换接口,这里我确实注册了一个ConversionService,但那也只是在默认之上添加了一个自己实现的DateFormatter,应该不会有影响的。

那到底是什么原因呢?我觉得像我这样的人,星期六在家完全不适合碰到稍微需要分析一下的问题。因为我容易逃避啊,觉得烦就跑去打Dota了,图个脑袋轻松。还好今天上班,可以来好好地分析一下这个问题。

其实我一开始并没有想到是<mvc:resources />标签解析的问题,而是按照Dispatcher启动的顺序来做调试的。关于Dispatcher的启动过程,我打算在下一篇文章中来记录。通过上面繁琐的跟踪,最终定位到org.springframework.beans.TypeConverterDelegate类中的
convertIfNecessary(String propertyName, Object oldValue, Object newValue, Class<T> requiredType, TypeDescriptor typeDescriptor)方法:

    public <T> T convertIfNecessary(String propertyName, Object oldValue, Object newValue,
            Class<T> requiredType, TypeDescriptor typeDescriptor) throws IllegalArgumentException {

        Object convertedValue = newValue;

        // Custom editor for this type?
        PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

        ConversionFailedException firstAttemptEx = null;

        // No custom editor but custom ConversionService specified?
        ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
        if (editor == null && conversionService != null && convertedValue != null && typeDescriptor != null) {
            TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
            TypeDescriptor targetTypeDesc = typeDescriptor;
            if (conversionService.canConvert(sourceTypeDesc, targetTypeDesc)) {
                try {
                    return (T) conversionService.convert(convertedValue, sourceTypeDesc, targetTypeDesc);
                }
                catch (ConversionFailedException ex) {
                    // fallback to default conversion logic below
                    firstAttemptEx = ex;
                }
            }
        }

        // Value not of required type?
        if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
            if (requiredType != null && Collection.class.isAssignableFrom(requiredType) && convertedValue instanceof String) {
                TypeDescriptor elementType = typeDescriptor.getElementTypeDescriptor();
                if (elementType != null && Enum.class.isAssignableFrom(elementType.getType())) {
                    convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
                }
            }
            if (editor == null) {
                editor = findDefaultEditor(requiredType);
            }
            convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
        }

        boolean standardConversion = false;

        if (requiredType != null) {

            if (convertedValue != null) {
                // 省略部分代码,都是根据条件来调用不同的转换方法
                else if (convertedValue instanceof Collection) {
                    // Convert elements to target type, if determined.
                    convertedValue = convertToTypedCollection(
                            (Collection) convertedValue, propertyName, requiredType, typeDescriptor);
                    standardConversion = true;
                }
                // 省略部分代码,都是根据条件来调用不同的转换方法
            }
            // 省略部分代码
        }

        if (firstAttemptEx != null) {
            if (editor == null && !standardConversion && requiredType != null && !Object.class.equals(requiredType)) {
                throw firstAttemptEx;
            }
            logger.debug("Original ConversionService attempt failed - ignored since " +
                    "PropertyEditor based conversion eventually succeeded", firstAttemptEx);
        }

        return (T) convertedValue;
    }

断点进来发现老项目和新项目某些变量的类型居然不一样:
技术分享
在老项目中的newValueString[],而在新项目中的newValue则是一个ArrayList,一样的代码怎么就产生不同的结果了呢?那我们继续跟踪,看看这个newValue究竟是怎么来的。发现newValue是在AbstractAutowireCapableBeanFactory类的applyPropertyValues(...)方法里得到的:

    protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
        // 省略部分代码

        if (pvs instanceof MutablePropertyValues) {
            mpvs = (MutablePropertyValues) pvs;
            if (mpvs.isConverted()) {
                // Shortcut: use the pre-converted values as-is.
                try {
                    bw.setPropertyValues(mpvs);
                    return;
                }
                catch (BeansException ex) {
                    throw new BeanCreationException(
                            mbd.getResourceDescription(), beanName, "Error setting property values", ex);
                }
            }
            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<PropertyValue> deepCopy = new ArrayList<PropertyValue>(original.size());
        boolean resolveNecessary = false;
        for (PropertyValue pv : original) {
            if (pv.isConverted()) {
                deepCopy.add(pv);
            }
            else {
                String propertyName = pv.getName();
                Object originalValue = pv.getValue();
                Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
                Object convertedValue = resolvedValue;
                boolean convertible = bw.isWritableProperty(propertyName) &&
                        !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);
                if (convertible) {
                    // 这里的resolvedValue就是后面的newValue
                    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));
                }
            }
        }
        // 省略部分代码
    }
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
        if (pvs == null || pvs.isEmpty()) {
            return;
        }

        MutablePropertyValues mpvs = null;
        List<PropertyValue> original;

        if (System.getSecurityManager() != null) {
            if (bw instanceof BeanWrapperImpl) {
                ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext());
            }
        }

        if (pvs instanceof MutablePropertyValues) {
            mpvs = (MutablePropertyValues) pvs;
            if (mpvs.isConverted()) {
                // Shortcut: use the pre-converted values as-is.
                try {
                    bw.setPropertyValues(mpvs);
                    return;
                }
                catch (BeansException ex) {
                    throw new BeanCreationException(
                            mbd.getResourceDescription(), beanName, "Error setting property values", ex);
                }
            }
            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<PropertyValue> deepCopy = new ArrayList<PropertyValue>(original.size());
        boolean resolveNecessary = false;
        for (PropertyValue pv : original) {
            if (pv.isConverted()) {
                deepCopy.add(pv);
            }
            else {
                String propertyName = pv.getName();
                Object originalValue = pv.getValue();
                Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
                Object convertedValue = resolvedValue;
                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);
        }
    }

下图是调试的信息:
技术分享

诶,这里新项目的originalValue为什么是ManagedList类型的,这也是我第一次看到它。而它是通过PropertyValue.getValue()得到的,PropertyValue不就是我们设的location属性的值吗?这里怎么被封装成ManagedList了?总算想到可能是<mvc:resources />惹的祸了,于是看下新项目中解析该标签的关键源码:

private String registerResourceHandler(ParserContext parserContext, Element element, Object source) {

        ManagedList<String> locations = new ManagedList<String>();
        locations.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(locationAttr)));

        RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class);
        resourceHandlerDef.setSource(source);
        resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        resourceHandlerDef.getPropertyValues().add("locations", locations);

        return beanName;
    }

终于看到这个”熟悉”的ManagedList了,然后又去检查了下老项目中该解析类的关键源码:

private String registerResourceHandler(ParserContext parserContext, Element element, Object source) {

        RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class);
        resourceHandlerDef.setSource(source);
        resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        resourceHandlerDef.getPropertyValues().add("locations", StringUtils.commaDelimitedListToStringArray(locationAttr));

        return beanName;
    }

然后。。。然后。。。就没有然后了,已经吐血不省人事。回过头来,既然知道原因了,那么赶紧解决了它!不使用<mvc:resources>标签就行,还记得之前我写过一篇关于处理静态资源的文章吗?传送门
该标签的本质我们都知道了,直接手动注册就好了~至此,问题终于解决!
不过有一点我不知道的是,为什么Spring项目组要将ResourcesBeanDefinitionParser中解析location的代码改掉?希望有知道的朋友可以跟我讲讲,多谢!

最后有一点体会:越是让人心烦的事情,解决了之后越是让人舒心。嘿嘿~

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。