目前在用java spring做web开发,不得不说,spring给我们留下了无数的坑,今天就遇到一个。
项目进行到将近一半的时候,同事发现在eclipse console飞速闪过的debug log(注意是debug)中,似乎有什么不得了的东西闪过了。仔细往上滚,发现了几段这样的exception被抛出:
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 '[/WEB-INF/css/]'
意思是,静态资源文件无法从ArrayList
转换为List
。
当然,既然是debug log才会打出来的东西,不会影响使用,只是会有一些问题,本着对项目负责的宗旨,组长把问题抛给了我。
擦,刚写了几天mybatis就让弄spring的bug,说好的循序渐进慢慢成长呢?
但是,作为一个负责的程序员,既然问题过来了,就算是象征性的搞一下,也要起码看得懂这是啥错误。从spring-mvc.xml的配置文件开始吧。
基本上,所有静态文件,在3.0以后,是可以通过标签mvc:resources来写入的。例如
<mvc:resources mapping="/css/**" location="/WEB-INF/css/" />
刚开始,以为是用错了方法,但是无论是改成mvc默认,或者添加任何参数进去,都是提示一样的转换错误。
经过一步一步的debug,发现在工程的xml里,还使用了一个自定义conversionService
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean" />
转型错误,那八成是他搞出来的?去掉之后果然没再报错了。
但是,就这么解决总觉得不是很完整啊,继续解释一下为什么去掉就没事了(正文开始?):
目前使用的是4.1.7,以下内容也全部基于这个,至于为什么不用最新版本的,就要问组长了。。。
通常,在spring3.0以后,为了简化配置,通常会写入
<mvc:annotation-driven/>
这个配置项,一般保持默认就没啥问题(至少官方是这么讲的)。
通常,一般玩家走到这里就结束了,因为嗯,很正常的跑起来了。但是,如果碰到喜欢乱加东西的,就会出错。如果细细查看一下,这个annotation-driven真的默默地干了好多工作呢:
1.相当于注册了一个RequestMappingHandlerMapping
, 一个RequestMappingHandlerAdapter
, 一个ExceptionHandlerExceptionResolver
;
2.Type ConversionService:默认有@NumberFormat
和@DateTimeFormat
(Date, Calendar, Long, and Joda Time);
3.@Controller
的支持,@Valid输入验证支持(如果用了JSR-303 Provider);
4.支持读写XML(如果用了JAXB);
5.支持读写JSON(如果用了Jackson)。
默认情况下,还给注册了10个HttpMessageConverters
(列表见原文http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-config-enable)
其中有一个:ResourceHttpMessageConverter
,用于把(从)Resource转换(成)其他类型。注意,这是自定义,即annotation-driven帮我们做的。不过这个和下面要说的conversionService似乎不再一个层上。。。
详细内容见文件注释(org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java)
好了,下面是问题复现时间。
把xml里删除的conversionService
加回去。然后在出错的地方打上断点,会看到各个bean都在AutowireCapableBeanFactory
被包装成了固定的结构,之后在BeanDefinitionValueResolver.resolveValueIfNecessary
里决定是否要转型(这个函数的注释给出了哪些会转型,哪些不会),如果有ManagedList
、ManagedSet
、ManagedMap
之类的,会一层一层解开并转型。
接着就是抛出exception的地方了:
具体TypeConverterDelegate
这个做了啥,可以进去看看,基本就是看这个类型有没有注册自定义propertyEditor -> 有没有注册自定义的conversionService -> 通常处理。代码如下:
TypeConverterDelegate.public T convertIfNecessary(String propertyName, Object oldValue, Object newValue,
Class requiredType, TypeDescriptor typeDescriptor) line: 160
... // 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? ...
大概过程就是:Resource
被包装成了ManagedList
,然后经过层层转型,最后搞成ArrayList
,调用了一个CollectionToCollection
的Converter,这哥们发现我靠好像我搞不定啊,标记firstAttemptEx
,并抛出这个异常,然后转手给了默认的conversion处理。
当然,如果没注册这个自定义的conversion,那么他直接就默认的conversionService
,肯定就会直接走下面”// Value not of required type?“
了。
这么看来,问题就应该出在这个自定义的conversionService
上了,断点之,可以看到加载的Converter列表
@org.springframework.format.annotation.DateTimeFormat java.lang.Long -> java.lang.String: org.springframework.format.datetime.DateTimeFormatAnnotationFormatterFactory@7ad60, ..... org.springframework.core.convert.support.StringToArrayConverter@12bf62a org.springframework.core.convert.support.StringToCollectionConverter@12e0f74
大概有20多个,其中也包含了io.Resource
被转型成的managedList
。那就是说,当你自定义了一个conversionService
,并且默认注册了FormattingConversionServiceFactoryBean
,他就会拿这个去匹配任何可能被转型的东西。当然其中也包括了可能转型失败的,一旦失败,那就抛个debug级别的的异常,交给兜底的代码去完成。
继续翻一下Spring的bug处理表,可以看到好几个相关的bug:比如这个
https://jira.spring.io/browse/SPR-6564,还有这个https://jira.spring.io/browse/SPR-7079。看来这个问题是有年头了,目前项目里使用的是4.1.7,不知道最新的版本有没有解决这个问题。。。
所以,我能像到的解决办法就是绕开自定义的ConversionService
:
1、使用mvc:annotation-driven
默认提供的converter;
2、写一个propertyEditor
来处理Resource
。
完毕。
这个bug的勘察过程也顺便练习了一下maven的配置(update index竟然用了一个多小时),对于Spring的配置这部分也有了一些理解,刚刚接触难免有误,以后慢慢(被)端正吧。
参考:
1. What’s the difference between mvc:annotation-driven and context:annotation-config in servlet?
2. Spring doc: 8. Validation, Data Binding, and Type Conversion