由 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就是很好的一個案例說明,既不影響原有的功能,又實現了自己的需求。

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