前言
本文介绍Spring MVC
中的一个极其重要的组件:HttpMessageConverter
消息转换器。
有一副非常著名的图,来形容Spring MVC
对一个请求的处理:
从图中可见HttpMessageConverter对Spring MVC的重要性。它对请求、响应都起到了非常关键的作用~
为何需要消息转换器
HttpMessageConverter是用来处理request和response里的数据的。.
请求和响应都有对应的body,而这个body就是需要关注的主要数据。
请求体的表述一般就是一段字符串,当然也可以是二进制数据(比如上传~)。
响应体则是浏览器渲染页面的依据,对于一个普通html页面得响应,响应体就是这个html页面的源代码。
请求体和响应体都是需要配合Content-Type头部使用的,这个头部主要用于说明body中得字符串是什么格式的,比如:text,json,xml等。对于请求报文,只有通过此头部,服务器才能知道怎么解析请求体中的字符串,对于响应报文,浏览器通过此头部才知道应该怎么渲染响应结果,是直接打印字符串还是根据代码渲染为一个网页
对于HttpServletRequest和HttpServletResponse,可以分别调用getInputStream和getOutputStream来直接获取body。但是获取到的仅仅只是一段字符串
**而对于java来说,处理一个对象肯定比处理一个字符串要方便得多,也好理解得多。**所以根据Content-Type头部,将body字符串转换为java对象是常有的事。反过来,根据Accept头部,将java对象转换客户端期望格式的字符串也是必不可少的工作。这就是我们本文所讲述的消息转换器的工作~
消息转换器它能屏蔽你对底层转换的实现,分离你的关注点,让你专心操作java对象,其余的事情你就交给我Spring MVC吧~大大提高你的编码效率(可议说比源生Servlet开发高级太多了)
Spring内置了很多HttpMessageConverter,比如MappingJackson2HttpMessageConverter,StringHttpMessageConverter,甚至还有FastJsonHttpMessageConverter(需导包和自己配置)
HttpMessageConverter
在具体讲解之前,先对所有的转换器来个概述:
Jaxb也是和Sax、Dom、JDOM类似的解析XML的类库,jackson-module-jaxb-annotations对它提供了支持,但是由于关注太少了,所以Jaxb相关的转换器此处省略~~~
MarshallingHttpMessageConverter也是Spring采用Marshaller/Unmarshaller的方式进行xml的解析,也不关注了
FastJsonHttpMessageConverter4和FastJsonpHttpMessageConverter4都继承自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 message HttpHeaders 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 { @Nullable default HttpMethod getMethod() { // 可议根据String类型的值 返回一个枚举 return HttpMethod.resolve(getMethodValue()); } String getMethodValue(); // 可以从请求消息里 拿到URL URI 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类型的对象 流对象为:HttpInputMessage T 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-urlencoded public 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; @Nullable private Charset multipartCharset; // 唯一的一个构造函数~ public FormHttpMessageConverter() { // 默认支持处理两种MediaType:application/x-www-form-urlencoded和multipart/form-data this.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它才会去处理~~~~ @Override public 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的区别,有点对着干的意思~~~ @Override public 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这个变量并没有使用~ @Override public 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-8 Charset 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; } }