title: SpringMVC 源码分析 -- HttpMessageConverter
date: 2020/11/06 09:42
引言
在项目中遇到了类似下面这样的代码,就比较好奇到底返回给前端的是文件流还是那个地址,并且跟下源码看看底层到底是怎么处理的。
@RestController
@SpringBootApplication
public class TestReponseApplication {
public static void main(String[] args) {
SpringApplication.run(TestReponseApplication.class, args);
}
@RequestMapping
public String func(HttpServletResponse response) throws Exception {
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition", "attachment; filename=aaa.jpg");
response.addHeader("Cache-Control", "no-cache");
FileInputStream in = new FileInputStream(new File("/Users/x5456/Downloads/1501994358.jpg"));
StreamUtils.copy(in, response.getOutputStream());
// 此时文件的所有字节全部传输到了前端,但是连接还没关闭
return "http://123.jpg";
}
}
测试的结果是返回了文件流,但是好奇http://123.jpg
这个返回信息跑哪去了,是 SpringMVC 看到 response 已经写入了,所以就将其抛弃了?但是看了源码之后发现好像并没有这个步骤,它是直接将http://123.jpg
继续写进了response 中,经过下载下来的文件对比也证明了这一点。
HttpMessageConverter
public interface HttpMessageConverter<T> {
/**
* 表明是否该消息转化器可以(从 request)读出给定的 class 的对象
*
* @param clazz 要测试可读性的类
* @param mediaType 要被读取的 MediaType 如果未指定为null,通常这个值是请求头中的 content-type
* @return 如果可读返回true,否则是false
*/
boolean canRead(Class<?> clazz, MediaType mediaType);
/**
* 表明是否该消息转化器可以(向 response)写入给定 class 的对象
*
* @param clazz 要测试可写性的类
* @param mediaType 要被读取的 MediaType 如果未指定为null,通常这个值是请求头中的Accept
* @return 如果可写返回true,否则是false
*/
boolean canWrite(Class<?> clazz, MediaType mediaType);
/**
* @return 返回这个消息转换器可以支持的MediaType的集合
*/
List<MediaType> getSupportedMediaTypes();
/**
* 从给出的input message中读出给定类型的对象 然后返回
*
* @param clazz 返回对象的类型. 这个类必须支持前面的canRead方法且返回true
* @param inputMessage the HTTP input message to read from
* @return 被转换的对象
*/
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/**
* 将给定的对象写入给定的输出消息
*
* @param t 要写入输出消息的对象. 这个类型必须通过了前面的canWrite方法且返回true
* @param contentType 编写时使用的内容类型. 可能是{@code null}来表示必须使用转换器的默认内容类型。如果不是{@code null},则此媒体类型必须调用过这个接口的{@link #canWrite canWrite}方法且必须返回{@code true}。
* @param outputMessage the message to write to
*/
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
Http报头Accept与Content-Type的区别?
1、Accept属于请求头, Content-Type属于实体头。
Http报头分为通用报头、请求报头、响应报头和实体报头。
请求方的http报头结构:通用报头|请求报头|实体报头
响应方的http报头结构:通用报头|响应报头|实体报头
2、Accept代表发送端(客户端)希望接受的数据类型。
比如:Accept:text/xml;
代表客户端希望接受的数据类型是xml类型
3、Content-Type代表发送端(客户端|服务器)发送的实体数据的数据类型。
比如:Content-Type:text/html;
代表发送端发送的数据格式是html。
???为啥读方法不需要 MediaType
在读写操作方面springMVC又提供了读操作接口HttpInputMessage和写操作接口HttpOutputMessage来完成数据的读写操作。
public interface HttpInputMessage extends HttpMessage {
// 底层实现 request.getInputStream();
InputStream getBody() throws IOException;
}
public interface HttpOutputMessage extends HttpMessage {
// 底层实现 response.getOutputStream();
OutputStream getBody() throws IOException;
}
// 父接口
public interface HttpMessage {
//获取头部中的信息
HttpHeaders getHeaders();
}
read 流程
我们从这篇文章的 tag4 的第三张图片继续往下讲,因为那上面讲的是 get 请求的参数解析器,没有用到 HttpMessageConverter,下面讲使用了 @RequestBody 注解所采用的参数解析器。
注:RequestResponseBodyMethodProcessor 校验是否能解析当前参数
我们接下来看下这三个方法吧,不写注释应该也能看懂,Advice 是通知的意思,所以他的作用就是在使用 @RequestBody 接收参数的前后做一些事情。
public interface RequestBodyAdvice {
boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable
Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}
其中默认有一个 JsonView 相关的 RequestAdvice,@JsonView 介绍
问题
- HttpMessageConverter 是怎么创建出来的?
之后又被 @EnableMvc 注解引入的配置类中的方法覆盖掉了(不过无所谓了)。
- 只有部分参数解析器需要 HttpMessageConverter,那么是怎么注入进去的呢
write 流程
我们从这篇文章的 tag5 的最后一张图片开始讲起。
因为他是直接写入流中的,没有管流里是否已经存在了,所以会出现下面这种情况:
常用HttpMessageConverter
与媒体类型的对应信息:
HttpMessageConverter | 处理哪些格式的数据 |
---|---|
StringHttpMessageConverter | 读取字符串格式的数据和写出字符串式的数据 |
ByteArrayHttpMessageConverter | 读取二进制格式的数据和写出二进制格式的数据 |
FormHttpMessageConverter | 负责读取form提交的数据(能读取的数据格式为 application/x-www-form-urlencoded,不能读取multipart/form-data格式数据);负责写入application/x-www-from-urlencoded和multipart/form-data格式的数据 |
ResourceHttpMessageConverter | 负责读取资源文件和写出资源文件数据 |
Jaxb2RootElementHttpMessageConverter | 读取和写入xml 标签格式的数据 |
ResourceHttpMessageConverter | 负责读取资源文件和写出资源文件数据 |
MappingJacksonHttpMessageConverter | 读取和写入json格式的数据 |
???MVC中的适配器作用是啥,他到底适配了什么