作者:一一哥
我们进行web开发时,现在一般都是设计成RESTful风格的url。如果此时我们希望在请求同一个RESTful的URL时,得到不同的PDF视图、JSON视图、Html视图,也就是说我们需要对同一个url返回多种不同的结果,这该如何实现?
要想实现上面的需求,这就可以用到我今天给大家讲解的内容协商ContentNegotiation机制了!
一个URL资源服务端可以以多种形式进行响应:即MIME(MediaType)媒体类型。但对于某一个客户端(浏览器、APP、Excel导出…)来说它只需要一种。此时在客户端和服务端之间就得有一种机制来沟通这个事情,这就是我们要说的内容协商机制。
内容协商机制是指客户端和服务器端就响应的资源内容进行协商交涉,然后提供给客户端最为合适的资源。内容协商是以响应资源的语言、字符集、编码方式等作为判断的基准。
这也是RESTful服务中很重要的一个特性是:对同一资源可以有多种形式的表述。
ContentNegotiationManager是Spring Web提供的一个重要工具类,用于判断一个请求的媒体类型MediaType列表。具体的做法是委托给它所维护的一组ContentNegotiationStrategy实例。实际上它自身也实现了接口ContentNegotiationStrategy,使用者可以直接将它作为一个ContentNegotiationStrategy使用。
另外,ContentNegotiationManager也实现了接口MediaTypeFileExtensionResolver,从而可以根据MediaType查找到相应的文件扩展名。这一点也是通过将任务委托给他所维护的一组MediaTypeFileExtensionResolver实例完成的。
一般来说,Accept属于请求头,Content-Type属于响应头,但这并不完全准确。在前后端分离的请求中,在前端的request请求上大都有Content-Type:application/json;charset=utf-8这个请求头,因此可见Content-Type并不仅仅属于响应头。其实Content-Type指请求消息体的数据格式,因为请求和响应中都可以有消息体,所以它既可以用在请求头中,也可以用在响应头中。
<request-line>(请求消息行)
<headers>(请求消息头)
<blank line>(请求空白行)
<request-body>(请求消息体)
Spring MVC在实现了HTTP内容协商的同时,又进行了扩展,它支持4种协商方式:
HTTP的Accept头;
扩展名;
请求参数;
固定类型(producers).
我们创建一个新的web项目,在该项目中验证Spring MVC的内容协商机制。
我们在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;
}
}
在浏览器中,可以看到一个json结果的数据类型。
在postman中,我们也可以看到一个json结果的数据类型。
我们在web项目中,添加2个如下依赖包:
com.fasterxml.jackson.core
jackson-databind
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
此时在浏览器中,我们可以看到一个xml结果的数据类型。
在postman中,默认时看到的却是一个json结果的数据类型,这说明postman中默认的Accept类型应该是application/json格式。
但是如果我们将postman中的Accept
的值改为application/xml类型,则可以得到一个xml类型的结果。

在本案例中,起初返回的是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请求头的请求方式。
该方式缺点:
我们在上一个实验的基础上,继续往下验证。
本实验中,仍然需要添加如下依赖:
com.fasterxml.jackson.core
jackson-databind
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
在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
两种方式我们选择一种开启就可以了。
浏览器中输入如下地址:
http://localhost:8080/show/1.json
可以得到json格式的数据内容。
然后浏览器中再输入如下地址:
http://localhost:8080/show/1.xml
可以得到xml格式的数据内容。
通过扩展名方式,我们就实现了若输入/show/1.json,则返回的是json数据,若访问/show/1.xml,则返回的是xml数据,这样就可以实现同一个接口不同内容格式的展现效果。
该方式使用起来非常便捷,并且还不依赖于浏览器。
我们继续在上面案例的基础上进行验证。
请求参数的内容协商方式,在Spring MVC是支持的,但默认情况下是关闭的,需要我们显式的打开。
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);
}
}
#开启支持扩展名功能
#例如访问/test/1.xml则返回xml格式的文件;如访问/test/1.json返回的是json格式数据.
#该方式丧失了同一url多种展现的方式,但现在这种在实际环境中是使用最多的.因为更加符合程序员的审美观.
spring.mvc.contentnegotiation.favor-path-extension=true
#开启内容协商的请求参数功能,默认没有开启
spring.mvc.contentnegotiation.favor-parameter=true
在浏览器中输入如下地址:
http://localhost:8080/show?format=json
可以得到json格式的数据内容。
在浏览器中输入如下地址:
http://localhost:8080/show?format=xml
可以得到xml格式的数据内容。
该方式的优先级低于扩展名方式。
我们继续在上一个案例的基础上,往下进行测试。
@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;
}
此时我们在浏览器中,输入地址:
http://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的情况
RequestMappingInfoHandlerMapping源码:
@Override
protected HandlerMethod handleNoMatch(...) {
if (helper.hasConsumesMismatch()) {
...
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
// 抛出异常:HttpMediaTypeNotAcceptableException
if (helper.hasProducesMismatch()) {
Set mediaTypes = helper.getProducibleMediaTypes();
throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
}
}
protected ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
return new ModelAndView();
}
内容协商对RESTful的url来说还是很重要的,它可以提升用户体验,提升效率和降低维护成本。
扩展名 > format请求参数 > HTTP的Accept请求头。
一般情况下,我们为了通用都会使用基于Http的内容协商(Accept),但在实际应用中其实很少用它,因为不同的浏览器可能会采取不同的行为(比如Chrome和Firefox就很不一样),所以为了保证“稳定性”一般都选择使用 扩展名 或 format请求头 或 固定类型(produces)方式。