十个光头九个富,最后一个会砍树
不知你在使用Spring Boot
时是否对这样一个现象"诧异"过:同一个
接口(同一个URL)在接口报错状况下,若你用rest访问,它返回给你的是一个json
串;但若你用浏览器访问,它返回给你的是一段html
。恰以下面例子(Spring Boot
环境~):html
@RestController @RequestMapping public class HelloController { @GetMapping("/test/error") public Object testError() { System.out.println(1 / 0); // 强制抛出异常 return "hello world"; } }
使用浏览器访问:http://localhost:8080/test/error
使用Postman访问:
同根不一样命有木有。RESTful
服务中很重要的一个特性是:同一资源能够有多种表述,这就是咱们今天文章的主题:内容协商(ContentNegotiation
)。前端
虽然本文主要是想说Spring MVC
中的内容协商机制,可是在此以前是颇有必要先了解HTTP
的内容协商是怎么回事(Spring MVC
实现了它而且扩展了它更为强大~)。java
一个URL资源
服务端能够以多种形式进行响应:即MIME(MediaType
)媒体类型。但对于某一个客户端(浏览器、APP、Excel导出...)来讲它只须要一种。so这样客户端和服务端就得有一种机制来保证这个事情,这种机制就是内容协商机制。程序员
http
的内容协商方式大体有两种:web
(经常使用)客户端发请求时就指明须要的MIME
们(好比Http
头部的:Accept
),服务端根据客户端指定的要求返回合适的形式,而且在响应头中作出说明(如:Content-Type
)json
1. 若客户端要求的MIME类型服务端提供不了,那就406错误吧~
==请求头==Accept
:告诉服务端须要的MIME(通常是多个,好比text/plain
,application/json
等。/表示能够是任何MIME资源)Accept-Language
:告诉服务端须要的语言(在中国默认是中文嘛,但浏览器通常均可以选择N多种语言,可是是否支持要看服务器是否能够协商)Accept-Charset
:告诉服务端须要的字符集Accept-Encoding
:告诉服务端须要的压缩方式(gzip,deflate,br)
==响应头==Content-Type
:告诉客户端响应的媒体类型(如application/json
、text/html
等)Content-Language
:告诉客户端响应的语言Content-Charset
:告诉客户端响应的字符集Content-Encoding
:告诉客户端响应的压缩方式(gzip)segmentfault
Accept
与Content-Type
的区别有不少文章粗暴的解释:Accept
属于请求头,Content-Type
属于响应头,其实这是不许确的。
在先后端分离开发成为主流的今天,你应该不乏见到前端的request请求上大都有Content-Type:application/json;charset=utf-8
这个请求头,所以可见Content-Type
并不只仅是响应头。后端
HTTP协议规范的格式以下四部分:浏览器
Content-Type
指请求消息体的数据格式,由于请求和响应中均可以有消息体,因此它便可用在请求头,亦可用在响应头。
关于更多Http中的Content-Type
的内容,我推荐参见此文章:Http请求中的Content-Type服务器
Spring MVC
实现了HTTP
内容协商的同时,又进行了扩展。它支持4种协商方式:
HTTP
头Accept
说明:如下示例基于Spring进行演示,而非
Spring Boot
Accept
@RestController @RequestMapping public class HelloController { @ResponseBody @GetMapping("/test/{id}") public Person test(@PathVariable(required = false) String id) { System.out.println("id的值为:" + id); Person person = new Person(); person.setName("fsx"); person.setAge(18); return person; } }
若是默认就这样,无论浏览器访问仍是Postman访问,获得的都是json串。
但若你仅仅只需在pom
加入以下两个包:
<!-- 此处须要导入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>
再用浏览器/Postman访问,获得结果就是xml了,形如这样:
有的文章说:浏览器是xml,postman是json。本人亲试:都是xml。
但若咱们postman
手动指定这个头:Accept:application/json
,返回就和浏览器有差别了(若不手动指定,Accept
默认值是*/*
):
而且咱们能够看到response
的头信息对好比下:
手动指定了Accept:application/json
:
木有指定Accept(默认*/*
):
Chrome
浏览器请求默认发出的Accept
是:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
。
因为我例子使用的是@ResponseBody
,所以它不会返回一个view:交给消息转换器处理,所以这就和MediaType
以及权重有关了。
消息最终都会交给AbstractMessageConverterMethodProcessor.writeWithMessageConverters()
方法:
// @since 3.1 AbstractMessageConverterMethodProcessor: protected <T> void writeWithMessageConverters( ... ) { Object body; Class<?> valueType; Type targetType; ... HttpServletRequest request = inputMessage.getServletRequest(); // 这里交给contentNegotiationManager.resolveMediaTypes() 找出客户端能够接受的MediaType们~~~ // 此处是已经排序好的(根据Q值等等) List<MediaType> acceptableTypes = getAcceptableMediaTypes(request); // 这是服务端它所能提供出的MediaType们 List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); // 协商。 通过必定的排序、匹配 最终匹配出一个合适的MediaType ... // 把待使用的们再次排序, MediaType.sortBySpecificityAndQuality(mediaTypesToUse); // 最终找出一个最合适的、最终使用的:selectedMediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } }
acceptableTypes
是客户端经过Accept
告知的。producibleTypes
表明着服务端所能提供的类型们。参考这个getProducibleMediaTypes()
方法:
AbstractMessageConverterMethodProcessor: protected List<MediaType> getProducibleMediaTypes( ... ) { // 它设值的地方惟一在于:@RequestMapping.producers属性 // 大多数状况下:咱们通常都不会给此属性赋值吧~~~ Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } // 大多数状况下:都会走进这个逻辑 --> 从消息转换器中匹配一个合适的出来 else if (!this.allSupportedMediaTypes.isEmpty()) { List<MediaType> result = new ArrayList<>(); // 从全部的消息转换器中 匹配出一个/多个List<MediaType> result出来 // 这就表明着:我服务端所能支持的全部的List<MediaType>们了 for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } else if (converter.canWrite(valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } }
能够看到服务端最终可以提供哪些MediaType
,来源于消息转换器HttpMessageConverter
对类型的支持。
本例的现象:起初返回的是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
这种媒体格式,因此若不导包协商出来的结果是:application/json
。
默认状况下优先级是xml高于json。固然通常都木有xml包,因此才轮到json的。
另外还须要注意一点:有的小伙伴说经过在请求头里指定Content-Type:application/json
来达到效果。如今你应该知道,这样作显然是没用的(至于为什么没用,但愿读者作到了心知肚明),只能使用Accept
这个头来指定~~~
第一种协商方式是Spring MVC
彻底基于HTTP Accept
首部的方式了。该种方式Spring MVC
默认支持且默认已开启。
优缺点:
基于上面例子:若我访问/test/1.xml
返回的是xml,若访问/test/1.json
返回的是json;完美~
这种方式使用起来很是的便捷,而且还不依赖于浏览器。但我总结了以下几点使时的注意事项:
test.json / test.xml
就404~@PathVariable
的参数类型只能使用通用类型(String/Object
),由于接收过来的value值就是1.json/1.xml
,因此若用Integer
接收将报错类型转换错误~
1. 小技巧:我我的建议是这部分不接收(这部分不使用`@PathVariable`接收),拿出来**只为内容协商使用**
优缺点:
这种协商方式Spring MVC
支持,但默认是关闭的,须要显示的打开:
@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 支持请求参数协商 configurer.favorParameter(true); } }
请求URL:/test/1?format=xml
返回xml;/test/1?format=json
返回json。一样的我总结以下几点注意事项:
低于
扩展名(所以你测试时若想它生效,请去掉url的后缀)优缺点:
它就是利用@RequestMapping
注解属性produces
(可能你平时也在用,但并不知道缘由):
@ResponseBody @GetMapping(value = {"/test/{id}", "/test"}, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Person test() { ... }
访问:/test/1
返回的就是json;即便你已经导入了jackson的xml包,返回的依旧仍是json。
它也有它很很很重要的一个注意事项:produces
指定的MediaType
类型不能和后缀、请求参数、Accept冲突。例如本利这里指定了json
格式,若是你这么访问/test/1.xml
,或者format=xml
,或者Accept
不是application/json或者*/*
将没法完成内容协商:http状态码为406,报错以下:produces
使用当然也比较简单,针对上面报错406的缘由,我简单解释以下。
一、先解析请求的媒体类型:1.xml
解析出来的MediaType
是application/xml
二、拿着这个MediaType
(固然还有URL、请求Method等全部)去匹配HandlerMethod
的时候会发现producers
匹配不上
三、匹配不上就交给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)); } }
四、抛出异常后最终交给DispatcherServlet.processHandlerException()
去处理这个异常,转换到Http
状态码
会调用全部的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(); }
Spring MVC
默认注册的异常处理器是以下3个:
![]()
有了关于Accept
的原理描述,理解它就很是简单了。由于指定了produces
属性,因此getProducibleMediaTypes()
方法在拿服务端支持的媒体类型时:
protected List<MediaType> getProducibleMediaTypes( ... ){ Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } ... }
由于设置了producers
,因此代码第一句就能拿到值了(后面的协商机制彻底同上)。
备注:若produces属性你要指定的很是多,建议可使用
!xxx
语法,它是支持这种语法(排除语法)的~
优缺点:
HandlerMethod
处理器缺失灵活性再回到开头的Spring Boot
为什么对异常消息,浏览器和postman的展现不同。这就是Spring Boot
默认的对异常处理方式:它使用的就是基于 固定类型(produces)实现的内容协商。
Spirng Boot
出现异常信息时候,会默认访问/error
,它的处理类是:BasicErrorController
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { ... // 处理类浏览器 @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { ... return (modelAndView != null ? modelAndView : new ModelAndView("error", model)); } // 处理restful/json方式 @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } ... }
有了上面的解释,对这块代码的理解应该就没有盲点了~
内容协商在RESTful
流行的今天仍是很是重要的一块内容,它对于提高用户体验,提高效率和下降维护成本都有不可忽视的做用,注意它三的优先级为:后缀 > 请求参数 > HTTP首部Accept
通常状况下,咱们为了通用都会使用基于Http的内容协商(Accept),但在实际应用中其实不多用它,由于不一样的浏览器可能致使不一样的行为(好比Chrome
和Firefox
就很不同),因此为了保证“稳定性”通常都选择使用方案二或方案三(好比Spring的官方doc)。
【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(二)---RequestMappingHandlerMapping系列
ContentNegotiation内容协商机制(一)---Spring MVC内置支持的4种内容协商方式【享学Spring MVC】
ContentNegotiation内容协商机制(二)---Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】
ContentNegotiation内容协商机制(三)---在视图View上的应用:ContentNegotiatingViewResolver深度解析【享学Spring MVC】
==The last:若是以为本文对你有帮助,不妨点个赞呗。固然分享到你的朋友圈让更多小伙伴看到也是被做者本人许可的~
==
**若对技术内容感兴趣能够加入wx群交流:Java高工、架构师3群
。
若群二维码失效,请加wx号:fsx641385712
(或者扫描下方wx二维码)。而且备注:"java入群"
字样,会手动邀请入群**==若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一块儿飞==