• 【Spring Boot】响应JSON实现原理


    1、相关依赖

    web项目引入的启动器spring-boot-starter-web中含有

    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-jsonartifactId>
    	<version>2.7.0version>
    	<scope>compilescope>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个依赖下面又有jackson的相关依赖,用于json的转换

    <dependency>
      <groupId>com.fasterxml.jackson.coregroupId>
      <artifactId>jackson-databindartifactId>
      <version>2.13.3version>
      <scope>compilescope>
    dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatypegroupId>
      <artifactId>jackson-datatype-jdk8artifactId>
      <version>2.13.3version>
      <scope>compilescope>
    dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatypegroupId>
      <artifactId>jackson-datatype-jsr310artifactId>
      <version>2.13.3version>
      <scope>compilescope>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2、ReturnValueHandlers—返回值处理器

    之前我们分析了参数解析器argumentResolvers的相关源码,了解了请求中的参数是如何找到合适的参数解析器,并与方法的入参进行绑定的,那么问题来了,响应值的返回值处理器是如何找到的呢?

    要分析源码,我们还是得进入DispatcherServlet这个类下的doDiapatch()方法
    与之前分析参数解析器一样的流程,我们跟到了RequestMappingHandlerAdapter这个类下的invokeHandlerMethod()方法
    通过断点可以看到我们默认支持15种返回值解析器
    在这里插入图片描述

    那么他是怎么从这么多返回值解析器中选出自己支持的那一个呢?
    我们可以发现,这些返回值解析器都是实现了HandlerMethodReturnValueHandler接口

    package org.springframework.web.method.support;
    
    import org.springframework.core.MethodParameter;
    import org.springframework.lang.Nullable;
    import org.springframework.web.context.request.NativeWebRequest;
    
    public interface HandlerMethodReturnValueHandler {
        boolean supportsReturnType(MethodParameter returnType);
    
        void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此接口下只有2个方法,一个是判断是否支持此类型返回值,另一个是处理返回值的方法
    是不是和之前的参数解析器的接口类有异曲同工之妙?
    没错,我们正是使用这两个方法来寻找适合自己的返回值解析器以及处理我们的返回值

    跟着断点一直向下,我们跟到了HandlerMethodReturnValueHandlerComposite类下的handleReturnValue()方法,在这里,我们就找到了对应的解析器并执行了相关方法

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    	// 获取自己适合的返回值解析器
        HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        } else {
        	// 调用该返回值解析器中的处理返回值的方法
            handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
        }
    }
    
    @Nullable
    private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
    	// 判断是否是异步的返回值
        boolean isAsyncValue = this.isAsyncReturnValue(value, returnType);
        Iterator var4 = this.returnValueHandlers.iterator();
    
        HandlerMethodReturnValueHandler handler;
        // 使用do-while进行遍历,找出支持当前返回值类型的解析器
        do {
            do {
                if (!var4.hasNext()) {
                    return null;
                }
    
                handler = (HandlerMethodReturnValueHandler)var4.next();
            } while(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler));
        } while(!handler.supportsReturnType(returnType));
    
        return handler;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    最终,我们确定使用解析器RequestResponseBodyMethodProcessor可以处理标注了@ResponseBody的返回值

    3、HttpMessageConvert—消息转换器

    除了寻找合适的返回值解析器之外,我们还有一个问题要思考
    为什么我们在控制器类的方法上加一个@ResponseBody注解就能将响应信息转换成json格式呢?
    这个转换是怎样实现的呢?

    我们接着上面的代码继续往下debug
    之前我们已经定位到了使用RequestResponseBodyMethodProcessor来处理@ResponseBody的返回值,所以我们继续深入到这个类下的handleReturnValue()

    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    	mavContainer.setRequestHandled(true);
    	// 创建输入和输出信息
    	ServletServerHttpRequest inputMessage = this.createInputMessage(webRequest);
    	ServletServerHttpResponse outputMessage = this.createOutputMessage(webRequest);
    	// 使用消息转换器进行写出操作
    	this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后我们来分析解析器父类AbstractMessageConverterMethodProcessor下的writeWithMessageConverters()方法

    首先我们了解一个概念:什么是内容协商?
    浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型,这里的q代表优先级
    然后服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
    在这里插入图片描述
    所以在writeWithMessageConverters()方法中我们获取到浏览器能接受什么内容
    在这里插入图片描述
    以及服务器能产生什么内容
    在这里插入图片描述
    然后我们经过协商,决定返回application/json格式的数据
    重点来了:这里有一系列的消息转换器,我们到底使用哪个呢?
    在这里插入图片描述
    随便点进一个消息转换器,我们发现他都是实现了HttpMessageConverter这个接口
    这里面有2个方法,canRead()和canWrite()来帮助我们判断能否支持源类型和目标类型的消息读写

    最终我们确定了,使用MappingJackson2HttpMessageConverter这个消息转换器来进行json转换

    package org.springframework.http.converter;
    
    import java.io.IOException;
    import java.util.Collections;
    import java.util.List;
    import org.springframework.http.HttpInputMessage;
    import org.springframework.http.HttpOutputMessage;
    import org.springframework.http.MediaType;
    import org.springframework.lang.Nullable;
    
    public interface HttpMessageConverter<T> {
    	// 读取,将我们当前消息转换器支持的对象以某种格式读取进来,例如将json数据读取成Person对象
        boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    
    	// 写出,我们当前消息转换器支持的对象以某种格式写出去,例如将Person对象以json格式写出去
        boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    
        List<MediaType> getSupportedMediaTypes();
    
        default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
            return !this.canRead(clazz, (MediaType)null) && !this.canWrite(clazz, (MediaType)null) ? Collections.emptyList() : this.getSupportedMediaTypes();
        }
    
        T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
    
        void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    然后我们在此处调用MappingJackson2HttpMessageConverter父类AbstractGenericHttpMessageConverter中的write()方法
    在这里插入图片描述
    在这里插入图片描述
    走到AbstractJackson2HttpMessageConverter类下的writeInternal()方法
    可以看出,它利用了底层的Jackson的objectMapper进行转换
    这样,我们就完成了Person数据到json格式的转换

    大概流程如下

    • 判断当前响应头中是否已经有确定的媒体类型,即MediaType
    • 获取客户端(PostMan、浏览器)支持接收的内容类型(获取客户端Accept请求头字段)
    • 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person),找到支持操作Person的converter,把该converter支持的媒体类型统计出来放到集合中,这样就获取到服务端能提供哪些媒体类型
    • 进行内容协商,找到最佳匹配媒体类型
    • 用 支持 将对象转为 最佳匹配媒体类型 的converter,调用它进行转化
    4、开启浏览器参数方式内容协商功能

    如果需要返回xml格式的数据,那么需要额外导入相关依赖

    <dependency>
    	<groupId>com.fasterxml.jackson.dataformatgroupId>
    	<artifactId>jackson-dataformat-xmlartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    我们可以看到,浏览器是默认支持如下返回格式的,一般情况下,我们无法指定自己需要的返回格式
    在这里插入图片描述

    但是我们可以通过修改配置+添加参数的方式指定我们需要的格式
    首先,我们在yaml文件中,开启基于浏览器参数方式内容协商功能

    spring:
      mvc:
        contentnegotiation:
          favor-parameter: true
    
    • 1
    • 2
    • 3
    • 4

    一旦此参数设置为true,那么我们的内容协商管理器contentNegotiationManager中,除了原有的从请求头获取媒体类型的策略之外,还多了一个从请求参数中获取媒体类型的策略,它支持xml和json两种媒体类型
    在这里插入图片描述

    然后我们使用http://localhost:8080/testPathVariable/001/split/decade?format=xml或者http://localhost:8080/testPathVariable/001/split/decade?format=json指定我们需要的返回格式
    在这里插入图片描述
    在这里插入图片描述

    如有错误,欢迎指正!!!

  • 相关阅读:
    HashMap线程不安全问题以及解决方法
    linux中常用的文件处理命令
    idea导入eclipse项目,不能直接open!
    代码随想录day57|647. 回文子串516. 最长回文子序列
    ElasticSearch是什么?有哪些应用?有哪些优缺点?
    如何使用show profile 查看sql的执行周期
    把握好数据要素,才能抓住数字化时代的浪潮
    如果重回大一学软件工程,你会做什么?
    static关键字总结-C/C++
    PartImageNet物体部件分割(Semantic Part Segmentation)数据集介绍
  • 原文地址:https://blog.csdn.net/Decade0712/article/details/126593133