<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;
}
断点进来发现老项目和新项目某些变量的类型居然不一样:
在老项目中的newValue
是String[]
,而在新项目中的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
的代码改掉?希望有知道的朋友可以跟我讲讲,多谢!
最后有一点体会:越是让人心烦的事情,解决了之后越是让人舒心。嘿嘿~
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。