程序员社区

SpringMVC 源码分析 -- HttpMessageConverter


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 中,经过下载下来的文件对比也证明了这一点。

SpringMVC 源码分析 -- HttpMessageConverter插图

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 注解所采用的参数解析器。

SpringMVC 源码分析 -- HttpMessageConverter插图1

注:RequestResponseBodyMethodProcessor 校验是否能解析当前参数

SpringMVC 源码分析 -- HttpMessageConverter插图2
SpringMVC 源码分析 -- HttpMessageConverter插图3
SpringMVC 源码分析 -- HttpMessageConverter插图4
SpringMVC 源码分析 -- HttpMessageConverter插图5
SpringMVC 源码分析 -- HttpMessageConverter插图6

我们接下来看下这三个方法吧,不写注释应该也能看懂,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 介绍

SpringMVC 源码分析 -- HttpMessageConverter插图7

问题

  1. HttpMessageConverter 是怎么创建出来的?
SpringMVC 源码分析 -- HttpMessageConverter插图8
SpringMVC 源码分析 -- HttpMessageConverter插图9

之后又被 @EnableMvc 注解引入的配置类中的方法覆盖掉了(不过无所谓了)。

  1. 只有部分参数解析器需要 HttpMessageConverter,那么是怎么注入进去的呢
SpringMVC 源码分析 -- HttpMessageConverter插图10
SpringMVC 源码分析 -- HttpMessageConverter插图11

write 流程

我们从这篇文章的 tag5 的最后一张图片开始讲起。

SpringMVC 源码分析 -- HttpMessageConverter插图12
SpringMVC 源码分析 -- HttpMessageConverter插图13
SpringMVC 源码分析 -- HttpMessageConverter插图14
SpringMVC 源码分析 -- HttpMessageConverter插图15
SpringMVC 源码分析 -- HttpMessageConverter插图16
SpringMVC 源码分析 -- HttpMessageConverter插图17

因为他是直接写入流中的,没有管流里是否已经存在了,所以会出现下面这种情况:

SpringMVC 源码分析 -- HttpMessageConverter插图18

常用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中的适配器作用是啥,他到底适配了什么

赞(0) 打赏
未经允许不得转载:IDEA激活码 » SpringMVC 源码分析 -- HttpMessageConverter

一个分享Java & Python知识的社区