produces使用固然也比较简单,针对上面报错406的原因,我简单解释如下。
原因:
1、先解析请求的媒体类型:1.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状态码
会调用所有的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默认异常消息处理
再回到开头的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)。