一、背景介绍:
大家在使用spring进行开发接口的时候有没有思考过,框架对于接口的入参和返参的格式都是怎么处理的?比如正常的上传文件,下载文件,查询某个信息,入参的格式都不一样,有 json、文件流,返回信息也是,那这块框架帮助我们做了什么,所以下来咱们揭开 HttpMessageConverter 的面纱。
二、HttpMessageConverter介绍
org.springframework.http.converter.HttpMessageConverter
是一个策略接口,HTTP request (请求)和response (响应)的转换器,该类衍生出不同的子类处理不同的格式的请求。
在讲解子类关系之前首先需要了解一些HTTP的基本知识:
1、决定resp.body的Content-Type的第一要素是对应的req.headers.Accept属性的值,又叫做MediaType。如果服务端支持这个Accept,那么应该按照这个Accept来确定返回resp.body对应的格式,同时把resp.headers.Content-Type设置成自己支持的符合那个Accept的MediaType。服务端不支持Accept指定的任何MediaType时,应该返回错误406 Not Acceptable. 例如:req.headers.Accept = text/html,服务端支持的话应该让resp.headers.Content-Type = text/html,并且resp.body按照html格式返回。 例如:req.headers.Accept = text/asdfg,服务端不支持这种MediaType,应该返回406 Not Acceptable。
2、如果Accept指定了多个MediaType,并且服务端也支持多个MediaType,那么Accept应该同时指定各个MediaType的QualityValue,也就是q值,服务端根据q值的大小来决定这几个MediaType类型的优先级,一般是大的优先。q值不指定时,默认视为q=1. Chrome的默认请求的Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,表示服务端在支持的情况下应该优先返回text/html,其次是application/xhtml+xml. 前面几个都不支持时,服务器可以自行处理 /,返回一种服务器自己支持的格式。
3、一个HTTP请求没有指定Accept,默认视为指定 Accept: /;没有指定Content-Type,默认视为 null,就是没有。当然,服务端可以根据自己的需要改变默认值。
4、Content-Type必须是具体确定的类型,不能包含 *.
SpringMvc基本遵循上面这几点。
然后是启动时默认加载的Converter。在mvc启动时默认会加载下面的几种HttpMessageConverter,相关代码在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
中的addDefaultHttpMessageConverters
方法中,代码如下:
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
try {
messageConverters.add(new SourceHttpMessageConverter());
} catch (Throwable var3) {
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
Jackson2ObjectMapperBuilder builder;
if (jackson2XmlPresent) {
builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
} else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
} else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
} else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}
当我们配置了自己的 MessageConverter, SpringMVC 启动过程就不会调用 addDefaultHttpMessageConverters 方法,且看下面代码 if 条件,这样做也是为了定制化我们自己的 MessageConverter:
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList();
this.configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
this.addDefaultHttpMessageConverters(this.messageConverters);
}
this.extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
HttpMessageConverter 的子类支持的MediaType如下:
类名 | 支持的JavaType | 支持的MediaType |
---|---|---|
ByteArrayHttpMessageConverter | byte[] | application/octet-stream, * / * |
StringHttpMessageConverter | String | text/plain, * / * |
ResourceHttpMessageConverter | Resource | * / * |
SourceHttpMessageConverter | Source | application/xml, text/xml, application/*+xml |
AllEncompassingFormHttpMessageConverter | Map<K, List<?>> | application/x-www-form-urlencoded, multipart/form-data |
MappingJackson2HttpMessageConverter | Object | application/json, application/*+json |
Jaxb2RootElementHttpMessageConverter | Object | application/xml, text/xml, application/*+xml |
JavaSerializationConverter | Serializable | x-java-serialization;charset=UTF-8 |
FastJsonHttpMessageConverter | Object | * / * |
这里只列出重要的两个属性,详细的可以去看org.springframework.http.converter包中的代码。 另外,基本类型都视为对应的包装类的类型来算。还有,基本类型的json序列化就只有字面值,没有key,不属于规范的json序列化,但是基本上所有json框架都支持基本类型直接序列化。
三、MappingJackson2HttpMessageConverter介绍
此处专门讲解MappingJackson2HttpMessageConverter 的流程,因为该类是处理application/json的,咱们一般实际上都会在这里对数据进行自定义的处理:
下图为MappingJackson2HttpMessageConverter的类图关系:

咱们查看MappingJackson2HttpMessageConverter 的父类AbstractJackson2HttpMessageConverter,通过idea对该类 alt+7 查看该类的所有方法,如下图可以看到这个抽象类的方法:

其中关键的方法:
- read(): 该方法会读取请求的body里的数据,并根据当前的controller入参定义的对象,进行反序列;
- write():该方法将接口从service返回的数据进行序列化处理;
当然还有一些其他的方法,比如canRead()和canWrite()是做一些辅助处理的,具体流程可参照该图:

下面debug调试源码:
在接受请求转发后会到达 AbstractJackson2HttpMessageConverter 的readWithMessageConverters 方法,该方法会遍历当前加载的Converters信息与当前的请求的的信息进行判断,是否可以canRead,可以的话就会执行后续的读取操作:
label94: {
message = new AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage(inputMessage);
Iterator var11 = this.messageConverters.iterator();
HttpMessageConverter converter;
Class converterType;
GenericHttpMessageConverter genericConverter;
while(true) {
if (!var11.hasNext()) {
break label94;
}
converter = (HttpMessageConverter)var11.next();
converterType = converter.getClass();
genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
if (genericConverter != null) {
if (genericConverter.canRead(targetType, contextClass, contentType)) {
break;
}
} else if (targetClass != null && converter.canRead(targetClass, contentType)) {
break;
}
}
if (message.hasBody()) {
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
} else {
body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
}
}

当前的MappingJackson2HttpMessageConverter匹配上,进入了如下的界面,此时会根据当前的VO类进行处理:

继续深入debug,会进入到ObjectMapper类的_readMapAndClose方法将内容反序列出来:

接口在从db获取数据经过业务处理返回给客户端的时候按照上述的操作将数据write进行序列化。
发现了吗?Converter也是通过ObjectMapper 类进行序列化和反序列操作的,如果我们想要定制化可以通过他进行处理:
如下代码是对属性值为null的不序列化以及时间格式的处理(此处处理是全局设置),具体的还包含哪些处理可在网上寻找更多例子:
@Bean
public ObjectMapper getObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_NULL); // 不序列化null的属性
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 默认的时间序列化格式
return mapper;
}
到此位置,大家应该对这块的流程大概了解了一些,建议大家也手动debug走走流程。
四、结尾:
文章中讲解的ObjectMapper,我们可以通过对这块进行参数调整,来满足我们的需求,具体可参照这篇文章 Jackson详解