阅读完需:约 57 分钟
本文介绍Spring MVC
中的一个极其重要的组件:HttpMessageConverter
消息转换器。
有一副非常著名的图,来形容Spring MVC
对一个请求的处理:
从图中可见HttpMessageConverter
对Spring MVC
的重要性。它对请求、响应都起到了非常关键的作用
这是以前关于HttpMessageConverter
的笔记,只是一个初探。
为何需要消息转换器
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
在具体讲解之前,先对所有的转换器来个概述:
名称 | 作用 | 读支持MediaType | 写支持MediaType | 备注 |
FormHttpMessageConverter | 表单与MultiValueMap 的相互转换 |
application/x-www-form-urlencoded | application/x-www-form-urlencoded和multipart/form-data
|
可用于处理下载 |
XmlAwareFormHttpMessageConverter | Spring3.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 | 内部持有一个StringHttpMessageConverter 和ConversionService
|
他俩的&& | 他俩的&& | |
RssChannelHttpMessageConverter | 处理RSS <channel> 元素 | application/rss+xml |
application/rss+xml | |
MappingJackson2HttpMessageConverter | 使用Jackson的ObjectMapper 转换Json数据 |
application/json和application/*+json | application/json和application/*+json | 默认编码UTF-8 |
MappingJackson2XmlHttpMessageConverter | 使用Jackson的XmlMapper 转换XML数据 |
application/xml和text/xml | application/xml和text/xml | 需要额外导包Jackson-dataformat-XML 才能生效。从Spring4.1后才有 |
GsonHttpMessageConverter | 使用Gson 处理Json数据 |
application/json | application/json | 默认编码UTF-8 |
ResourceRegionHttpMessageConverter | 数据和org.springframework.core.io.support.ResourceRegion 的转换 |
application/octet-stream | application/octet-stream | Spring4.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的解析,也不关注了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>
既然它是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
。因为它独立的存在,所以可以看看源码内容:
// @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;
}
}
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();
@Nullable
private Charset defaultCharset;
// supports是个抽象方法,交给子类自己去决定自己支持的转换类型~~~~
// 而canRead(mediaType)表示MediaType也得在我支持的范畴了才行(入参MediaType若没有指定,就返回true的)
@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canRead(mediaType);
}
// 原理基本同上,supports和上面是同一个抽象方法 所以我们发现并不能入参处理Map,出餐处理List等等
@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
return supports(clazz) && canWrite(mediaType);
}
// 这是Spring的惯用套路:readInternal 虽然什么都没做,但我觉得还是挺有意义的。Spring后期也非常的好扩展了~~~~
@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return readInternal(clazz, inputMessage);
}
// 整体上就write方法做了一些事~~
@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
// 设置一个headers.setContentType 和 headers.setContentLength
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
// StreamingHttpOutputMessage增加的setBody()方法,关于它下面会给一个使用案例~~~~
streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
// 注意此处复写:返回的是outputStream ,它也是靠我们的writeInternal对它进行写入的~~~~
@Override
public OutputStream getBody() {
return outputStream;
}
@Override
public 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库很有作用
@FunctionalInterface
interface Body {
// 把当前的这个body写进给定的OutputStream
void writeTo(OutputStream outputStream) throws IOException;
}
}
SourceHttpMessageConverter
处理一些和xml相关的资源,比如DOMSource、SAXSource、SAXSource
等等
ResourceHttpMessageConverter
负责读取资源文件和写出资源文件数据
它来处理把Resource
进行写出去。当然它也可以把body的内容写进到Resource
里来。
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
// 是否支持读取流信息
private final boolean supportsReadStreaming;
// 默认支持所有的MediaType~~~~~ 但是它有个类型匹配,所以值匹配入参/返回类型是Resource类型的
public ResourceHttpMessageConverter() {
super(MediaType.ALL);
this.supportsReadStreaming = true;
}
@Override
protected boolean supports(Class<?> clazz) {
return Resource.class.isAssignableFrom(clazz);
}
// 直观感受:读的时候也只支持InputStreamResource和ByteArrayResource这两种resource的直接封装
@Override
protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
if (this.supportsReadStreaming && InputStreamResource.class == clazz) {
return new InputStreamResource(inputMessage.getBody()) {
@Override
public 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
@Nullable
public String getFilename() {
return inputMessage.getHeaders().getContentDisposition().getFilename();
}
};
}
else {
throw new HttpMessageNotReadableException("Unsupported resource class: " + clazz, inputMessage);
}
}
@Override
protected 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 resource
dumpStream(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
@Override
protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
return getGson().fromJson(reader, resolvedType);
}
@Override
protected 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);
}
}
总体上看,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;
@Nullable
private 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类型~
@Override
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
// 哪编码的原则为:
// 1、contentType自己指定了编码就以指定的为准
// 2、没指定,但是类型是`application/json`,统一按照UTF_8处理
// 3、否则使用默认编码:getDefaultCharset ISO_8859_1
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
// 按照此编码,转换为字符串~~~
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}
// 显然,ContentLength和编码也是有关的~~~
@Override
protected Long getContentLength(String str, @Nullable MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
return (long) str.getBytes(charset).length;
}
@Override
protected 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.2
boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType);
// @since 4.2
void 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
@Override
protected 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这个方法解析成一个object
private 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> {
// 用于仅仅只处理我自己自定义的指定的MediaType
private static final MediaType DEFAULT_MEDIATYPE = MediaType.valueOf("application/properties");
public PropertiesHttpMessageConverter() {
super(DEFAULT_MEDIATYPE);
setDefaultCharset(StandardCharsets.UTF_8);
}
// 要求入参、返回值必须是User类型我才处理
@Override
protected boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(User.class);
}
@Override
protected 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));
}
@Override
protected 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 {
@Override
public 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转换器。
细节
- 如果一个Controller类里面所有方法的返回值都需要经过消息转换器,那么可以在类上面加上
@ResponseBody
注解或者将@Controller
注解修改为@RestController
注解,这样做就相当于在每个方法都加上了@ResponseBody
注解了(言外之意别的方式都是不会经历消息转换器的) -
@ResponseBody
和@RequestBody
都可以处理Map类型的对象。如果不确定参数的具体字段,可以用Map接收。@ReqeustBody
同样适用。(List也是没有问题的) - 方法上的和类上的
@ResponseBody
都可以被继承 - 默认的xml转换器
Jaxb2RootElementHttpMessageConverter
需要类上有@XmlRootElement
注解才能被转换(虽然很少使用但此处还是指出)
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
}
- 返回值类型可声明为基类的类型,不影响转换(比如我们返回值是Object都是木有关系的)。但参数的类型必需为特定的类型(最好不要用接口类型,当然有的时候也是可以的比如Map/List/Resource等等)。这是显而易见的
请求和响应都有对应的body,而这个body就是需要关注的主要数据。
请求体与请求的查询参数或者表单参数是不同的:
- 请求体的表述一般就是一段字符串(当然也可能是二进制),而查询参数可以看作url的一部分,这两个是位于请求报文的不同地方
- 表单参数可以按照一定格式放在请求体中,也可以放在url上作为查询参数。
响应体则是浏览器渲染页面的依据,对于一个普通html页面得响应,响应体就是这个html页面的源代码。
请求体和响应体都是需要配合Content-Type头部使用的,这个头部主要用于说明body中得字符串是什么格式的,比如:text,json,xml等。
- 对于请求报文,只有通过此头部,服务器才能知道怎么解析请求体中的字符串
- 对于响应报文,浏览器通过此头部才知道应该怎么渲染响应结果,是直接打印字符串还是根据代码渲染为一个网页
还有一个与body有关的头部是Accept,这个头部标识了客户端期望得到什么格式的响应体。服务器可根据此字段选择合适的结果表述。
对于HttpServletRequest
和HttpServletResponse
,可以分别调用getInputStream
和getOutputStream
来直接获取body,但是获取到的仅仅只是一段字符串。
而对于Java来说,处理一个对象肯定比处理一个字符串要方便得多,也好理解得多。
所以根据Content-Type头部,将body字符串转换为java对象是常有的事。反过来,根据Accept头部,将java对象转换客户端期望格式的字符串也是必不可少的工作。
消息转换器HttpMessageConverter
就是专门来实现请求体/响应体到Java对象之间的转换的,具有非常重要的意义
匹配选择原理
介绍Spring MVC
中消息转换器的关键作用,并且也知道Spring MVC
其实是内置了非常非常多的转换器来处理各种各样的MediaType
。绝大多数情况下我们并不需要自己去定义转换器,全都交给Spring MVC
去处理就够了
但是Spring MVC既然帮我们内置了这么多的转换器,它默认都给我们加载进去了哪些了?若不是全部都加载进去,那我们遇到特殊的需求怎么自己往里放呢?
另外,我们一个请求request进来,Spring MVC到底是运用了怎么样的匹配规则,匹配到一个最适合的转换器进行消息转换的呢?
HTTP MediaType的基本知识
第一点
从上图可以看出Response
的Content-Type
为text/html
,但是我们需要明白的是:决定Response的Content-Type
的第一要素是Request请求头中的Accept
属性的值,它也被称为MediaType
。
这个Accept
的值传给服务端,如果服务端支持这种MediaType
,那么服务端就按照这个MediaType
来返回对应的格式给Response,同时会把返回的的Content-Type
设置成对应格式的MediaType
若服务端明确不支持请求头中Accept
指定的任何值时,那么就应该返回Http状态码:406 Not Acceptable
比如上面截图例子:请求头中Accept
支持多种MediaType
,服务端最终返回的Content-Type
为text/html
显然是没有问题的
第二点
如果Accept指定了多个MediaType,并且服务端也支持多个MediaType,那么Accept应该同时指定各个MediaType的QualityValue(也就是如图中的q值),服务端根据q值的大小来决定这几个MediaType类型的优先级,一般是大的优先。q值不指定时,默认视为q=1.
上图的Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,
/
;q=0.8,application/signed-exchange;v=b3
为Chrome浏览器的默认请求头的值。
它的含义为:服务端在支持的情况下应该优先返回text/html,其次是application/xhtml+xml。前面几个都不支持时,服务器可以自行处理 /,返回一种服务器自己支持的格式。
第三点
一个HTTP请求没有指定Accept,默认视为指定 Accept: /;请求头里没有指定Content-Type
,默认视为 null
,就是没有。
第四点
Content-Type
若指定了,必须是具体确定的类型,不能包含 *.
注意:上面属于Http规范的范畴,Spring MVC
基本遵循上面这几点
Spring MVC默认加载的消息转换器有哪些
为了更好的理解Spring MVC对消息转换器的匹配规则,先弄清楚Spring MVC默认给我们加载了哪些HttpMessageConverter
呢
首先我们从现象上直观的看一下:
(因为消息转换器都放在了RequestMappingHandlerAdapter
里,所以我们只需要关注运行时它里面的这个属性值即可)
开启了@EnableWebMvc
: 一共会有8个,只要我们classpath下有jackson的包,就会加载它进来。
理由如下:看代码吧(因为开启了@EnableWebMvc
,所以看WebMvcConfigurationSupport
它):
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
...
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
// 调用者自己配置消息转换器
// 若调用者自己没有配置,那就走系统默认的转换器们~~~~~
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
// 不管调用者配不配置,通过扩展接口进来的转换器都会添加进来
// 因为复写此个protected方法也是我们最为常用的自定义消息转换器的一个手段~~~~~
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
...
// 大多数情况下,我们并不需要配置。因此看看系统默认的addDefaultHttpMessageConverters(this.messageConverters);
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
try {
messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder 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) {
Jackson2ObjectMapperBuilder 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) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}
}
这个逻辑走下来,最终能被添加进去就是我们截图的那8个(当然这里指的我们只导入jackson处理json的这个jar的情况下)
说明一点:jackson2SmilePresent
用于处理application/x-jackson-smile
,代表类为:com.fasterxml.jackson.dataformat.smile.SmileFactory
jackson2CborPresent
用于处理application/cbor
,代表类为com.fasterxml.jackson.dataformat.cbor.CBORFactory
(Smile和CBOR就是一种数据格式,只是jackson强大的都给与了支持)当下绝大多数情况下我们只需要处理Json数据,所以只需要导入如下一个包即可:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
非常不建议导入jackson-all
做这种全量导入,臃肿
Smile是
二进制的JSON数据格式
,等同于标准的JSON数据格式。Smile格式于2010年发布,于2010年9月Jackson 1.6版已开始支持
没有开启@EnableWebMvc
: ,情况就不一样了:
我们发现仅仅只有4个,并且它并没有处理返回为Json的数据转换器。因此假如我们有如下两个Handler:
// 返回值为string类型
@ResponseBody
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String helloGet() throws Exception {
// 请注意:我这里又有中文 又有英文
return "哈喽,world";
}
// 返回值是个对象,希望被转换为
@ResponseBody
@RequestMapping(value = "/hello/json", method = RequestMethod.GET)
public Parent helloGetJson() throws Exception {
return new Parent("fsx", 18);
}
浏览器显示为:???world
,可以看见String类型能够正常处理,但是若出现中文需要注意处理。
再看第二个请求:
浏览器会显示报错:
它原理就是初始化RequestMappingHandlerAdapter
构造函数里默认加入的那4个:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
public RequestMappingHandlerAdapter() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(stringHttpMessageConverter);
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
...
}
由此可见,当我们使用Spring MVC的时候,强烈建议开启注解:@EnableWebMvc
,否则功能是比较弱的。
Spring MVC的转换器匹配原理
涉及到转换器的匹配,其实就有对read的匹配和write的匹配。
因为上面我们已经主要接触到了写的过程(比如String、json转换到body里),所以此处我们下跟踪看看向body里write内容的时候是怎么匹配的。
Response返回向body里write时消息转换器的匹配
此处先以请求:http://localhost:8080/demo_war_war/hello
为例
我们知道请求交给DispatcherServlet#doDispatch
方法,最终会匹配到一个HandlerAdapter
然后调用其ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
方法真正处理请求,然后最终都是返回一个ModelAndView
因为此处处理的是write过程,所以处理的是返回值。所以最终处理的是:RequestResponseBodyMethodProcessor#handleReturnValue()
:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
...
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 这里找到消息转换器,来把返回的结果写进response里面~~~
// 该方法位于父类`AbstractMessageConverterMethodArgumentResolver`中,通用的利用转换器处理返回值的方法
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
...
}
关于返回值的匹配原理
Request请求read请求参数、请求body时消息转换器的匹配(本文重点)
相应的,处理请求@RequestBody
的处理器选择,也发生在RequestResponseBodyMethodProcessor
里
此处以这个处理器为例进行讲解:
@ResponseBody
@RequestMapping(value = "/hello/post", method = RequestMethod.POST)
public Person hello(@RequestBody Person person) {
return person;
}
若我们用postman发如下请求:
服务端日志也能收到如下的警告信息
收到的竟然是一个报错,what竟然不支持我的类型???
这就是为什么我要解释上面的Http基础原理的原因了。
如图其实根本原因是Postman
给我们发送请求的时候,默认给我们发送了一个content-type
,有点自作主张了,所以导致的这问题。(Chome是不会这样自作主张的~~~)
解决方案其实非常简单:我们自己指定一个Content-Type:application/json
就木问题了
至于根本原因,看下面的源码分析
RequestResponseBodyMethodProcessor匹配入参的消息转换器
我们可以先看看RequestResponseBodyMethodProcessor
这个处理器:
// @since 3.1
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
...
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 入参是支持使用Optional包装一层的~~~
parameter = parameter.nestedIfOptional();
// 这个方法就特别重要了,实现就在下面,现在强烈要求吧目光先投入到下面这个方法实现上~~~~
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 拿到入参的形参的名字 比如此处为person
String name = Conventions.getVariableNameForParameter(parameter);
// 下面就是进行参数绑定、数据适配、转换的逻辑了 这个在Spring MVC处理请求参数这一章会详细讲解
// 数据校验@Validated也是在此处生效的
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
// 之前讲过writeWithMessageConverters,这个相当于读的时候进行一个消息转换器的匹配,实现逻辑大体一致~~~
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
// 核心是这个方法,它的实现逻辑在父类AbstractMessageConverterMethodArgumentResolver上~~~继续转移目光吧~~~
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
// body体为空,而且不是@RequestBody(required = false),那就抛错呗 请求的body是必须的 这个很好理解
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
}
上面已经分析到,读取的核心匹配逻辑,其实在父类AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
,因此我们继续看父类的实现源码:
// @since 3.1 可议看出这个实现是处理请求request的处理器~~~
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
...
@SuppressWarnings("unchecked")
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
// 如果已经制定了contentType,那就没什么好说的 以你的为准
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
// Content-Type默认值:application/octet-stream
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
// 这几句代码就是解析出入参的类型~
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
// 这里就是匹配消息转换器的的逻辑~~~ 还是一样的 优先以GenericHttpMessageConverter这种类型的转换器为准
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))) {
// 因为它是一个EmptyBodyCheckingHttpInputMessage,所以它有这个判断方法 body != null表示有body内容
// 注意此处的判断,若有body的时候,会执行我们配置的`RequestBodyAdvice` 进行事前、时候进行处理~~~ 后面我会示范
if (message.hasBody()) {
HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
// 执行消息转换器的read方法,从inputStream里面读出一个body出来~
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
// 读出body后的事后工作~~~
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
// 如果没有body体,就交给handleEmptyBody来处理~~~~ 第一个参数永远为null
// 这个处理器可以让我们给默认值,比如body为null时,我们返回一个默认的body之类的吧
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
// 到此处,如果body还没有被赋值过~~~~ 此处千万不能直接报错,因为很多请求它可以不用body体的
// 比如post请求,body里有数据。但是Content-type确实text/html 就会走这里(因为匹配不到消息转换器)
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) {
return null;
}
// 比如你是post方法,并且还指定了ContentType 你有body但是却没有找到支持你这种contentType的消息转换器,那肯定就报错了~~~
// 这个异常会被InvocableHandlerMethod catch住,虽然不会终止程序。但是会打印一个warn日志~~~
// 并不是所有的类型都能read的,从上篇博文可议看出,消息转换器它支持的可以read的类型还是有限的
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}
MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});
return body;
}
...
}
又比如我们这样子写:使用MultiValueMap
是能够接收多值入参的。注意请设值Content-Type=application/x-www-form-urlencoded
,这样FormHttpMessageConverter
这个消息转换器可以转换它
借助RequestBodyAdvice实现对请求参数进行干预
RequestBodyAdvice
它能对入参body封装的前后、空进行处理。比如下面我们就可以很好的实现日志打打印:
public class LogRequestBodyAdvice implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
return httpInputMessage;
}
@Override
public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
Method method=methodParameter.getMethod();
log.info("{}.{}:{}",method.getDeclaringClass().getSimpleName(),method.getName(),JSON.toJSONString(o));
return o;
}
@Override
public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
Method method=methodParameter.getMethod();
log.info("{}.{}",method.getDeclaringClass().getSimpleName(),method.getName());
return o;
}
}
效率非常高,也不需要对request的body进行多次读取了。
使用@ControllerAdvice
注解+ResponseBodyAdvice+ResponseBodyAdvice
,可以对请求的输入输出进行处理,避免了在controller中对业务代码侵入。FastJson有提供一个JSONPResponseBodyAdvice
可以直接使用的
它还有一个很不错的应用场景:就是对请求、返回数据进行加密、解密
自定义消息转换器HttpMessageConverter
虽然前面说了,Spring MVC已经为我们准备了好多个消息转换器了,能应付99.99%
的使用场景了。
但是我们经常会遇到说不喜欢用它自带的Jackson来序列化,而想换成我们想要的国产的FastJson
转换器。怎么弄呢?那么接下来就来实现这一波
其实FastJson
还是非常友好的,早在@since 1.2.10
版本就已经为我们写好了FastJsonHttpMessageConverter
,我们直接使用即可。
Spring MVC内置支持了jackson,gson。但却没有内置支持我们国产的FastJson
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
// 请不要复写这个方法,否则你最终只有你配置的转换器,Spring MVC默认的并不会加载了~~
//@Override
//public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
// fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
// converters.add(fastJsonHttpMessageConverter);
//}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
converters.add(fastJsonHttpMessageConverter);
}
}
可议看到我们的FastJsonHttpMessageConverter
已经被配置进去了。
此处我们把Handler写成这样:
@ResponseBody
@RequestMapping(value = "/hello/post", method = RequestMethod.POST)
public Object hello(@RequestBody(required = false) Person person) {
return person;
}
关于read: 关于writer:
总结一句:我们发现不管是读还是写,最终生效的都还是MappingJackson2HttpMessageConverter
,我们自定义的FastJson转换器并没有生效。相信这个原因大家都知道了:FastJson转换器排在Jackson转换器的后面,所以处理json不会生效
找到原因,那就只要把 自定义的消息转换器【FastJsonJsonpHttpMessageConverter
】添加到 MappingJackson2HttpMessageConverter
前面就可以
如果你是SpringBoot环境,你可以直接使用HttpMessageConverters
很方便的把你自定义的转换器提到最高优先级,但是此处我们介绍一下Spring中的处理方式:
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
// 把FastJsonHttpMessageConverter放到Jackson的前面去
// 这个list是个ArrayList 所以我们要么就放在首位(不建议),而converters.indexOf() 因为人家是new的 所以肯定是找不到此对象的位置的 所以采用遍历的方式吧
int jacksonIndex = 0;
for (int i = 0; i < converters.size(); i++) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
jacksonIndex = i;
break;
}
}
// 添加在前面
converters.add(jacksonIndex, fastJsonHttpMessageConverter);
}
效果
最终效果也是没有问题的,json数据的转换工作都会被我们的FastJson
接管了,完美
看到说可以通过HttpMessageConverters
这种方式配置自定义的消息转换器,那是不严谨的。因为HttpMessageConverters
它属于SpringBoot
的类,而不是属于Spring Framework
的
FastJsonHttpMessageConverter
绝大多数情况下,直接使用FastJsonHttpMessageConverter
是没有问题的。但是如果是这样的:
@ResponseBody
@RequestMapping(value = "/hello/post", method = RequestMethod.POST)
public Object hello(@RequestBody(required = false) MultiValueMap person) {
return person;
}
报错
JSON parse error: unsupport type interface org.springframework.util.MultiValueMap; nested exception is com.alibaba.fastjson.JSONException: unsupport type interface org.springframework.util.MultiValueMap]
我们发现报错竟然找到了FastJson
的头上。说明了什么:责任链模式下,fastjson接了这个活,最终发现自己干不了,然后还抛出异常,这是明显的甩锅行为嘛
这个根本原因在这,看它的源码:
// 会发现FastJson这个转换器它接受所有类型它都表示可以处理~~~~
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
显然它违反了“手伸得太长的”基本原则,是需要付一定的责任的
正确使用姿势(推荐使用姿势)
为了避免误伤,其实我们配置它的时候应该限制它的作用范围,如下:
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
fastJsonHttpMessageConverter.setCharset(StandardCharsets.UTF_8);
// 这步是很重要的,让它只处理application/json这种MediaType的就没有问题了~~~~
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
// 把FastJsonHttpMessageConverter放到Jackson的前面去
// 这个list是个ArrayList 所以我们要么就放在首位(不建议),而converters.indexOf() 因为人家是new的 所以肯定是找不到此对象的位置的 所以采用遍历的方式吧
int jacksonIndex = 0;
for (int i = 0; i < converters.size(); i++) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
jacksonIndex = i;
break;
}
}
// 添加在前面
converters.add(jacksonIndex, fastJsonHttpMessageConverter);
}
这样它就不会造成问题了,因为我们只让它处理MediaType.APPLICATION_JSON_UTF8
这种类型,而这就是它最擅长的。
使用Spring MVC实现优雅的文件下载
传统的,我们要进行文件下载,可以直接操作HttpServletRequest
和HttpServletResponse
来处理下载。那基本上就与Spring MVC
的关系不大了。 我们能看到形如下面的代码:
//设置响应头和客户端保存文件名
response.setCharacterEncoding("utf-8");
response.setContentType("multipart/form-data");
response.setHeader("Content-Disposition", "attachment;fileName=" + fileName);
...
//打开本地文件流
InputStream inputStream = new FileInputStream(filePath);
//激活下载操作
OutputStream os = response.getOutputStream();
//循环写入输出流
byte[] b = new byte[2048];
int length;
while ((length = inputStream.read(b)) > 0) {
os.write(b, 0, length);
downloadedLength += b.length;
}
// 这里主要关闭。
os.close();
inputStream.close();
...
显然这一大段处理起来还是比较麻烦的。本文另外一种方案:在Spring MVC
环境下能让你优雅的处理文件下载:使用ResponseEntity
方式
DEMO
// 处理下载 get/post/put请求等等都是可以的 但一般都用get请求
@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<Resource> downloadFile(@RequestParam String fileName) {
// 构造下载对象 读取出一个Resource出来 此处以类路径下的logback.xml
DownloadFileInfoDto downloadFile = new DownloadFileInfoDto(fileName, new ClassPathResource("logback.xml"));
return downloadResponse(downloadFile);
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
private static class DownloadFileInfoDto {
private String fileName; // 下载的文件
private Resource resource; // 下载的具体文件资源
}
// 共用方法 兼容到了IE浏览器~~~
private static ResponseEntity<Resource> downloadResponse(
DownloadFileInfoDto fileInfo) {
String fileName = fileInfo.getFileName();
Resource body = fileInfo.getResource();
// ========通过User-Agent来判断浏览器类型 做一定的兼容~========
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String header = request.getHeader("User-Agent").toUpperCase();
HttpStatus status = HttpStatus.CREATED;
try {
if (header.contains("MSIE") || header.contains("TRIDENT") || header.contains("EDGE")) {
fileName = URLEncoder.encode(fileName, "UTF-8");
fileName = fileName.replace("+", "%20"); // IE下载文件名空格变+号问题
status = HttpStatus.OK;
} else { // 其它浏览器 比如谷歌浏览器等等~~~~
fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1");
}
} catch (UnsupportedEncodingException e) {
}
// =====响应头需设置为MediaType.APPLICATION_OCTET_STREAM=====
HttpHeaders headers = new HttpHeaders();
// 注意:若这个响应头不是必须的,但是如果你确定要下载,建议写上这个响应头~
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 此处,如果你自己没有设置这个请求头,浏览器肯定就不会弹窗对话框了。它就会以body的形式直接显示在浏览器上
headers.setContentDispositionFormData("attachment", fileName);
return new ResponseEntity<>(body, headers, status);
}
备注:使用此种方式最终处理返回值的处理器为:
HttpEntityMethodProcessor
,使用的消息转换器为:ResourceHttpMessageConverter
这样请求就能弹出下载框了。响应头如下:
可以看到这里不仅设置了Content-Disposition
请求头,还是设置了Content-type为application/octet- stream
那就意味着你不想让浏览器直接显示内容,而是弹出一个”文件下载”的对话框。
关于application/octet-stream
等响应头的解释,请看如下例子形象解释:
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="picture.png"
它表示对浏览器说:“我不清楚代码内容,请把其保存为一个文件,最好命名为picture.png”。
Content-Type: image/png
Content-Disposition: attachment; filename="picture.png"
它表示对浏览器说:表示“这是一个PNG图像,请将其保存为一个文件,最好命名为picture.png”。
Content-Type: image/png
Content-Disposition: inline; filename="picture.png"
它表示对浏览器说:“这是一个PNG图像,除非你不知道如何显示PNG图像,否则请显示它,如果用户选择保存它,我们建议文件名保存为picture.png”。
在能够识别内联的浏览器中,可议使用这个方法(现在绝大多数浏览器都能识别这种方式),少数浏览器会对它进行保存~~~~
所以当你给客户端传递的不知道是文本、图片、还是其它的格式时,使用application/octet-stream
最佳。(是否弹出下载框不是由它决定的,主要是Content-Disposition
这个请求头来决定的)
ResponseEntity方式对比传统Java方式
单单从代码上看ResponseEntity方式
秒杀传统的Java方式,但是否我们想都不想就采用优雅方式呢?但其实非也,有些东西还是要注意的。
- 基于ResponseEntity的实现的局限性还是很大:这种下载方式是一种一次性读取的下载方式,在文件较大的时候会直接抛出内存溢出(所以适合小文件下载,不超过1G吧)。还有就是这种下载方式因为是一次性全部输出,所以无法统计已下载量、未下载量等扩展功能,所以也就不能实现断点续传
- 传统Java通用实现在功能上能够更加的丰富:对下载文件的大小无限制((循环读取一定量的字节写入到输出流中,因此不会造成内存溢出)。 因为是这种实现方式是基于循环写入的方式进行下载,在每次将字节块写入到输出流中的时都会进行输出流的合法性检测,在因为用户取消或者网络原因造成socket断开的时候,系统会抛出SocketWriteException。 当然我们可以捕获到这个异常,记录下当前已经下载的数据量、下载状态等。这样我们就可以实现断点续传的功能了
ResponseEntity
方式的优点就是简洁,所以在比较小的文件下载时,它绝对是首选。若是有大量的下载需求,其实一般都建议使用ftp服务器而不是http了。
当然还有一种使用ResponseEntity<byte[]>
的方式,也挺优雅的,这里就只提供代码参考了:
@RequestMapping("/testResponseEntity")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
byte[] body = null;
ServletContext servletContext = session.getServletContext();
InputStream in = servletContext.getResourceAsStream("/files/abc.txt");
body = new byte[in.available()];
in.read(body);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition","attachment;filename=abc.txt");
HttpStatus statusCode = HttpStatus.OK;
ResponseEntity<byte[]> response = new ResponseEntity<byte[]>(body,headers,statusCode);
return response;
}
此处使用的返回值处理同上,消息转换器是:
ByteArrayHttpMessageConverter
代码中有顺序遍历匹配这种逻辑,或者叫责任链模式时,功能越具体的节点越是应该放在前面,功能最广最泛的节点应该放在最后面