SpringBoot07——Web02数据响应与内容协商的源码分析

什么是数据响应?

简单来说就是我们的请求处理结束了,现在我们需要把处理好的数据响应到想去的页面上

 给页面进行json数据的响应

想要响应json数据,我们一定要引入json相关的依赖 

但是我们web的场景启动器已经帮助我们引用好了

而且我们上面加上@ResponseBody注解就可以了

进行测试

@Controller
public class ResponseTestController {

    
    @ResponseBody
    @GetMapping("/test/person")
    public Person getPerson(){
        Person person = new Person();
        person.setUserName("LALALA");
        person.setAge(22);
        person.setBirth(new Date());
        return person;
    }

}

那我们以后的开发就方便很多了,可以自动给前端返回json数据,但是为什么会这样

源码对于数据响应处理的原理

 首先一定还是从doDispatch开始

 一直向下走,在doDispatch的mv = ha.handle(目标handler的目标方法,req,resp)的handelInternal方法的mav = this.invokeHandlerMethod(request, response, handlerMethod);的invokeHandlerMethod方法中,做出了把参数解析器和返回值解析器,封装到了invocableMethod里面

现在根据请求处理 得到了我们需要的返回值,就在returnValue里面

 现在我们得到了返回值,直到这一句

 里面的getReturnValue.(返回值),对我们的返回值进行了处理,得到了我们现在的返回值是一个person类型的对象 ,说明对于我们的返回值已经被处理了,我们需要下一步查看是如何处理返回值的

getReturnValue.(返回值)如何找到对应的参数解析器?

在执行改行代码的时候,会把返回值(person对象)和返回值类型(person类型),拿出来寻找哪个返回值处理器能够处理,但是他是怎么寻找的?

 如何寻找匹配的返回值处理器?

先使判断是不是需要一个异步的返回值

 如何判断呢?判断基准就是使用for循环和返回值处理器returnValueHandlers进行类型匹配,其中的内容

 

其实按照我们的判断基准,现在应该都不是异步的

进入for循环,判断当前需要的返回类型,我们的返回值处理器是否支持

返回值处理器的本质其实也是一个接口,里面的方法有两个

 现在我们的for循环内就是调用了supports方法检查是否支持

其实如何判断也很简单,我们传入了我们的返回值类型returnType(person类型),是不是和当前循环到的返回值处理器类型一样,如果不一样就下一个

找到了requestresponeBody返回值解析器

 我们在这次判断的时候成功进来了

 里面的这个方法语句意思是,我们现在这个方法使用Annotated工具类,判断当前这个返回值的上面有没有标注responseBody注解——我们标注了

最后进行上级返回

 现在找到了,就是这个 

确定了使用哪个参数解析器进行解析

使用参数解析器进行详细解析

使用消息转换器进行写出操作,利用 MessageConverters 进行处理 将数据写为json 

利用 MessageConverters 进行处理 将数据写为json 

先判断返回值类型是不是字符串

把返回值,返回值类型,需要转换成的类型拿过来封装

判断是不是资源类型

 如何判断?

看看返回值是不是如下类型 

再继续向下执行,直到看到了mediatype

mediatype内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)

请求头里要求的一般内容如下

注意:我们里面有一个*/*就是要是实在没有其他的类型,你给啥类型我就要啥类型

q=0.9/0.8是权重的意思

 在源码中会拿到响应头,看看里面有没有东西,可能我们这个响应已经被前面处理了一般,所以会拿到

如果里面有东西,就用这个处理了一半的

如果没有就正常else继续执行

第一行浏览器告诉服务器自己能接收什么样的类型(之前在请求头中)

第二行浏览器自己看看能响应什么样的类型数据

 

 

 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,

之后把能提供的和能接收的进行for循环匹配,最后注入到mediaTypesTouse中

 现在执行结束了知道我们可以给浏览器写json

 再向下执行

SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter(消息转换器,把person写成json) ,看谁能处理?

 所有的HttpMessageConverter

HttpMessageConverter到底是个啥?——是一个接口

继续代码,判断当前循环的消息转换器支不支持我们当前浏览器需要的类型和响应类型的写操作

如何判断,深入代码(以循环到0号消息转换器为例子)

 

我们看到0号只是支持Byte类型的返回值,所以不可以,下一个转换器

 但是我们执行到7号的时候,接口都不用调用,直接返回true

找到了,说明现在可以写

看person类型的对象能不能转换为json

 

 

深入这个方法

调用canWrite方法看看到底把参数传进来能不能写(之前那个canWrite是找数据转换器)

看看我们现在的转换器到底支持什么类型

 

 是支持json的

看json数据是不是我们想要的媒体数据,是的,返回true

 回到

执行结束后再返回到

现在我们的if相当于判断成功了

 向下走,拿到我们想要的内容(person对象封装为body)

使用消息转换器的写方法

进入

 新建一个什么都没有的响应头

给这个响应头添加默认数据(也就是响应格式为json) 

 把type=person,写给outMess=响应

进入

 把person写入

 flush写出去

回到

写给响应头

现在我们的响应头

 总结

 内容协商原理

内容协商是指,通过遍历所有MessageConverent(消息转换器),找到一个适合处理当前媒体数据的消息转换器

内容协商的作用

如果现在有这种情况,我们有两个客户端,我们的网页客户端想接收json,我们的安卓想接收xml 

首先我们引入返回的是xml的依赖

 再次发出测试,已经可以了,但是我们达不到需求

因为在网页我们xml的权重是比我们之前json格式的*/*要高的

我们可以用postman自己设定请求头

使用PostMan测试

自己修改请求头

XML:

 格式正确

JSON:

 json也可以

我们什么都没有做,但是两种都可以,说明底层已经封装好了

内容协商的原理

以请求头为xml为例子

之前的步骤都一样,知道获取到资源媒体的类型

 判断当前响应头中是否已经有确定的媒体类型。MediaType

我们现在是第一次请求,一定没有,继续向下

 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)

如何获取的?我们不用跳转接口,直接下一步

我们看到使用内容协商管理器解析媒体的类型

调用了这个方法跳转接口

解析媒体的类型 

 如何解析的,我们跳转到下一个接口

其实很简单,就是使用原生的request请求获取了请求头的ACCEPT字段

 这就是获取的内容

 向上级返回,回到获取好了媒体资源

向下执行代码,现在我们知道这个里面为什么存放着我们能接收的客户端响应类型了 

继续向下执行,得到当前我们可以针对现在的媒体类型(返回的对象person)支持以什么样的数据类型写出

 执行该方法,转换接口

 其实就是和之前一样,会到当前返回的数据类型和所有的数据处理器进行比对,看看哪个处理器可以对person对象进行写出处理

之前我们有10个处理器,由于我们导入了xml的处理器依赖,现在有11个

其实在这边,我们的7到10号

7和8可以处理json,9和10可以处理xml

 继续向下代码,如果某个处理器支持,就把他放入支持的媒体类型集合中

 执行完毕之后(里面就是我们7到10号处理器可以支持的东西),我们支持的媒体类型集合里面有

 所以可以对我们刚刚的操作做一个小总结

遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
找到支持操作Person的converter,把converter支持的媒体类型统计出来。

向上返回,回到获取完毕客户端需要的和服务器能提供的

 目前的状况:
客户端需要【application/xml】。服务端能力【10种、json、xml】

继续向下执行代码

循环寻找客户端需要的和服务器能提供的有没有可以相互匹配的

把可以的放入mediaTypesTouse(可以被使用的媒体类型)

 

 继续向下执行代码,如果mediaTypesTouse有我们可以用的就直接break跳出(两个中确定一个就可以)

向下执行代码,再次使用返回值处理器,看哪个处理器真正的有能力把我们的person写成xml(之前那一次只是为了统计每个处理器有什么能力)

通过for循环,找到了可以使用的处理器

 之后在向下执行代码,就是找到了要真正的去写数据了,就和之前的流程相同了

总结

基于请求参数的内容协商管理

基于请求参数的内容协商管理是个什么?

我们之前使用postman进行测试,是手动改变了请求头,但是如果我们使用纯网页的话,请求头是不可能进行修改的,但是如果我们想要在网页上显示json或者xml的数据是我们自己想要规定的怎么办?

我们可以在yaml文件里进行配置

如果请求路径里面带了format,我们可以在format后面写自己想接收什么样的响应数据

 测试:

 非常的方便

 请求参数的内容协商管理的原理(以json为例子)

还是要看源码,再次回到内容协商即将开始的时候

在我们获取确定客户端想接收的数据类型 的时候

之前我们会调用内容协商管理器,里面会有一个属性叫做策略 

 这是之前的里面只有一个请求头的策略

 但是现在变成了两个策略(因为我们开启了参数内容协商),多出来的那一个就是基于参数的内容协商策略 

 而且这个内容协商策略已经写好了返回的是json或者xml数据类型

调用当前方法,进入下一个接口

 遍历所有的内容协商策略

 找到了参数内容协商的策略,执行对应方法的接口

 

 获取当前参数的名称(其实就是获取format,是在参数的内容协商管理器里面规定的) 

 

带着上一步获取的format,从请求中获取请求参数

 

 本质上就是获取名称为format的请求参数,相当于通过format获取到了json(我们传入的请求参数)

向上返回到循环执行策略的代码

通过策略我们得知了此时客户端需要的类型是json

再次向上返回,把json封装到浏览器能接收的媒体类型(acceptableTypes)中

 

 之后的流程就都一样了

自定义MessageConverter(消息转换器)

扩展知识:为什么我们会有这么多默认的消息转换器呢?

 我们发现springboot是给我们默认配置好的

 从一个地方获取好直接加进去的

深入看看

 

 converters其实是一个属性,在创建构造器的时候就被创建和添加了

 会添加默认的converters,赋值给属性converters

 默认的converters又是从哪里来?

 找到了

在源码,只要判断jackson2为真就会创建这个的converent

 如何判断为真?

只要看到系统里导入了这个类就可以了 

我们现在的需求:

 如果使用第三种,希望把person内的属性用分号隔开

步骤:

实现:

我们之前就说过,我们如果想自己定制springMVC的功能,我们就给配置类自己加组件

在我们的这个接口中

就已经给我们定义好了两种

配置消息转换器(把默认的都消除)

 扩展消息转换器(默认的不动,只做额外扩展)

 我们需要扩展就好了

添加组件

 书写消息转换器

 首先我们肯定不支持读(读就是我们传入了一个person数据,我们不关心,我们只关心把person写到客户端上)

我们肯定关心写

@Override
    public boolean canWrite(Class<?> aClass, MediaType mediaType) {
        //只要当前处理的类是Person,就可以对它进行写操作
        return aClass.isAssignableFrom(Person.class);
    }

我们还关心是否支持当前的媒体类型

//服务器要统计哪种converter能够处理当前传入的媒体类型
    //也就是我们自定义的application/x-LALALA
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        //调用方法说明要支持请求头中带有application/x-LALALA的媒体类型集合
        return MediaType.parseMediaTypes("application/x-LALALA");
    }

当然,前面的工作只是服务器第一次统计哪种转换器适合当前的媒体类型,我们之后还要用转换器完成真正的写

@Override
    public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        //自定义类型的写出
        String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();

        //找到一个数据输出流
        OutputStream body = httpOutputMessage.getBody();
        //把我们的数据放入body
        body.write(data.getBytes());
    }

最后在我们的组件中加入我们写的转换器

测试:

 

 原理流程:


之前的流程都是一样,但是在获取客户端能接收的内容类型的时候

 它通过拿到请求头里面的东西知道了我们要的是x-LALALA这个媒体类型

 统计服务器能够产出的时候,现在的转换器里面就有我们自己的了(第一次使用转换器

 

 成功判断转换器,也就是我们的这行代码

 

 然后再调用我们自己写的getSupport方法

 返回的服务器能处理的转换器

for循环判断那种转换器能够成功转换我们需要的媒体类型(第二次使用转换器)

找到了我们自己的之后,会调用我们自己写的write方法

 

 这就是所有的流程了

但是现在还是有个问题,我们是用postman进行测试的,但是网页修改不了请求头,可不可以我们在请求路径上写什么就给我们什么呢?类似于完成自定义路径的内容协商

浏览器与PostMan内容协商的完全适配

需求:

现在我们想要传入format=ll就给我响应自定义的响应类型

 分析:

我们判断客户端需要什么类型的媒体类型,无非就两种策略

一种是获取请求头中的ACCEPT,这种方法只适合上一种需区。

还有一种就是通过使用参数策略,但是我们的参数里面现在只能支持两种媒体类型

 那么唯一合适的办法就是我们自己定义策略组件了

很巧合的是,springMVC里面还真有配置内容协商功能的接口

实现


在配置类中 添加组件

 我们发现使用configurer可以自己创建策略

现在系统中有一个参数的,一个请求头的

我们加上自己的参数方法

 但是创建的时候爆红了

我们点进来发现要求传入一个map类型的k为string,v为媒体数据类型

也就是之前我们使用参数方式使用的,参数的xml对应application/xml,参数的json对应application/json,参数的ll对应application/x-LALALA,所以我们还要创建一个Map类型用于存放对应关系

 

完整的代码

@Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                //自定义内容协商策略的对应关系
                Map<String, MediaType> mediaTypeMaps = new HashMap<>();
                mediaTypeMaps.put("json",MediaType.APPLICATION_JSON);
                mediaTypeMaps.put("XML",MediaType.APPLICATION_XML);
                mediaTypeMaps.put("ll",MediaType.parseMediaType("application/x-LALALA"));
                //把对应关系放到新的参数策略当中
                ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypeMaps);
                //添加新的内容协商策略
                configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy));
            }

测试

原理

先到内容协商的地方

 获取客户端需要的媒体类型

 找策略,看到我们添加的参数策略已经在里面了

 

 向上返回,发现现在客户端需要的媒体类型就是我们自己定义的

 继续执行代码,在我们服务器可提供的消息转换器中就有处理这种类型的(之前我们自己定义的)转换器

 之后把需要的和提供的进行适配

寻找使用哪个适配器进行真正的写给浏览器

之后的步骤就都一样了

存在的问题(以请求头发送json为例子)

我们现在只写了针对于参数的策略,那我们请求头的策略还可以用吗?

不可以

来到获取浏览器需要的媒体类型

由于现在的内容协商管理器里面只有针对于参数进行内容管理的,所以最终的解析结果应该解析不出来(因为只有在请求头里会获取ACCEPT的值

 所以最终选取的客户端需要的媒体类型的结果是

 而服务器筛选出来的结果对于*/*都可以使用

 所以导致的结果就是无论请求头里面要求什么,我们的源码都只会找第一个服务器能生成对应媒体数据的转换器进行适配(也就i是json),即使发出xml请求,也是返回的json类型

怎么样能让我们自定义的参数的和请求头的策略同时生效?

根本原因还是我们自定义了策略,但是只是自定义了参数的策略,我们把请求头的策略也放进去就可以了

@Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                //自定义内容协商策略的对应关系
                Map<String, MediaType> mediaTypeMaps = new HashMap<>();
                mediaTypeMaps.put("json",MediaType.APPLICATION_JSON);
                mediaTypeMaps.put("XML",MediaType.APPLICATION_XML);
                mediaTypeMaps.put("ll",MediaType.parseMediaType("application/x-LALALA"));
                //把对应关系放到新的参数策略当中
                ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypeMaps);
                //创建请求头的策略
                HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy();
                //添加新的内容(新的参数策略和原本的请求头策略)协商策略
                configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy,headerContentNegotiationStrategy));
            }

现在一切测试都可以成功了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值