為什么說HttpMessageConverter的順序非常重要_SpringBoot


問題描述

系統內配置了,ProtobufJsonFormatHttpMessageConverter和FastJsonHttpMessageConverter。
Spring官方內置的默認MessageConverter 比較標准,遇到什么 MediaType 就怎么解析。但是這兩個比較特殊。

對於Protobuf生成的參數:

@PostMapping("/proto")
    public ResponseEntity<String> proto(@RequestBody AddressBookProtos.Person person) {
        try {
            log.info("input is {}", JsonFormat.printer().print(person));
        } catch (Exception e) {
            //
        }
        return ResponseEntity.ok().body("ok");
    }

這里用到的是普通的JSON請求,也就是Request Header 的 ContentType是 application/json;charset=UTF-8;

如果ProtobufJsonFormatHttpMessageConverter在FastJsonHttpMessageConverter 之后,那么讀到的Protobuf消息是空白。
也就說:Controller的 RequestBody 參數是空白的字符串。

問題分析

先看 org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver 類:


//method: readWithMessageConverters()

for (HttpMessageConverter<?> converter : this.messageConverters) {
    Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
    GenericHttpMessageConverter<?> genericConverter =
            (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
    if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
            (targetClass != null && converter.canRead(targetClass, contentType))) {
        if (message.hasBody()) {
            HttpInputMessage msgToUse =
                    getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
            body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                    ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
            body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
        }
        else {
            body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
        }
        break;
    }
}

這個類說明,Spring會根據Convert列表,逐個調用converter.canRead,判斷是否能夠支持這種內容的讀寫。
FastJsonHttpMessageConverter 的canRead相當於直接返回true,因為mediaType 也支持 application/json;charset=UTF-8;
這里考慮到JSON只是一個字符串,所以沒法根據類型判斷能不能讀。字符串肯定能讀。所以FastJSON這個地方還不能直接說他這么設計不合理。

//FastJsonHttpMessageConverter.java

    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }


    public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
        return super.canRead(contextClass, mediaType);
    }

所以如果先找到了FastJsonHttpMessageConverter,那么FastJSON不認識 protobuf的 Bean,無法進行讀寫,因此讀到一個空字符串。

再看看ProtobufJsonFormatHttpMessageConverter的實現:

//org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter#supports

	@Override
	protected boolean supports(Class<?> clazz) {
		return Message.class.isAssignableFrom(clazz);
	}

這里十分精確,他就是要支持Message接口的,所有的Protobuf定義message的時候,都會繼承這個接口。

因此這里需要將 ProtobufJsonFormatHttpMessageConverter 提到FastJson之前。

解決方案

方案一


@Bean 
public ProtobufJsonFormatHttpMessageConverter protobufJsonFormatHttpMessageConverter() {
    return new ProtobufJsonFormatHttpMessageConverter();
}

這里定義的MessageConverter 會很早就掃描到Spring Context中。這里還不清楚為什么這個地方的ProtobufJsonFormatHttpMessageConverter 每次都是第一個。
嘗試修改Configuration的類名字為z開頭 也總是第一個。

同時FastJson轉換器通常配置方式如下:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
        fastJsonHttpMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8));
        converters.add(fastJsonHttpMessageConverter);
    }
}

這樣這個WebMvcConfigurer 在Spring Boot啟動比較晚的時候才會加載,所以這里的MessageConverter 會排到最后面。

方案二(推薦)

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        ProtobufJsonFormatHttpMessageConverter protobufJsonFormatHttpMessageConverter = new ProtobufJsonFormatHttpMessageConverter();
        converters.add(protobufJsonFormatHttpMessageConverter);
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
        fastJsonHttpMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8));
        converters.add(fastJsonHttpMessageConverter);
    }
}

這里configureMessageConverters 的調用順序一定是在extendMessageConverters之前的。
參見:

//org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters

	protected final List<HttpMessageConverter<?>> getMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<>();
			configureMessageConverters(this.messageConverters);
			if (this.messageConverters.isEmpty()) {
				addDefaultHttpMessageConverters(this.messageConverters);
			}
			extendMessageConverters(this.messageConverters);
		}
		return this.messageConverters;
	}

Spring並沒有對HttpMessageConverter做什么特殊的排序。(只針對XML的排到最后,"with some slight re-ordering to put XML converters at the back of the list")

另外參考一篇cnBlog文章 講的HttpMessageConverter的比較詳細。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM