由 SpringBoot文件上传引发的bug 解决及springmvc 自动配置原理浅析

背景

使用的 springboot 版本:2.4.12

需求

文件上传:既需要上传文件的同时,还需要携带其他的消息内容。

解决方案

1.  接收文件+接收一个字符串
public ReturnJson saveImg(@RequestParam String advertisement
        , @RequestParam MultipartFile file) {
这种方式就需要将参数advertisement会通过查询参数携带,file 通过formData的形式传输。但核心依然只能接收一个字符串,如果参数很多则不适用。
 
2. 接收文件+接收对象body
可以参考如下帖子:
最后写出来的代码大概是这样:
public void createAdvertisement(@RequestPart @Validated Advertisement advertisement, @RequestPart MultipartFile file) {
    System.out.println("上传成功");
}

问题发现

很不幸,采用方案而出现了错误,返回的响应:
{

    "timestamp": 1637578285092,

    "status": 415,

    "error": "Unsupported Media Type",

    "message": "",

    "path": "xxxx"

}
后台报错信息如下:
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver
 
找了一圈,网上的答案千奇百怪,都没有很好的解决我的问题,stackoverflow 也提出问题:
最后,决定自己从源头解决。
 

解决过程

根据异常抛出的地方,定位到  AbstractMessageConverterMethodArgumentResolver . readWithMessageConverters方法。

HttpMessageConverters 默认有10 个消息转换器实现类,能够处理不同类型的消息。依次调用这些convert,能够处理就处理,最后没有一个convert 处理就会抛出异常。

 

也就是说已有的 HttpMessageConverters 解析不了octet-stream,最后就抛出了异常:

源码上有个特别的点,如果获取不到 contentType,那就默认设置为 MediaType.APPLICATION_OCTET_STREAM;

try {
   contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
   throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
   noContentType = true;
   contentType = MediaType.APPLICATION_OCTET_STREAM;
}

根本原因就是没有一个HttpMessageConverter 能够处理 APPLICATION_OCTET_STREAM 的格式,所以根本解决答案就是手动添加一个HttpMessageConverter就可以了。可以添加一个FastJsonConverterConfig,实现方式有两种:

方法一:配置方式

@Bean 
public JavaSerializationConverter javaSerializationConverter() { 
    return new JavaSerializationConverter();
 }

springboot会把我们自定义的converter放在顺序上的最高优先级,优先使用我们这个。

方式二:实现WebMvcConfigurer,覆写相关方法:

@Configuration
public class InterceptorAdapterConfig implements WebMvcConfigurer {
 
     void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }
     void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

 我这里就采用第一种方式,代码如下

@Configuration
public class FastJsonConverterConfig {

    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteNullBooleanAsFalse
        );
        fastConverter.setFastJsonConfig(fastJsonConfig);
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");

        fastConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8, MediaType.APPLICATION_OCTET_STREAM));
        HttpMessageConverter<?> converter = fastConverter;
        return new HttpMessageConverters(converter);
    }
}

fastConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM));

表示支持 MediaType.APPLICATION_OCTET_STREAM 类型的解析。重新启动服务,发现自定义的序列化类果然优先级最高。

 

最后采用formdata 格式请求可以成功接收到并且解析数据:

 

问题复现

问题又来了,过了两天,又前端同事说这个接口又报415 了。我人傻了,一顿操作后,我都差点放弃了,打算做成两个接口来迂回解决这个问题了。最后,我又一个断点去看 HttpMessageConverters的源码,发现 FastJsonConverterConfig这个应该处于第一个的实现类不见了,可是看代码发现FastJsonConverterConfig确实是生效了。但是执行的时候又没有它,真的是神奇了。
 
我想一定是有个什么配置使得FastJsonConverterConfig失效了,最后发现元凶是有个同事做了如下配置:
 
@Configuration
@EnableWebMvc
@Slf4j
public class InterceptorAdapterConfig implements WebMvcConfigurer {

@EnableWebMvc 是罪魁祸首

我们去看下EnableWebMvc  的原理:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

 他导入DelegatingWebMvcConfiguration 这个配置类,继续跟下去

@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

   private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

   @Autowired(required = false)
   public void setConfigurers(List<WebMvcConfigurer> configurers) {
      if (!CollectionUtils.isEmpty(configurers)) {
         this.configurers.addWebMvcConfigurers(configurers);
      }
   }

setConfigurers  方法表明了会将所有WebMvcConfigurer 的实现类注入进来。DelegatingWebMvcConfiguration是WebMvcConfigurationSupport的一种实现,其主要目的是提供 MVC 配置。通过前面的解释,其实就是通过将 @EnableWebMvc 添加到应用程序 @Configuration 类来导入。 看到这里似乎并没有发现有什么冲突。

既然这个配置类没有什么特殊的,我们换个思路,从spring mvc 的自动配置入手,看看有没有什么问题,也就是WebMvcAutoConfiguration 配置类

@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {

哦豁,还真的有发现,我们看到有个条件配置类

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})

没有WebMvcConfigurationSupport 这个配置类时,WebMvcAutoConfiguration才会生效。恍然大悟了,@EnableWebMvc  确实会使得spring mvc的自动配置失效,EnableWebMvc 更多适用于想要定制化处理,但是我们既然使用了springmvc 框架,又不利用其封装好的特性,反而自己去写实现,这不是多此一举吗。更多的是,我们基于其特性,做一些必要的兼容,修改。而对修改开放这正是spring 的强大之处,比如前面的加入自定义的FastJsonHttpMessageConverter就是很好的一个案例说明,既不影响原有的功能,又实现了自己的需求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章