ITPub博客

首页 > 数据库 > 数据库开发技术 > 爆破专栏丨SpringBoot2.x系列教程之ContentNegotiation内容协商机制实现对同一请求的不同格式响应

爆破专栏丨SpringBoot2.x系列教程之ContentNegotiation内容协商机制实现对同一请求的不同格式响应

原创 数据库开发技术 作者:Finally_m 时间:2021-11-27 08:03:00 0 删除 编辑

点击上方蓝字关注我们

前言

我们现在进行Web开发时,一般都是遵循RESTful风格来设计URL。那么如果此时,我们希望在请求同一个RESTful的URL接口时,可以得到不同形式的数据,比如PDF格式、JSON格式、HTML格式、XML格式等,也就是说我们希望访问同一个URL接口时,可以返回多种不同形式的数据内容,这该如何实现?

有的小伙伴可能会问,怎么还有这么变态的需求?有的!因为需求总是变态的!

因为现在开发时,后端设计出来的URL接口,可能同时面向Android、iOS、H5等各个方向,他们可能需要相同的数据,但是对数据格式的要求可能不同。所以,需求就变态了。

那,该怎么办呢?别怕,壹哥 告诉你该如何实现!

要想实现上面的需求,请跟着我来学习今天要给大家讲的ContentNegotiation内容协商机制!

一. ContentNegotiation内容协商机制

1. ContentNegotiation内容协商概念

服务端可以把一个URL接口地址,以用多种MIME形式进行响应输出。但对于某一个客户端(浏览器、APP、Excel导出…)来说,它其实只需要一种。此时在客户端和服务端之间就得有一种机制来沟通协调这个事情,看看两者之间到底采用什么形式传递数据最合适,这就是我们要说的ContentNegotiation内容协商机制。

ContentNegotiation内容协商机制,是指客户端和服务器端就输出响应的资源内容进行协商交涉,然后服务端提供给客户端一个最合适的资源。内容协商是以响应资源的语言、字符集、编码方式等作为判断的基准。

这也是RESTful服务中很重要的一个特性是:对同一资源可以有多种形式的表述。

2. 内容协商常见概念简介

2.1 ContentNegotiationManager

ContentNegotiationManager是SpringMVC提供的一个重要工具类,用于判断一个请求的媒体类型MediaType列表。具体的做法是委托给它所维护的一组ContentNegotiationStrategy实例,实际上它自身也实现了接口ContentNegotiationStrategy,使用者可以直接将它作为一个ContentNegotiationStrategy使用。

另外,ContentNegotiationManager也实现了接口MediaTypeFileExtensionResolver,从而可以根据MediaType查找到相应的文件扩展名,这一点也是通过将任务委托给他所维护的一组MediaTypeFileExtensionResolver实例完成的。

2.2 请求头、响应头内容

请求头
  • Accept:告诉服务端,客户端这边需要的MIME类型(一般是多个,比如text/plain,application/json等,/表示可以是任何MIME类型的资源);

  • Accept-Language:告诉服务端,客户端这边需要的语言;

  • Accept-Charset:告诉服务端,客户端这边需要的字符集;

  • Accept-Encoding:告诉服务端,客户端需要的压缩方式(gzip,deflate,br)。

响应头
  • Content-Type:告诉客户端,服务器端响应的媒体类型(如application/json、text/html等);

  • Content-Language:告诉客户端,服务器端响应的语言;

  • Content-Charset:告诉客户端,服务器端响应的字符集;

  • Content-Encoding:告诉客户端,服务器端响应的压缩方式(gzip)。

2.3 Accept与Content-Type的区别

一般来说,Accept属于请求头,Content-Type属于响应头,但这并不完全准确。在前后端分离的请求中,在前端的request请求上大都有Content-Type:application/json;charset=utf-8这个请求头,因此可见Content-Type并不仅仅属于响应头。其实Content-Type指请求消息体的数据格式,因为请求和响应中都可以有消息体,所以它既可以用在请求头中,也可以用在响应头中。

2.4 HTTP协议格式

<request-line>(请求消息行)
<headers>(请求消息头)
<blank line>(请求空白行)
<request-body>(请求消息体)

3. Http内容协商的2种方式

http内容协商的方式大致有两种

  • ①. 服务端将可用的MIME类型列表发给客户端,客户端选择某个MIME类型后再告诉服务端。这样服务端就按照客户端告诉它的MIME类型来返回给它具体的内容。缺点:多一次网络交互,而且使用对使用者要求高,所以此方式一般不用。

  • ②. 客户端发请求时就指明需要的MIME类型(比如Http头部的Accept),服务端根据客户端指定的要求返回合适的内容类型,并且在响应头中做出说明(如Content-Type)。如果客户端要求的MIME类型数据服务端提供不了,那就会产生406异常。

4. SpringMVC内容协商的4种方式

SpringMVC在实现了HTTP内容协商的同时,又进行了自定义的扩展,它支持4种协商方式。

HTTP的Accept头;

扩展名;

请求参数;

固定类型(producers).

接下来我们创建就一个新的Web项目,具体创建过程略,在该项目中利用这个SpringMVC内容协商机制,来实现对HTTP中不同内容格式的协商,我会对HTTP中的4部分内容进行分别讲解。

二. 验证HTTP的Accept头

1. 创建测试接口方法

我们先在Controller中创建一个测试接口方法。

package com.yyg.boot.web;

import com.yyg.boot.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* @Description 内容协商管理
* @Author 一一哥Sun
* @Date Created in 2020/3/21
*/
@Controller
public class NegotiationController {

   @ResponseBody
   @GetMapping(value = "/show")
   public User showUser() {
       User user = new User();
       user.setName("一一哥");
       user.setSex("男");
       user.setDesc("一一哥讲解内容协商机制了...");
       return user;
   }

}

2. 重启项目进行测试

重启项目后,我们在浏览器中访问上面定义的接口,正常情况下可以看到返回了一个JSON格式的数据内容。

在Postman中,我们也可以看到一个JSON格式的响应数据。

3. 改造项目

接下来我们在这个Web项目中,添加2个jackson相关的依赖包。

<dependencies>
   
   <dependency>    
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
       
   </dependency>
       
       
   <dependency>
       <groupId>com.fasterxml.jackson.dataformat</groupId>
       <artifactId>jackson-dataformat-xml</artifactId>
       
   </dependency>
</dependencies>

4. 重启项目进行测试

当再次访问上面的URL接口时,你会在浏览器中发现,输出的响应内容竟然变了,我们得到了一个XML格式的响应数据。

而在Postman中,默认看到的却是一个JSON结果的响应数据,这说明Postman中默认的Accept类型应该是application/json格式。

但是如果我们将Postman中的Accept的值改为application/xml类型,则可以得到一个xml类型的结果。

5. 小结

在本案例中,起初返回的是JSON响应格式,但是在导入jackson-dataformat-xml依赖包后就返回XML格式了,这是因为有了MappingJackson2XmlHttpMessageConverter转换器。

private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

if (jackson2XmlPresent) {
   addPartConverter(new
MappingJackson2XmlHttpMessageConverter());
}

默认情况下,Spring MVC并不支持application/xml这种媒体格式,所以在没有jackson-dataformat-xml依赖包的情况下,协商出来的结果就是:application/json。

默认情况下XML格式的优先级高于JSON格式,但是开发时一般都没有XML包,所以很多时候都是以JSON格式进行默认展示的。

该方式缺点:

  • 由于浏览器的差异,导致发送的Accept Header头可能会不一样,而得到的结果可能不会很好的兼容不同的浏览器。

三. 验证扩展名

我们在上一个实验的基础上,继续往下验证。本实验中,仍然需要添加如下依赖:

<dependencies>
   
   <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
       
   </dependency>
       
   
   <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
       
   </dependency>
</dependencies>

1. 开启支持扩展名功能

在SpringBoot 2.x中,默认情况下是不支持扩展名功能的,所以要想支持扩展名功能,必须开启对该功能的支持。有两种方式可以开启扩展名支持:

  • 配置类中开启(较麻烦);

  • 配置文件中开启(较简单)。

1.1 配置类中开启扩展名功能

开启代码如下:

package com.yyg.boot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
* @Description Description
* @Author 一一哥Sun
* @Date Created in 2020/3/22
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

   @Override
   protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
       //开启支持扩展名功能
       configurer.favorPathExtension(true);
       // .useJaf(false)
       //                .favorParameter(true)
       //                .ignoreAcceptHeader(true)
       //                .defaultContentType(MediaType.APPLICATION_JSON)
       //                .mediaType("json", MediaType.APPLICATION_JSON)
       //                .mediaType("xml", MediaType.APPLICATION_XML);
   }

}

1.2 配置文件中开启扩展名功能

配置代码如下:

#开启支持扩展名功能
#例如访问/test/1.xml则返回xml格式的文件;如访问/test/1.json返回的是json格式数据.
#该方式丧失了同一url多种展现的方式,但现在这种在实际环境中是使用最多的.因为更加符合程序员的审美观.
spring.mvc.contentnegotiation.favor-path-extension=true

两种开启方式我们选择其中的一种开就可以了。

2. 重启项目进行测试

重启项目后,我们在浏览器中输入如下地址:
http://localhost:8080/show/1.json

2.1 JSON格式数据

这时候可以得到JSON格式的数据内容。

2.2 XML格式数据

然后浏览器中再输入如下地址:
http://localhost:8080/show/1.xml

可以得到XML格式的数据内容。

3. 小结

通过扩展名方式,我们就实现了若输入/show/1.json,则返回的是JSON数据;若访问/show/1.xml,则返回的是XML数据,这样就可以实现同一个接口不同内容格式的展现效果。

该方式使用起来非常便捷,并且还不依赖于浏览器。

注意:
  • 扩展名中的数据类型应该是String/Object类型;

  • 扩展名优先级比Accept要高。


4. 优缺点

  • 优点:灵活简洁,不受浏览器约束;

  • 缺点:丧失了同一URL的多种展现形式,但是在实际环境中使用较多,因为这更符合程序员的开发习惯。

四. 验证请求参数

我们继续在上面案例的基础上进行验证。

请求参数的内容协商方式,在SpringMVC中是支持的,但默认情况下是关闭的,需要我们显式的打开。

有两种打开方式:
  • 配置类中打开;

  • 配置文件中打开。

1. 配置类中打开请求参数功能

package com.yyg.boot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
* @Description Description
* @Author 一一哥Sun
* @Date Created in 2020/3/22
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

   @Override
   protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
       //开启支持扩展名功能
       configurer.favorPathExtension(true)
       //开启内容协商的请求参数功能,默认没有开启
       .favorParameter(true);
   }

}

2. 配置文件中打开请求参数功能

#开启支持扩展名功能
#例如访问/test/1.xml则返回xml格式的文件;如访问/test/1.json返回的是json格式数据.
#该方式丧失了同一url多种展现的方式,但现在这种在实际环境中是使用最多的.因为更加符合程序员的审美观.
spring.mvc.contentnegotiation.favor-path-extension=true

#开启内容协商的请求参数功能,默认没有开启
spring.mvc.contentnegotiation.favor-parameter=true

3. 重启项目进行测试

3.1 JSON格式数据

在浏览器中输入如下地址:
http://localhost:8080/show?format=json
可以得到json格式的数据内容。

3.2 XML格式数据

在浏览器中输入如下地址:
http://localhost:8080/show?format=xml
可以得到xml格式的数据内容。

4. 小结

该方式的优先级低于扩展名方式。

优缺点:
  • 优点: 不受浏览器约束;

  • 缺点: 需要传递额外的format参数,URL变得冗余繁琐,缺少了REST的简洁风范,另外还需手动显式开启。

五. 验证固定类型(produces)

1. 编写测试接口

我们继续在上一个案例的基础上,往下进行测试。

@ResponseBody
@GetMapping(value = "/showMsg",produces = MediaType.APPLICATION_JSON_VALUE)
public User showMsg() {
   User user = new User();
   user.setName("一一哥Sun");
   user.setSex("男");
   user.setDesc("一一哥Sun讲解内容协商机制了...,关注我的头条号:一一哥Sun,可以得到更多内容哦!");
   return user;
}

2. 重启项目进行测试

2.1 JSON格式数据

此时我们在浏览器中,输入地址:
http://localhost:8080/showMsg

可以看到返回的就是json数据。
即使我们项目中已经导入了jackson的xml包,返回的依旧还是json数据。

或者输入http://localhost:8080/showMsg.json地址,返回的也是json数据。

或者输入http://localhost:8080/showMsg?format=json地址,返回的也是json数据。

2.2 产生406状态码

但是如果此时,我们将Accept设置成非json格式,或者 format=xml ,后者 showMsg.xml 这些方式,将无法完成内容协商,此时http状态码变为了406!

3. 406状态码原因分析

  • 1.首先我们请求时,解析请求的媒体类型:showMsg.xml请求,解析出来的MediaType是application/xml;

  • 2.然后系统带着这个MediaType(当然还有URL、请求Method等所有)去匹配HandlerMethod的时候会发现producers匹配不上;

  • 3.匹配不上就交给RequestMappingInfoHandlerMapping.handleNoMatch()处理。

  • RequestMappingInfoHandlerMapping源码:

    @Override
    protected HandlerMethod handleNoMatch(...) {
    if (helper.hasConsumesMismatch()) {
    ...
    throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
    }
    // 抛出异常:HttpMediaTypeNotAcceptableException
    if (helper.hasProducesMismatch()) {
    Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
    throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
    }
    }
  • 4.在抛出异常后最终交给DispatcherServlet.processHandlerException()去处理这个异常,转换到Http 406状态码。

  • 5.最终会调用所有的handlerExceptionResolvers来处理这个异常,本处会被DefaultHandlerExceptionResolver最终处理,最终处理代码如下(406状态码):

  • protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
    HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
       response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
    return new ModelAndView();
    }

4. 小结

  • 优点: 使用简单,原生支持;

  • 缺点: 让HandlerMethod处理器缺失灵活性。

结语

内容协商对RESTful的url来说还是很重要的,它可以提升用户体验,提升效率和降低维护成本。最后我把以上4种实现方式总结一下:

优先级:

扩展名 > format请求参数 > HTTP的Accept请求头。

使用选择:

一般情况下,我们为了通用都会使用基于Http的内容协商(Accept),但在实际应用中其实很少用它,因为不同的浏览器可能会采取不同的行为(比如Chrome和Firefox就很不一样),所以为了保证“稳定性”一般都选择使用 扩展名 或 format请求头 或 固定类型(produces)方式。

今天的内容比较多,各位可以慢慢看,不要退缩哦。


完成任务在留言区回复:666

第一个回复666并被精选的同学可以领取一本台历哦

点击【阅读全文】,锋迷商城项目免费学

原文作者:Java架构栈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-04-13

  • 博文量
    573
  • 访问量
    243793