spring boot2 (五)web中数据返回原理及内容协商

上一章简单的说了下spring boot接收请求数据,并且用解析器解析成想要的类型。包括一下复杂的包装类。然后这章主要是说返回数据的封装和类型转换,已经http内容协商的原理。

@ResponseBody

说到数据返回,这个注解几乎必不可少。无论是之前spring mvc的时候,这个注解用来返回json数据,还是现在的spring boot中@ResponseBody和@Controller合二为一的@RestController,反正都用到了这个注解。


@RestController注解

这个注解的意思就是可以将返回值以json字符串的形式返回。并且如果在类上标注,那么作用域是这个类中所有的方法。所以不要觉得现在spring boot没有用这个注解了,大多数时候这个注解已经是必要且必须的了。咱们先探究一下这个注解的原理。

咱们根据上文的请求参数的解析,其实就能猜到这个响应参数的解析。大同小异,也是spring boot配置好了一堆解析器,然后根据不同的类型去转化。而这个@ResponseBody注解就是其中一个解析器,其作用就是将数据转化成json。其大概的过程如下:

  • 返回值处理器判断是否支持这种类型返回值 supportsReturnType
  • 返回值处理器调用 handleReturnValue 进行处理
  • RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
    • 利用 MessageConverters 进行处理 将数据写为json
    • 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
    • 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
    • SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
      • 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。

下面我们去看看源码:
首先我上面就说了这个是类似于请求处理器一样,是有一堆的,下面我们看看这一堆:

选择返回值的解析器

具体可选的解析器

可选项默认有15个,其实这里可以看名字就大概知道哪些肯定不可能了,咱们这里我直接剧透用的第12个:RequestResponseBodyMethodProcessor
选择处理器后继续走逻辑(如果没有合适的处理器会报错,上面截图中已经说了)。进入处理的方法:
主要的处理方法

走到这步了:
writeWithMessageConverters方法

其实逻辑也不是很复杂:
将返回值转化成字符串

单纯的转成字符串逻辑其实不复杂,但是复杂的这里涉及到了内容协商(大概意思就是请求的时候就要告诉服务器要什么样的数据,所以返回的时候也要与之对应。)正常不设置的情况下是都要的,比如我这个demo中的:
请求中要求的返回类型

这个最后一个/的意思就是啥都行,也就是说这个返回啥都可以(默认一般就是这样)。
所以说咱们这个返回json是可以的,以下又是一系列操作,几乎没啥了,就是在这里利用转换器转换成想要的数据返回的。
这里数据类型是要看双方能不能匹配上,但是上面的实现还使用了一个重要的类,就是这个:HttpMessageConverter
这个类的作用简而言之一句话:将Class类型的对象转成MediaType类型的http支持数据。
messageConverter支持类型

比如返回值是int和返回值是布尔型还有字符串型,肯定是有不一样的转化方式的,所以这又是一堆转化的类。这个就不咋见名知意的,除了第一个是字节码类型,还有字符串类型,剩下看不好,我就不一一点进去了,反正每一个功能肯定不一样,再直接剧透这里用下标为7的那个处理器:
MappingJackson2HttpMessageConverter判断源码

代码逻辑上,只要这个类能被序列化,就可以使用这个处理器。所以继续往下走。
最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
至此这个@ResponseBody标注的方法的返回值就返回结束了。
(ps:如果这里返回的类型不同会使用不同的处理器,我上面只说了返回json数据的。比如返回资源的话会用资源处理器。)

内容协商

其实这个上文简单的介绍过了,在发送请求的时候,我们可以设置想要返回数据类型。比如我们之前说过的报错,在接口请求中是json,在页面请求种是xml。这个就是内容协商的作用。
这个想要接收的数据类型是在消息头中设置的(默认使用基于请求头的策略)。


想要接收的数据

前面是可以接收的类型,后面的小数是权重。比如图中前面的几个json/xml都是0.9.而后面imgs和/等都是0.8.
当然了这块我们也可以手动去设置,只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
这里在我们上面走代码的时候跑过了,我们直接总结以下:

  1. 获取可以处理当前返回值的所有返回值类型(结果集是list)。
  2. 获取服务器所要的所有返回值类型(结果集是list)。
  3. 双层for循环找到两者匹配的。如下图


    双层for循环找到最终使用类型

    这里有一点比较好的是客户端类型优先。而且可能我们会发现有多种处理办法都可以实现这个功能,但是这里是当遇到一个可以处理的就break。


    使用第一个结果,后面不管了

    其实这个设计的挺巧妙,也挺很简单的。就是要的和能给的,选择最佳匹配的那个。只要知道原理就很容易理解啦。而且一般浏览器按照权重,都会优先html等,所以error页面会出现网页和接口测试工具出现的东西不一样。

上面说了好多都是基于请求头设置的接收方法,但是有一个问题!浏览器的请求头轻易不好改,所以这里还有一种策略:基于format参数的内容协商。
想要实现这个只要一个配置:

spring:
    contentnegotiation:
      favor-parameter: true  #开启请求参数内容协商模式

这个配置就是开启请求参数内容协商模式。并且参数的内容协商策略是优先于消息头的内容协商策略。
大致流程开启请求参数内容协商模式以后,先获取请求参数中的值进行内容协商返回给客户端json即可。当然了默认这个参数格式只支持xml和json。就是这两个字面量。别的都不好使。

strategies中参数优先于消息头

如果参数中是/,那么还会继续去匹配消息头中的。否则就不管消息头中的了。

自定义返回值转换类型

其实这个和解析器差不多,一般我们都可以自定义。而我们只要找到之前spring boot自带的一堆解析器是从哪里来的,就比较容易把自己写好的加进去了。这里我直接说spring boot加载默认配置的地方:


这个类

其实名字也比较容易理解,然后我们进去看看这个类,这个类几乎是webmvc中好多默认提供的解析器的汇集了。包括请求解析器,参数解析器,和现在我们要找的返回值解析器等。我们直接说返回值解析器这块:


添加默认解析器

当然了这个类也有一些小细节:比如是存在jacksonXml依赖,才会添加xml的解析器。这里就顺便提一下,继续说自己定制东西在spring boot中统一的方式:给容器中添加WebMvcConfigurer里面放入自己的配置。如下操作:
  1. 写一个自己的解析器(这里其实代码比较简单,但是因为有很多方法,所以我直接贴代码以便复制粘贴)
/**
 * 自定义的converter
 * @author 11511
 *
 */
public class MyMessageConverter implements HttpMessageConverter<SysUser>{

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        // TODO Auto-generated method stub
        return false;
    }
     
    //可以读什么类型
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        // TODO Auto-generated method stub
        return clazz.isAssignableFrom(SysUser.class);
    }
    
    //服务器统计可以处理的内容协商的类型
    @Override
    public List<MediaType> getSupportedMediaTypes() {
        // TODO Auto-generated method stub
        return MediaType.parseMediaTypes("application/x");
    }

    @Override
    public SysUser read(Class<? extends SysUser> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void write(SysUser t, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
         //接收参数后,返回如下数据,这样能明确测出是不是走了这个解析器
        String data = t.getName()+",自定义格式启动成功!";
        outputMessage.getBody().write(data.getBytes());
        
    }

}

这个类的方法都挺明显的,毕竟这个converter可读可写,咱们这是为了测试写,所以read方法都不懂,注意我这里能解析的消息头我设置的application/x。这个是我自己编的,正常不存在这个格式。剩下就没啥了。

  1. 自己写webMvcConfigurer并重写这个方法:


    WebMvcConfigurer可重写方法

    把我们自己的解析器添加进去

    然后这一块的东西最好的一点就是见名知意。我们是要往httpMessageConverter中添加自定义的东西,所以重写这个方法,其实这里可以举一反三,如果添加别的也是差不多的方式。

这两步做好以后,我们可以去测试下这个转换器是不是生效了(因为这个格式浏览器不好设置,所以我用postman测试)


测试成功!

网页和postman结果不同

再放个对比图:一样的接口一样的参数,就是内容协商中的类型不同就返回了不同的结果。所以说这块的逻辑没问题,和我们学到了和理解的都一样。
而且事实证明往spring 中添加一些自定义的东西只要知道流程还是很简单的。毕竟它的扩展性是很好的,咱们再说一个小问题:
上文已经说了正常是参数内容协商虽然优先级高,但是只支持xml和json。那么想要实现自定义的怎么办?
简单来说,去用webMvcConfigurer添加自定义配置。这个思路绝对没问题。然后我们要去webMvcConfigurer中找去重写哪个方法:


直接剧透,是这个方法

见名知意

咳咳,反正英语不好的我是用的百度翻译。。然后名字这么明显了所以重写这个方法就得了:
下面是自定的方法(ps:这里为了复制粘贴方法所以贴代码):
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            
            

            @Override
            public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                converters.add(new MyMessageConverter());
                WebMvcConfigurer.super.configureMessageConverters(converters);
            }

            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                Map<String,MediaType> map = new HashMap<String,MediaType>();
                map.put("lsj", MediaType.parseMediaType("application/x"));
                map.put("json", MediaType.APPLICATION_JSON);
                List<ContentNegotiationStrategy> list = new ArrayList<ContentNegotiationStrategy>();
                list.add(new ParameterContentNegotiationStrategy(map));
                configurer.strategies(list);
                WebMvcConfigurer.super.configureContentNegotiation(configurer);
            }

        };
    }

这里因为各种数据类型的不互通,而且比如参数的list,参数是map之类的,所以要一层一层找代码。但是过程还是很简单也很有意思,建议自己写出来,也算是熟悉的过程。
配置完成以后我们访问一下:


测试结果

虽然乱码了但是是因为浏览器的编码问题,反正能看出来是名字+一段话,所以说明测试成功!

至此关于内容协商的东西简单的就说完了,其实我感觉这章最好的内容是如何自定义配置spring的一些配置,当然了要注意当自定义配置以后有可能会覆盖spring原本的配置,这里要慎重使用。

本篇文章就记录到这里,如果稍微帮到你记得点个喜欢点个关注,也祝大家工作顺顺利利!

推荐阅读更多精彩内容