【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter 消息转换器详解

本文主要是介绍【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter 消息转换器详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

每篇一句

华为,不惹事但也不怕事;中国,不惹事更不怕事。

相关阅读

【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—HttpMessageConverter的匹配规则(选择原理)
【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler

前言

本文介绍Spring MVC中的一个极其重要的组件:HttpMessageConverter消息转换器。

有一副非常著名的图,来形容Spring MVC对一个请求的处理:
在这里插入图片描述
从图中可见HttpMessageConverterSpring MVC的重要性。它对请求、响应都起到了非常关键的作用~

为何需要消息转换器

HttpMessageConverter是用来处理request和response里的数据的。.

请求和响应都有对应的body,而这个body就是需要关注的主要数据。
请求体的表述一般就是一段字符串,当然也可以是二进制数据(比如上传~)。
响应体则是浏览器渲染页面的依据,对于一个普通html页面得响应,响应体就是这个html页面的源代码。

请求体和响应体都是需要配合Content-Type头部使用的,这个头部主要用于说明body中得字符串是什么格式的,比如:text,json,xml等。对于请求报文,只有通过此头部,服务器才能知道怎么解析请求体中的字符串,对于响应报文,浏览器通过此头部才知道应该怎么渲染响应结果,是直接打印字符串还是根据代码渲染为一个网页

对于HttpServletRequest和HttpServletResponse,可以分别调用getInputStreamgetOutputStream来直接获取body但是获取到的仅仅只是一段字符串
**而对于java来说,处理一个对象肯定比处理一个字符串要方便得多,也好理解得多。**所以根据Content-Type头部,将body字符串转换为java对象是常有的事。反过来,根据Accept头部,将java对象转换客户端期望格式的字符串也是必不可少的工作。这就是我们本文所讲述的消息转换器的工作~

消息转换器它能屏蔽你对底层转换的实现,分离你的关注点,让你专心操作java对象,其余的事情你就交给我Spring MVC吧~大大提高你的编码效率(可议说比源生Servlet开发高级太多了)

Spring内置了很多HttpMessageConverter,比如MappingJackson2HttpMessageConverterStringHttpMessageConverter,甚至还有FastJsonHttpMessageConverter(需导包和自己配置)

HttpMessageConverter

在具体讲解之前,先对所有的转换器来个概述:


名称作用读支持MediaType写支持MediaType备注
FormHttpMessageConverter表单与MultiValueMap的相互转换application/x-www-form-urlencodedapplication/x-www-form-urlencoded和multipart/form-data可用于处理下载
XmlAwareFormHttpMessageConverterSpring3.2后已过期,使用下面AllEnc…代替
AllEncompassingFormHttpMessageConverter对FormHttp…的扩展,提供了对xml和json的支持同上同上
SourceHttpMessageConverter数据与javax.xml.transform.Source的相互转换application/xml和text/xml和application/*+xml同read和Sax/Dom等有关
ResourceHttpMessageConverter数据与org.springframework.core.io.Resource*/**/*
ByteArrayHttpMessageConverter数据与字节数组的相互转换*/*application/octet-stream
ObjectToStringHttpMessageConverter内部持有一个StringHttpMessageConverterConversionService他俩的&&他俩的&&
RssChannelHttpMessageConverter处理RSS <channel> 元素application/rss+xmlapplication/rss+xml很少接触
MappingJackson2HttpMessageConverter使用Jackson的ObjectMapper转换Json数据application/json和application/*+jsonapplication/json和application/*+json默认编码UTF-8
MappingJackson2XmlHttpMessageConverter使用Jackson的XmlMapper转换XML数据application/xml和text/xmlapplication/xml和text/xml需要额外导包Jackson-dataformat-XML才能生效。从Spring4.1后才有
GsonHttpMessageConverter使用Gson处理Json数据application/jsonapplication/json默认编码UTF-8
ResourceRegionHttpMessageConverter数据和org.springframework.core.io.support.ResourceRegion的转换application/octet-streamapplication/octet-streamSpring4.3才提供此类
ProtobufHttpMessageConverter转换com.google.protobuf.Message数据application/x-protobuf和text/plain和application/json和application/xml同read@since 4.1
StringHttpMessageConverter数据与String类型的相互转换*/**/*转成字符串的默认编码为ISO-8859-1
BufferedImageHttpMessageConverter数据与java.awt.image.BufferedImage的相互转换Java I/O API支持的所有类型Java I/O API支持的所有类型
FastJsonHttpMessageConverter使用FastJson处理Json数据*/**/*需要导入Jar包和自己配置,Spring并不默认内置

Jaxb也是和Sax、Dom、JDOM类似的解析XML的类库,jackson-module-jaxb-annotations对它提供了支持,但是由于关注太少了,所以Jaxb相关的转换器此处省略~~~
MarshallingHttpMessageConverter也是Spring采用Marshaller/Unmarshaller的方式进行xml的解析,也不关注了
FastJsonHttpMessageConverter4FastJsonpHttpMessageConverter4都继承自FastJsonHttpMessageConverter,现在都已经标记为过期。直接使用FastJsonHttpMessageConverter它即可

需要知道的是:上面说的支持都说的是默认支持,当然你是可以自定义让他们更强大的。比如:我们可以自己配置StringHttpMessageConverter,改变(增强)他的默认行为:

<mvc:annotation-driven><mvc:message-converters><bean class="org.springframework.http.converter.StringHttpMessageConverter"><property name="supportedMediaTypes"><list><value>text/plain;charset=UTF-8</value><value>text/html;charset=UTF-8</value></list></property></bean></mvc:message-converters>
</mvc:annotation-driven>

talk is cheap,show me the code,我们还是从代码的角度,直接看问题吧。

既然它是HttpMessageConverter,所以铁定和HttpMessage有关,因为此接口涉及的内容相对来说比较偏底层,因此本文只在接口层面做简要的一个说明。

HttpMessage

它是Spring 3.0后增加一个非常抽象的接口。表示:表示HTTP请求和响应消息的基本接口

public interface HttpMessage {// Return the headers of this messageHttpHeaders getHeaders();
}

看看它的继承树:
在这里插入图片描述

HttpInputMessage和HttpOutputMessage

这就是目前都在使用的接口,表示输入、输出信息~

public interface HttpInputMessage extends HttpMessage {InputStream getBody() throws IOException;
}
public interface HttpOutputMessage extends HttpMessage {OutputStream getBody() throws IOException;
}
HttpRequest

代表着一个Http请求信息,提供了多的几个API,是对HttpMessage的一个补充。Spring3.1新增的

public interface HttpRequest extends HttpMessage {@Nullabledefault HttpMethod getMethod() {// 可议根据String类型的值  返回一个枚举return HttpMethod.resolve(getMethodValue());}String getMethodValue();// 可以从请求消息里  拿到URLURI getURI();
}
ReactiveHttpInputMessage和ReactiveHttpOutputMessage

显然,是Spring5.0新增的接口,也是Spring5.0最重磅的升级之一。自此Spring容器就不用强依赖于Servlet容器了。它还可以选择依赖于reactor这个框架。
比如这个类:reactor.core.publisher.Mono就是Reactive的核心类之一~

因为属于Spring5.0的最重要的新特性之一,所以此处也不再过多介绍了。后面会是重磅内容~


HttpMessageConverter接口是Spring3.0之后新增的一个接口,它负责将请求信息转换为一个对象(类型为T),并将对象(类型为T)绑定到请求方法的参数中或输出为响应信息

// @since 3.0  Spring3.0后推出的   是个泛型接口
// 策略接口,指定可以从HTTP请求和响应转换为HTTP请求和响应的转换器
public interface HttpMessageConverter<T> {// 指定转换器可以读取的对象类型,即转换器可将请求信息转换为clazz类型的对象// 同时支持指定的MIME类型(text/html、application/json等)boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);// 指定转换器可以将clazz类型的对象写到响应流当中,响应流支持的媒体类型在mediaType中定义boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);// 返回当前转换器支持的媒体类型~~List<MediaType> getSupportedMediaTypes();// 将请求信息转换为T类型的对象 流对象为:HttpInputMessageT read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;// 将T类型的对象写到响应流当中,同事指定响应的媒体类型为contentType 输出流为:HttpOutputMessage void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}

看看它的继承树:
在这里插入图片描述
在这里插入图片描述
它的继承树,用品牌繁多来形容真的非常贴切。
按照层级划分,它的直接子类是如下四个:
FormHttpMessageConverter、AbstractHttpMessageConverter、BufferedImageHttpMessageConverter、GenericHttpMessageConverter(Spring3.2出来的,支持到了泛型)

FormHttpMessageConverter:form表单提交/文件下载

从名字知道,它和Form表单有关。浏览器原生表单默认的提交数据的方式(就是没有设置enctype属性),它默认是这个:Content-Type: application/x-www-form-urlencoded;charset=utf-8

从请求和响应读取/编写表单数据。默认情况下,它读取媒体类型 application/x-www-form-urlencoded 并将数据写入 MultiValueMap<String,String>。因为它独立的存在,所以可以看看源码内容:

// @since 3.0
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {// 默认UTF-8编码  MediaType为:application/x-www-form-urlencodedpublic static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE = new MediaType(MediaType.APPLICATION_FORM_URLENCODED, DEFAULT_CHARSET);// 缓存下它所支持的MediaType们private List<MediaType> supportedMediaTypes = new ArrayList<>();// 用于二进制内容的消息转换器们~~~ 毕竟此转换器还支持`multipart/form-data`这种  可以进行文件下载~~~~~private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();private Charset charset = DEFAULT_CHARSET;@Nullableprivate Charset multipartCharset;// 唯一的一个构造函数~public FormHttpMessageConverter() {// 默认支持处理两种MediaType:application/x-www-form-urlencoded和multipart/form-datathis.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316// === 它自己不仅是个转换器,还内置了这三个转换器 至于他们具体处理那种消息,请看下面 都有详细说明 ==// 注意:这些消息转换器都是去支持part的,支持文件下载this.partConverters.add(new ByteArrayHttpMessageConverter());this.partConverters.add(stringHttpMessageConverter);this.partConverters.add(new ResourceHttpMessageConverter());// 这是为partConverters设置默认的编码~~~applyDefaultCharset();}// 省略属性额get/set方法// 从这可以发现,只有Handler的入参类型是是MultiValueMap它才会去处理~~~~@Overridepublic boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {if (!MultiValueMap.class.isAssignableFrom(clazz)) {return false;}// 若没指定MedieType  会认为是可读的~if (mediaType == null) {return true;}// 显然,只有我们Supported的MediaType才会是true(当然multipart/form-data例外,此处是不可读的)for (MediaType supportedMediaType : getSupportedMediaTypes()) {// We can't read multipart....if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {return true;}}return false;}// 注意和canRead的区别,有点对着干的意思~~~@Overridepublic boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {if (!MultiValueMap.class.isAssignableFrom(clazz)) {return false;}// 如果是ALL 说明支持所有的类型  那就恒返回true  当然null也是的if (mediaType == null || MediaType.ALL.equals(mediaType)) {return true;}for (MediaType supportedMediaType : getSupportedMediaTypes()) {// isCompatibleWith是否是兼容的if (supportedMediaType.isCompatibleWith(mediaType)) {return true;}}return false;}// 把输入信息读进来,成为一个 MultiValueMap<String, String>// 注意:此处发现class这个变量并没有使用~@Overridepublic MultiValueMap<String, String> read(@Nullable Class<? extends MultiValueMap<String, ?>> clazz,HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {// 拿到请求的ContentType请求头~~~~MediaType contentType = inputMessage.getHeaders().getContentType();// 这里面 编码放在contentType里面  若没有指定  走默认的编码// 类似这种形式就是我们自己指定了编码:application/json;charset=UTF-8Charset charset = (contentType != null && contentType.getCharset() != null ? contentType.getCharset() : this.charset);// 把body的内容读成字符串~String body = StreamUtils.copyToString(inputMessage.getBody(), charset);// 用"&"分隔   因为此处body一般都是hello=world&fang=shi这样传进来的String[] pairs = StringUtils.tokenizeToStringArray(body, "&");MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);// 这个就不说了,就是把键值对保存在map里面。注意:此处为何用多值Map呢?因为一个key可能是会有多个value的for (String pair : pairs) {int idx = pair.indexOf('=');if (idx == -1) {result.add(URLDecoder.decode(pair, charset.name()), null);}else {String name = URLDecoder.decode(pair.substring(0, idx), charset.name());String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());result.add(name, value);}}return result;}}

AbstractHttpMessageConverter

一个基础抽象实现,它也还是个泛型类。对于泛型的控制,有如下特点:

  • 最广的可以选择Object,不过Object并不都是可以序列化的,但是子类可以在覆盖的supports方法中进一步控制,因此选择Object是可以的
  • 最符合的是Serializable,既完美满足泛型定义,本身也是个Java序列化/反序列化的充要条件
  • 自定义的基类Bean,有些技术规范要求自己代码中的所有bean都继承自同一个自定义的基类BaseBean,这样可以在Serializable的基础上再进一步控制,满足自己的业务要求

若我们自己需要自定义一个消息转换器,大多数情况下也是继承抽象类再具体实现。比如我们最熟悉的:FastJsonHttpMessageConverter它就是一个子类实现

public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {// 它主要内部维护了这两个属性,可议构造器赋值,也可以set方法赋值~~private List<MediaType> supportedMediaTypes = Collections.emptyList();@Nullableprivate Charset defaultCharset;// supports是个抽象方法,交给子类自己去决定自己支持的转换类型~~~~// 而canRead(mediaType)表示MediaType也得在我支持的范畴了才行(入参MediaType若没有指定,就返回true的)@Overridepublic boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {return supports(clazz) && canRead(mediaType);}// 原理基本同上,supports和上面是同一个抽象方法  所以我们发现并不能入参处理Map,出餐处理List等等@Overridepublic boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {return supports(clazz) && canWrite(mediaType);}// 这是Spring的惯用套路:readInternal  虽然什么都没做,但我觉得还是挺有意义的。Spring后期也非常的好扩展了~~~~@Overridepublic final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException {return readInternal(clazz, inputMessage);}// 整体上就write方法做了一些事~~@Overridepublic final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException {final HttpHeaders headers = outputMessage.getHeaders();// 设置一个headers.setContentType 和 headers.setContentLengthaddDefaultHeaders(headers, t, contentType);if (outputMessage instanceof StreamingHttpOutputMessage) {StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;// StreamingHttpOutputMessage增加的setBody()方法,关于它下面会给一个使用案例~~~~streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {// 注意此处复写:返回的是outputStream ,它也是靠我们的writeInternal对它进行写入的~~~~@Overridepublic OutputStream getBody() {return outputStream;}@Overridepublic HttpHeaders getHeaders() {return headers;}}));}// 最后它执行了flush,这也就是为何我们自己一般不需要flush的原因else {writeInternal(t, outputMessage);outputMessage.getBody().flush();}}// 三个抽象方法protected abstract boolean supports(Class<?> clazz);protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException;protected abstract void writeInternal(T t, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException;}

关于StreamingHttpOutputMessage的使用:
表示允许设置流正文的HTTP输出消息,需要注意的是,此类消息通常不支持getBody()访问

// @since 4.0
public interface StreamingHttpOutputMessage extends HttpOutputMessage {// 设置一个流的正文,提供回调void setBody(Body body);// 定义可直接写入@link outputstream的主体的协定。// 通过回调机制间接的访问HttpClient库很有作用@FunctionalInterfaceinterface Body {// 把当前的这个body写进给定的OutputStreamvoid writeTo(OutputStream outputStream) throws IOException;}}
SourceHttpMessageConverter

处理一些和xml相关的资源,比如DOMSource、SAXSource、SAXSource等等,本文略过.

ResourceHttpMessageConverter

负责读取资源文件和写出资源文件数据

这个在上一篇Spring MVC下载的时候有提到过,它来处理把Resource进行写出去。当然它也可以把body的内容写进到Resource里来。

public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {// 是否支持读取流信息private final boolean supportsReadStreaming;// 默认支持所有的MediaType~~~~~   但是它有个类型匹配,所以值匹配入参/返回类型是Resource类型的public ResourceHttpMessageConverter() {super(MediaType.ALL);this.supportsReadStreaming = true;}@Overrideprotected boolean supports(Class<?> clazz) {return Resource.class.isAssignableFrom(clazz);}// 直观感受:读的时候也只支持InputStreamResource和ByteArrayResource这两种resource的直接封装@Overrideprotected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException {if (this.supportsReadStreaming && InputStreamResource.class == clazz) {return new InputStreamResource(inputMessage.getBody()) {@Overridepublic String getFilename() {return inputMessage.getHeaders().getContentDisposition().getFilename();}};}// 若入参类型是Resource接口,也是当作ByteArrayResource处理的else if (Resource.class == clazz || ByteArrayResource.class.isAssignableFrom(clazz)) {// 把inputSteeam转换为byte[]数组~~~~~~byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());return new ByteArrayResource(body) {@Override@Nullablepublic String getFilename() {return inputMessage.getHeaders().getContentDisposition().getFilename();}};}else {throw new HttpMessageNotReadableException("Unsupported resource class: " + clazz, inputMessage);}}@Overrideprotected void writeInternal(Resource resource, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException {writeContent(resource, outputMessage);}// 写也非常的简单,就是把resource这个资源的内容写到body里面去,此处使用的StreamUtils.copy这个工具方法,专门处理流// 看到此处我们自己并不需要flush,但是需要自己关闭流protected void writeContent(Resource resource, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException {try {InputStream in = resource.getInputStream();try {StreamUtils.copy(in, outputMessage.getBody());}catch (NullPointerException ex) {// ignore, see SPR-13620}finally {try {in.close();}catch (Throwable ex) {// ignore, see SPR-12999}}}catch (FileNotFoundException ex) {// ignore, see SPR-12999}}
}

使用它模拟完成上传功能
上传表单如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>测试FormHttpMessageConverter</title>
</head>
<body>
<!-- 表单的enctype一定要标注成multipart形式,否则是拿不到二进制流的 -->
<form action="http://localhost:8080/demo_war_war/upload" method="post" enctype="multipart/form-data">用户名 <input type="text" name="userName">头像 <input type="file" name="touxiang"><input type="submit">
</form>
</body>
</html>

在这里插入图片描述

    // 模拟使用Resource进行文件的上传~~~@ResponseBody@RequestMapping(value = "/upload", method = RequestMethod.POST)public String upload(@RequestBody Resource resource) { //此处不能用接口Resource resourcedumpStream(resource);return "success";}// 模拟写文件的操作(此处写到控制台)private static void dumpStream(Resource resource) {InputStream is = null;try {//1.获取文件资源is = resource.getInputStream();//2.读取资源byte[] descBytes = new byte[is.available()];is.read(descBytes);System.out.println(new String(descBytes, StandardCharsets.UTF_8));} catch (IOException e) {e.printStackTrace();} finally {try {//3.关闭资源is.close();} catch (IOException e) {}}}

控制台结果为:
在这里插入图片描述
由此可见利用它是可以把客户端的资源信息都拿到的,从而间接的实现文件的上传的功能。

ByteArrayHttpMessageConverter

和上面类似,略

ObjectToStringHttpMessageConverter

它是对StringHttpMessageConverter的一个扩展。它在Spring内部并没有装配进去。若我们需要,可以自己装配到Spring MVC里面去

public class ObjectToStringHttpMessageConverter extends AbstractHttpMessageConverter<Object> {// 我们只需要自定定义这个转换器   让它实现String到Obj之间的互相转换~~~private final ConversionService conversionService;private final StringHttpMessageConverter stringHttpMessageConverter;... // 下面省略// 读的时候先用stringHttpMessageConverter读成String,再用转换器转为Object对象// 写的时候先用转换器转成String,再用stringHttpMessageConverter写进返回的body里
}
Json相关转换器

在这里插入图片描述
可以看到一个是谷歌阵营,一个是jackson阵营。

GsonHttpMessageConverter

利用谷歌的Gson进行json序列化的处理~~~

// @since 4.1  课件它被Spring选中的时间还是比较晚的
public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {private Gson gson;public GsonHttpMessageConverter() {this.gson = new Gson();}// @since 5.0  调用者可以自己指定一个Gson对象了public GsonHttpMessageConverter(Gson gson) {Assert.notNull(gson, "A Gson instance is required");this.gson = gson;}	// 因为肯定是文本,所以这里使用Reader 没有啥问题// 父类默认用UTF-8把inputStream转为了更友好的Reader@Overrideprotected Object readInternal(Type resolvedType, Reader reader) throws Exception {return getGson().fromJson(reader, resolvedType);}@Overrideprotected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {// 如果带泛型  这里也是特别的处理了兼容处理~~~~if (type instanceof ParameterizedType) {getGson().toJson(o, type, writer);} else {getGson().toJson(o, writer);}}// 父类定义了它支持的MediaType类型~public AbstractJsonHttpMessageConverter() {super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));setDefaultCharset(DEFAULT_CHARSET);}
}
MappingJackson2HttpMessageConverter

利用亲儿子Jackson进行json序列化(当然,它并不是真正的亲儿子)

// @since 3.1.2  出来可谓最早。正统太子
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {// 该属性在父类定义~~~protected ObjectMapper objectMapper;@Nullableprivate String jsonPrefix;// 支持指定的MediaType类型~~public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));}// 所有的读、写都在父类AbstractJackson2HttpMessageConverter里统一实现的,稍微有点复杂性
}

总体上看,jackson的实现是最为完善的~~~

备注:Gson和Jackson转换器他俩都是支持jsonPrefix我们可以自定义Json前缀的~~~

若你的返回值是Map、List等,只要MediaType对上了,这种json处理器都是可以处理的。因为他们泛型上都是Object表示入参、 返回值任意类型都可以处理~~~

ProtobufHttpMessageConverter、ProtobufJsonFormatHttpMessageConverter

StringHttpMessageConverter

这个是使用得非常广泛的一个消息转换器,专门处理入参/出参字符串类型。

// @since 3.0  出生非常早
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {// 这就是为何你return中文的时候会乱码的原因(若你不设置它的编码的话~)public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;@Nullableprivate volatile List<Charset> availableCharsets;// 标识是否输出 Response Headers:Accept-Charset(默认true表示输出)private boolean writeAcceptCharset = true;public StringHttpMessageConverter() {this(DEFAULT_CHARSET);}public StringHttpMessageConverter(Charset defaultCharset) {super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL);}//Indicates whether the {@code Accept-Charset} should be written to any outgoing request.// Default is {@code true}.public void setWriteAcceptCharset(boolean writeAcceptCharset) {this.writeAcceptCharset = writeAcceptCharset;}// 只处理String类型~@Overridepublic boolean supports(Class<?> clazz) {return String.class == clazz;}@Overrideprotected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {// 哪编码的原则为:// 1、contentType自己指定了编码就以指定的为准// 2、没指定,但是类型是`application/json`,统一按照UTF_8处理// 3、否则使用默认编码:getDefaultCharset  ISO_8859_1Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());// 按照此编码,转换为字符串~~~return StreamUtils.copyToString(inputMessage.getBody(), charset);}// 显然,ContentLength和编码也是有关的~~~@Overrideprotected Long getContentLength(String str, @Nullable MediaType contentType) {Charset charset = getContentTypeCharset(contentType);return (long) str.getBytes(charset).length;}@Overrideprotected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {// 默认会给请求设置一个接收的编码格式~~~(若用户不指定,是所有的编码都支持的)if (this.writeAcceptCharset) {outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());}// 根据编码把字符串写进去~Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());StreamUtils.copy(str, charset, outputMessage.getBody());}...
}

我们有可以这么来写,达到我们一定的目的:

	// 因为它支持MediaType.TEXT_PLAIN, MediaType.ALL所有类型,所以你的contentType无所谓~~~ 它都能够处理@ResponseBody@RequestMapping(value = "/test", method = RequestMethod.POST)public String upload(@RequestBody String body) {return "Hello World";}

这种书写方式它不管是入参,还是返回值处理的转换器,都是用到的StringHttpMessageConverter。用它来接收入参和上面例子Resource有点像,只是StringHttpMessageConverter它只能解析文本内容,而Resource可以处理所有。

需要注意的是:若你的项目中大量使用到了此转换器,请一定要注意编码问题。一般不建议直接使用StringHttpMessageConverter,而是我们配置好编码(UTF-8)后,再把它加入到Spring MVC里面,这样就不会有乱码问题了

另外我们或许看到过有的小伙伴竟这么来写:为了给前端返回一个json串

    @ResponseBody@RequestMapping(value = "/test")public String test() {return "{\"status\":0,\"errmsg\":null,\"data\":{\"query\":\"酒店查询\",\"num\":65544,\"url\":\"www.test.com\"}}";}

虽然这么做结果是没有问题的,但是非常非常的不优雅,属于低级的行为。
通过自己构造Json串的形式(虽然你可能直接借助Fastjson去转,但也很低级),现在看来这么做是低级的、愚蠢的,小伙伴们千万别~~~~这么去做

BufferedImageHttpMessageConverter

处理java.awt.image.BufferedImage,和awt相关。略

GenericHttpMessageConverter 子接口

GenericHttpMessageConverter接口继承自HttpMessageConverter接口,二者都是在org.springframework.http.converter包下。它的特点就是:它处理目标类型为泛型类型的类型~~~

public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {//This method should perform the same checks than {@link HttpMessageConverter#canRead(Class, MediaType)} with additional ones related to the generic type.// 它的效果同父接口的canRead,但是它是加了一个泛型类型~~~来加以更加详细的判断boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType);// 一样也是加了泛型类型T read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException;//@since 4.2boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType);// @since 4.2void write(T t, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}

在这里插入图片描述
可以看出处理Json方面的转换器,都实现了此接口。此处主要以阿里巴巴的FastJson转换器为例加以说明:

FastJsonHttpMessageConverter

它和Gson和fastjson类似,只不过它内部引擎用的是Ali的FastJson库

// Fastjson for Spring MVC Converter. Compatible Spring MVC version 3.2+
// @since 1.2.10
public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>implements GenericHttpMessageConverter<Object> {public FastJsonHttpMessageConverter() {super(MediaType.ALL);}// 永远返回true,表示它想支持所有的类型,所有的MediaType,现在这算一个小Bug@Overrideprotected boolean supports(Class<?> clazz) {return true;}// 它竟然对泛型Type都没有任何的实现,这也是一个小bug// 包括读写的时候  对泛型类型都没有做很好的处理~~~public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {return super.canRead(contextClass, mediaType);}public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {return super.canWrite(clazz, mediaType);}// 这是处理读的方法,主要依赖于JSON.parseObject这个方法解析成一个objectprivate Object readType(Type type, HttpInputMessage inputMessage) {try {InputStream in = inputMessage.getBody();return JSON.parseObject(in,fastJsonConfig.getCharset(),type,fastJsonConfig.getParserConfig(),fastJsonConfig.getParseProcess(),JSON.DEFAULT_PARSER_FEATURE,fastJsonConfig.getFeatures());} catch (JSONException ex) {throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex);} catch (IOException ex) {throw new HttpMessageNotReadableException("I/O error while reading input message", ex);}}
}

总体来说,如果你是FastJson的死忠粉,你可以替换掉默认的Jackson的实现方式。但是由于FastJson在效率在对标Jackson并没有多少优势,所以绝大多数情况下,我并不建议修改Spring MVC处理json的默认行为

ResourceRegionHttpMessageConverter

org.springframework.core.io.support.ResourceRegion有关,它只能写为一个ResourceRegion或者一个它的List

只能写不能读,读方法都会抛异常~

// 这个类很简单,就是对Resource的一个包装  所以它和`application/octet-stream`也是有关的
// @since 4.3
public class ResourceRegion {private final Resource resource;private final long position;private final long count;...
}

若你报错说ResourceRegionHttpMessageConverter类找不到,请检查你的Spring版本。因此此类@since 4.3

自定义消息转换器PropertiesHttpMessageConverter处理Properties类型数据

自定义的主要目的是加深对消息转换器的理解。此处我们仍然是通过继承AbstractHttpMessageConverter方式来扩展:

public class PropertiesHttpMessageConverter extends AbstractHttpMessageConverter<User> {// 用于仅仅只处理我自己自定义的指定的MediaTypeprivate static final MediaType DEFAULT_MEDIATYPE = MediaType.valueOf("application/properties");public PropertiesHttpMessageConverter() {super(DEFAULT_MEDIATYPE);setDefaultCharset(StandardCharsets.UTF_8);}// 要求入参、返回值必须是User类型我才处理@Overrideprotected boolean supports(Class<?> clazz) {return clazz.isAssignableFrom(User.class);}@Overrideprotected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {InputStream is = inputMessage.getBody();Properties props = new Properties();props.load(is);// user的三个属性String id = props.getProperty("id");String name = props.getProperty("name");String age = props.getProperty("age");return new User(Integer.valueOf(id), name, Integer.valueOf(age));}@Overrideprotected void writeInternal(User user, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {OutputStream os = outputMessage.getBody();Properties properties = new Properties();// 属性判空此处我就不做了~~~properties.setProperty("id", user.getId().toString());properties.setProperty("name", user.getName());properties.setProperty("age", user.getAge().toString());properties.store(os, "user comments");}
}

其实发现,处理代码并不多。需要注意的是:此处我们只处理我们自定义的application/properties-user这一种MediaType即可,职责范围放到最小。

接下来就是要注册进Spring MVC里:

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {@Overridepublic void extendMessageConverters(List<HttpMessageConverter<?>> converters) {// 因为此转换器职责已经足够单一,所以放在首位是木有问题的~converters.add(0, new PropertiesHttpMessageConverter());// 若放在末尾,将可能不会生效~~~~(比如如果Fastjson转换器 处理所有的类型的话,所以放在首位最为保险)//converters.add(0, new PropertiesHttpMessageConverter());}
}

这里需要注意的是,为了避免意外,一定要注意自定义消息转换器的注册顺序问题。至于为什么,在参考阅读的博文里已经详细解释了~~~

编写Handler处理器如下:

    @ResponseBody@RequestMapping(value = "/test/properties", method = RequestMethod.POST)public User upload(@RequestBody User user) {System.out.println(user);return user;}

下面可以用postman模拟访问了,就能看到如下效果
在这里插入图片描述
这样就大功告成了,我们自定义的消息处理器,只处理我们我们指定的MediaType、指定的Class类型,可以帮助我们实现某些个性化逻辑

Spring MVC默认注册哪些HttpMessageConverter

说明:此处情况完全以Spring MVC版本讲解,和Spring Boot无关。

Spring 版本号为:5.1.6.RELEASE

不开启该注解:@EnableWebMvc
在这里插入图片描述
开启该注解:@EnableWebMvc
在这里插入图片描述
可以看到@EnableWebMvc注解的“威力”还是蛮大的,一下子让Spring MVC变强不少,所以一般情况下,我是建议开启它的。

当然如果是在Spring Boot环境下使用Spring MVC,到时候会再具体问题具体分析~~~
在纯Spring环境下,我是无理由建议标注@EnableWebMvc上此注解的
而且从上面可以看出,若我们classpath下有Jackson的包,那装配的就是MappingJackson2HttpMessageConverter,若没有jackson包有gson包,那装配的就是gson转换器。

小细节
  1. 如果一个Controller类里面所有方法的返回值都需要经过消息转换器,那么可以在类上面加上@ResponseBody注解或者将@Controller注解修改为@RestController注解,这样做就相当于在每个方法都加上了@ResponseBody注解了(言外之意别的方式都是不会经历消息转换器的)
  2. @ResponseBody@RequestBody都可以处理Map类型的对象。如果不确定参数的具体字段,可以用Map接收。@ReqeustBody同样适用。(List也是木有问题的)
  3. 方法上的和类上的@ResponseBody都可以被继承
  4. 默认的xml转换器Jaxb2RootElementHttpMessageConverter需要类上有@XmlRootElement注解才能被转换(虽然很少使用但此处还是指出)
    @Overridepublic boolean canWrite(Class<?> clazz, MediaType mediaType) {return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));}
  1. 返回值类型可声明为基类的类型,不影响转换(比如我们返回值是Object都是木有关系的)。但参数的类型必需为特定的类型(最好不要用接口类型,当然有的时候也是可以的比如Map/List/Resource等等)。这是显而易见的
最后

请求和响应都有对应的body,而这个body就是需要关注的主要数据。

请求体与请求的查询参数或者表单参数是不同的:
请求体的表述一般就是一段字符串(当然也可能是二进制),而查询参数可以看作url的一部分,这两个是位于请求报文的不同地方
表单参数可以按照一定格式放在请求体中,也可以放在url上作为查询参数。

响应体则是浏览器渲染页面的依据,对于一个普通html页面得响应,响应体就是这个html页面的源代码。
请求体和响应体都是需要配合Content-Type头部使用的,这个头部主要用于说明body中得字符串是什么格式的,比如:text,json,xml等。

  • 对于请求报文,只有通过此头部,服务器才能知道怎么解析请求体中的字符串
  • 对于响应报文,浏览器通过此头部才知道应该怎么渲染响应结果,是直接打印字符串还是根据代码渲染为一个网页

还有一个与body有关的头部是Accept,这个头部标识了客户端期望得到什么格式的响应体。服务器可根据此字段选择合适的结果表述。

对于HttpServletRequestHttpServletResponse,可以分别调用getInputStreamgetOutputStream来直接获取body,但是获取到的仅仅只是一段字符串。
而对于Java来说,处理一个对象肯定比处理一个字符串要方便得多,也好理解得多。

所以根据Content-Type头部,将body字符串转换为java对象是常有的事。反过来,根据Accept头部,将java对象转换客户端期望格式的字符串也是必不可少的工作。

因此本文讲述的消息转换器HttpMessageConverter就是专门来实现请求体/响应体到Java对象之间的转换的,具有非常重要的意义


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

这篇关于【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter 消息转换器详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/164697

相关文章

IDEA运行spring项目时,控制台未出现的解决方案

《IDEA运行spring项目时,控制台未出现的解决方案》文章总结了在使用IDEA运行代码时,控制台未出现的问题和解决方案,问题可能是由于点击图标或重启IDEA后控制台仍未显示,解决方案提供了解决方法... 目录问题分析解决方案总结问题js使用IDEA,点击运行按钮,运行结束,但控制台未出现http://

解决Spring运行时报错:Consider defining a bean of type ‘xxx.xxx.xxx.Xxx‘ in your configuration

《解决Spring运行时报错:Considerdefiningabeanoftype‘xxx.xxx.xxx.Xxx‘inyourconfiguration》该文章主要讲述了在使用S... 目录问题分析解决方案总结问题Description:Parameter 0 of constructor in x

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时

Java中注解与元数据示例详解

《Java中注解与元数据示例详解》Java注解和元数据是编程中重要的概念,用于描述程序元素的属性和用途,:本文主要介绍Java中注解与元数据的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参... 目录一、引言二、元数据的概念2.1 定义2.2 作用三、Java 注解的基础3.1 注解的定义3.2 内

JavaScript中的isTrusted属性及其应用场景详解

《JavaScript中的isTrusted属性及其应用场景详解》在现代Web开发中,JavaScript是构建交互式应用的核心语言,随着前端技术的不断发展,开发者需要处理越来越多的复杂场景,例如事件... 目录引言一、问题背景二、isTrusted 属性的来源与作用1. isTrusted 的定义2. 为

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

四种Flutter子页面向父组件传递数据的方法介绍

《四种Flutter子页面向父组件传递数据的方法介绍》在Flutter中,如果父组件需要调用子组件的方法,可以通过常用的四种方式实现,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录方法 1:使用 GlobalKey 和 State 调用子组件方法方法 2:通过回调函数(Callb

一文详解Python中数据清洗与处理的常用方法

《一文详解Python中数据清洗与处理的常用方法》在数据处理与分析过程中,缺失值、重复值、异常值等问题是常见的挑战,本文总结了多种数据清洗与处理方法,文中的示例代码简洁易懂,有需要的小伙伴可以参考下... 目录缺失值处理重复值处理异常值处理数据类型转换文本清洗数据分组统计数据分箱数据标准化在数据处理与分析过

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

SpringBoot项目中Maven剔除无用Jar引用的最佳实践

《SpringBoot项目中Maven剔除无用Jar引用的最佳实践》在SpringBoot项目开发中,Maven是最常用的构建工具之一,通过Maven,我们可以轻松地管理项目所需的依赖,而,... 目录1、引言2、Maven 依赖管理的基础概念2.1 什么是 Maven 依赖2.2 Maven 的依赖传递机