SpringBoot教程27整合SpringMVC之内容协商ContentNegotiation机制

发布时间 2024-01-08 17:49:06作者: l_v_y_forever

SpringBoot教程27整合SpringMVC之内容协商ContentNegotiation机制

SpringBoot2.x系列教程27--整合SpringMVC之内容协商ContentNegotiation机制

原作者:一一哥

一.内容协商ContentNegotiation机制

1.需求概述

我们进行web开发时,现在一般都是设计成RESTful风格的url。如果此时我们希望在请求同一个RESTful的URL时,得到不同的PDF视图、JSON视图、Html视图,也就是说我们需要对同一个url返回多种不同的结果,这该如何实现?

要想实现上面的需求,这就可以用到我今天给大家讲解的内容协商ContentNegotiation机制了!

2.内容协商ContentNegotiation概念

一个URL资源服务端可以以多种形式进行响应:即MIME(MediaType)媒体类型。但对于某一个客户端(浏览器、APP、Excel导出…)来说它只需要一种。此时在客户端和服务端之间就得有一种机制来沟通这个事情,这就是我们要说的内容协商机制。

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

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

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

3.1 ContentNegotiationManager

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

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

3.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)。

3.3 Accept与Content-Type的区别

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

3.4 HTTP协议格式

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

4. Http内容协商的方式

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

  • ①.服务端将可用的MIME类型列表发给客户端,客户端选择某个MIME类型后再告诉服务端。这样服务端就按照客户端告诉它的MIME类型来返回给它具体的内容。(缺点:多一次网络交互,而且使用对使用者要求高,所以此方式一般不用)
  • ②.客户端发请求时就指明需要的MIME类型(比如Http头部的Accept),服务端根据客户端指定的要求返回合适的内容类型,并且在响应头中做出说明(如Content-Type)。如果客户端要求的MIME类型数据服务端提供不了,那就会产生406异常。

5. Spring MVC内容协商方式

Spring MVC在实现了HTTP内容协商的同时,又进行了扩展,它支持4种协商方式:

HTTP的Accept头;
扩展名;
请求参数;
固定类型(producers).

二.Spring MVC内容协商验证

我们创建一个新的web项目,在该项目中验证Spring MVC的内容协商机制。

1. 验证HTTP的Accept头

1.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;
    }

}

1.2 重启项目,进行测试:

直接在浏览器访问该接口

在浏览器中,可以看到一个json结果的数据类型。

在postman中,我们也可以看到一个json结果的数据类型。

1.3 改造项目

我们在web项目中,添加2个如下依赖包:

<dependencies>
    <!-- 此处需要导入databind包即可,jackson-annotations、jackson-core都不需要显示自己的导入了-->
    <dependency>    
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <!--<version>2.9.8</version>-->
    </dependency>
        <!-- jackson默认只会支持的json。若要xml的支持,需要额外导入如下包 -->
        
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <!--<version>2.9.8</version>-->
    </dependency>
</dependencies>

1.4 重启项目,在进行测试

此时在浏览器中,我们可以看到一个xml结果的数据类型。

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

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

1.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格式进行默认展示的。

Spring MVC默认支持HTTP Accept请求头的请求方式。

该方式缺点:

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

2. 验证扩展名

我们在上一个实验的基础上,继续往下验证。

注意:

本实验中,仍然需要添加如下依赖:

<dependencies>
    <!-- 此处需要导入databind包即可, jackson-annotations、jackson-core都不需要显示自己的导入了-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <!--<version>2.9.8</version>-->
    </dependency>
        
    <!-- jackson默认只会支持的json。若要xml的支持,需要额外导入如下包 -->
    <dependency>
         <groupId>com.fasterxml.jackson.dataformat</groupId>
         <artifactId>jackson-dataformat-xml</artifactId>
         <!--<version>2.9.8</version>-->
    </dependency>
</dependencies>

2.1 开启支持扩展名功能

在SpringBoot2.x中,默认情况下是不支持扩展名功能的,所以要想支持扩展名功能,必须开启对该功能的支持。

可以有两种方式开启:

  • 配置类中开启(较麻烦);
  • 配置文件中开启(较简单)。

配置类中开启扩展名功能

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);
    }

}

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

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

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

2.2 重启项目,进行测试

浏览器中输入如下地址:
localhost:8080/show/1.j
可以得到json格式的数据内容。

然后浏览器中再输入如下地址:
localhost:8080/show/1.x
可以得到xml格式的数据内容。

2.3 小结

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

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

注意:

  • 扩展名中的数据类型应该是String/Object类型;
  • 扩展名优先级比Accept要高。

2.4 优缺点

  • 优点:灵活简洁,不受浏览器约束;
  • 缺点:丧失了同一URL的多种展现形式,但是在实际环境中使用较多,因为这更符合程序员的开发习惯。

3. 验证请求参数

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

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

有两种打开方式:

  • 配置类中打开;
  • 配置文件中打开。

3.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);
    }

}

3.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 重启项目,进行测试

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

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

3.4 小结

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

优缺点:

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

4. 验证固定类型(produces)

4.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;
}

4.2 重启项目,进行测试

此时我们在浏览器中,输入地址:
localhost:8080/showMsg
可以看到返回的就是json数据。
即使我们项目中已经导入了jackson的xml包,返回的依旧还是json数据。

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

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

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

Accept为非json格式

format=xml的情况

showMsg.xml的情况

4.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.4 小结

  • 优点: 使用简单,原生支持;
  • 缺点: 让HandlerMethod处理器缺失灵活性。

5. 总结

内容协商对RESTful的url来说还是很重要的,它可以提升用户体验,提升效率和降低维护成本。

优先级:

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

使用选择:

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