IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    spring加载Resources遇到ConversionFailedException异常

    axiu发表于 2015-11-22 02:12:50
    love 0

    目前在用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

    具体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

    转载请注明来源:spring加载Resources遇到ConversionFailedException异常
    本文链接地址:http://axiu.me/coding/spring-resources-conversionfailedexception/


沪ICP备19023445号-2号
友情链接