点击上方蓝字关注我们
我们现在进行Web开发时,一般都是遵循RESTful风格来设计URL。那么如果此时,我们希望在请求同一个RESTful的URL接口时,可以得到不同形式的数据,比如PDF格式、JSON格式、HTML格式、XML格式等,也就是说我们希望访问同一个URL接口时,可以返回多种不同形式的数据内容,这该如何实现?
有的小伙伴可能会问,怎么还有这么变态的需求?有的!因为需求总是变态的!
因为现在开发时,后端设计出来的URL接口,可能同时面向Android、iOS、H5等各个方向,他们可能需要相同的数据,但是对数据格式的要求可能不同。所以,需求就变态了。
那,该怎么办呢?别怕,壹哥 告诉你该如何实现!
要想实现上面的需求,请跟着我来学习今天要给大家讲的ContentNegotiation内容协商机制!
服务端可以把一个URL接口地址,以用多种MIME形式进行响应输出。但对于某一个客户端(浏览器、APP、Excel导出…)来说,它其实只需要一种。此时在客户端和服务端之间就得有一种机制来沟通协调这个事情,看看两者之间到底采用什么形式传递数据最合适,这就是我们要说的ContentNegotiation内容协商机制。
ContentNegotiation内容协商机制,是指客户端和服务器端就输出响应的资源内容进行协商交涉,然后服务端提供给客户端一个最合适的资源。内容协商是以响应资源的语言、字符集、编码方式等作为判断的基准。
这也是RESTful服务中很重要的一个特性是:对同一资源可以有多种形式的表述。
ContentNegotiationManager是SpringMVC提供的一个重要工具类,用于判断一个请求的媒体类型MediaType列表。具体的做法是委托给它所维护的一组ContentNegotiationStrategy实例,实际上它自身也实现了接口ContentNegotiationStrategy,使用者可以直接将它作为一个ContentNegotiationStrategy使用。
另外,ContentNegotiationManager也实现了接口MediaTypeFileExtensionResolver,从而可以根据MediaType查找到相应的文件扩展名,这一点也是通过将任务委托给他所维护的一组MediaTypeFileExtensionResolver实例完成的。
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)。
一般来说,Accept属于请求头,Content-Type属于响应头,但这并不完全准确。在前后端分离的请求中,在前端的request请求上大都有Content-Type:application/json;charset=utf-8这个请求头,因此可见Content-Type并不仅仅属于响应头。其实Content-Type指请求消息体的数据格式,因为请求和响应中都可以有消息体,所以它既可以用在请求头中,也可以用在响应头中。
<request-line>(请求消息行)
<headers>(请求消息头)
<blank line>(请求空白行)
<request-body>(请求消息体)①. 服务端将可用的MIME类型列表发给客户端,客户端选择某个MIME类型后再告诉服务端。这样服务端就按照客户端告诉它的MIME类型来返回给它具体的内容。缺点:多一次网络交互,而且使用对使用者要求高,所以此方式一般不用。
②. 客户端发请求时就指明需要的MIME类型(比如Http头部的Accept),服务端根据客户端指定的要求返回合适的内容类型,并且在响应头中做出说明(如Content-Type)。如果客户端要求的MIME类型数据服务端提供不了,那就会产生406异常。
SpringMVC在实现了HTTP内容协商的同时,又进行了自定义的扩展,它支持4种协商方式。
HTTP的Accept头;
扩展名;
请求参数;
固定类型(producers).接下来我们创建就一个新的Web项目,具体创建过程略,在该项目中利用这个SpringMVC内容协商机制,来实现对HTTP中不同内容格式的协商,我会对HTTP中的4部分内容进行分别讲解。
我们先在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个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>当再次访问上面的URL接口时,你会在浏览器中发现,输出的响应内容竟然变了,我们得到了一个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格式进行默认展示的。
该方式缺点:
由于浏览器的差异,导致发送的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>在SpringBoot 2.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数据,这样就可以实现同一个接口不同内容格式的展现效果。
该方式使用起来非常便捷,并且还不依赖于浏览器。
扩展名中的数据类型应该是String/Object类型;
扩展名优先级比Accept要高。

优点:灵活简洁,不受浏览器约束;
缺点:丧失了同一URL的多种展现形式,但是在实际环境中使用较多,因为这更符合程序员的开发习惯。
我们继续在上面案例的基础上进行验证。
请求参数的内容协商方式,在SpringMVC中是支持的,但默认情况下是关闭的,需要我们显式的打开。
配置类中打开;
配置文件中打开。
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格式的数据内容。
该方式的优先级低于扩展名方式。
优点: 不受浏览器约束;
缺点: 需要传递额外的format参数,URL变得冗余繁琐,缺少了REST的简洁风范,另外还需手动显式开启。
我们继续在上一个案例的基础上,往下进行测试。
@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!

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();
}优点: 使用简单,原生支持;
缺点: 让HandlerMethod处理器缺失灵活性。
内容协商对RESTful的url来说还是很重要的,它可以提升用户体验,提升效率和降低维护成本。最后我把以上4种实现方式总结一下:
扩展名 > format请求参数 > HTTP的Accept请求头。
一般情况下,我们为了通用都会使用基于Http的内容协商(Accept),但在实际应用中其实很少用它,因为不同的浏览器可能会采取不同的行为(比如Chrome和Firefox就很不一样),所以为了保证“稳定性”一般都选择使用 扩展名 或 format请求头 或 固定类型(produces)方式。
今天的内容比较多,各位可以慢慢看,不要退缩哦。
完成任务在留言区回复:666

点击【阅读全文】,锋迷商城项目免费学
原文作者:Java架构栈;如有侵权,请联系管理员删除。