Axiu Blog
了的东西闪过了。仔细往上滚,发现了几段这样的exception被抛出: org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.util.ArrayList to type java.util.List for value '\
目前在用java spring做web开发,不得不说,spring给我们留下了无数的坑,今天就遇到一个。 项目进行到将近一半的时候,同事发现在eclipse console飞速闪过的debug log(注意是debug)中,似乎有什么不得了的东西闪过了。仔细往上滚,发现了几段这样的exception被抛出: org.springframework.core.convert.Conversion
目前在用java spring做web开发,不得不说,spring给我们留下了无数的坑,今天就遇到一个。 项目进行到将近一半的时候,同事发现在eclipse console飞速闪过的debug log(注意是debug)中,似乎有什么不得了的东西闪过了。仔细往上滚,发现了几段这样的exception被抛出: org.springframework.core.convert.Conversion
spring加载Resources遇到ConversionFailedException异常
Max

目前在用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 for value '[/WEB-INF/css/]'

意思是,静态资源文件无法从ArrayList转换为List

当然,既然是debug log才会打出来的东西,不会影响使用,只是会有一些问题,本着对项目负责的宗旨,组长把问题抛给了我。

解决过程

擦,刚写了几天mybatis就让弄spring的bug,说好的循序渐进慢慢成长呢?

但是,作为一个负责的程序员,既然问题过来了,就算是象征性的搞一下,也要起码看得懂这是啥错误。从spring-mvc.xml的配置文件开始吧。

基本上,所有静态文件,在3.0以后,是可以通过标签mvc:resources来写入的。例如

刚开始,以为是用错了方法,但是无论是改成mvc默认,或者添加任何参数进去,都是提示一样的转换错误。

经过一步一步的debug,发现在工程的xml里,还使用了一个自定义conversionService

转型错误,那八成是他搞出来的?去掉之后果然没再报错了。

看代码

但是,就这么解决总觉得不是很完整啊,继续解释一下为什么去掉就没事了(正文开始?):

目前使用的是4.1.7,以下内容也全部基于这个,至于为什么不用最新版本的,就要问组长了。。。

通常,在spring3.0以后,为了简化配置,通常会写入

这个配置项,一般保持默认就没啥问题(至少官方是这么讲的)。

通常,一般玩家走到这里就结束了,因为嗯,很正常的跑起来了。但是,如果碰到喜欢乱加东西的,就会出错。如果细细查看一下,这个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里决定是否要转型(这个函数的注释给出了哪些会转型,哪些不会),如果有ManagedListManagedSetManagedMap之类的,会一层一层解开并转型。

接着就是抛出exception的地方了:

TypeConverterDelegate

具体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处理。

CollectionToCollectionConverter

当然,如果没注册这个自定义的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

Comments