前言
在前一篇文章:
【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—HttpMessageConverter 消息转换器
介绍Spring MVC中消息转换器的关键作用,并且也知道Spring MVC其实是内置了非常非常多的转换器来处理各种各样的MediaType。绝大多数情况下我们并不需要自己去定义转换器,全都交给Spring MVC去处理就够了~
但是Spring MVC既然帮我们内置了这么多的转换器,它默认都给我们加载进去了哪些了?若不是全部都加载进去,那我们遇到特殊的需求怎么自己往里放呢?
另外,我们一个请求request进来,Spring MVC到底是运用了怎么样的匹配规则,匹配到一个最适合的转换器进行消息转换的呢?带着这个问题,通过这篇文章来找找来龙去脉~
HTTP MediaType的基本知识(建议先了解,若很熟悉了可跳过)
配上一张经典的Http请求详情图,方便下面的讲解
第一点:
从上图可以看出Response的Content-Type为text/html,但是我们需要明白的是:决定Response的Content-Type的第一要素是Request请求头中的Accept属性的值,它也被称为MediaType。
这个Accept的值传给服务端,如果服务端支持这种MediaType,那么服务端就按照这个MediaType来返回对应的格式给Response,同时会把返回的的Content-Type设置成对应格式的MediaType
若服务端明确不支持请求头中Accept指定的任何值时,那么就应该返回Http状态码:406 Not Acceptable
**比如上面截图例子:**请求头中Accept支持多种MediaType,服务端最终返回的Content-Type为text/html显然是木有问题的。
第二点:
如果Accept指定了多个MediaType,并且服务端也支持多个MediaType,那么Accept应该同时指定各个MediaType的QualityValue(也就是如图中的q值),,,服务端根据q值的大小来决定这几个MediaType类型的优先级,一般是大的优先。q值不指定时,默认视为q=1.
上图的Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3为Chrome浏览器的默认请求头的值。
它的含义为:服务端在支持的情况下应该优先返回text/html,其次是application/xhtml+xml。前面几个都不支持时,服务器可以自行处理 /,返回一种服务器自己支持的格式。
第三点:
一个HTTP请求没有指定Accept,默认视为指定 Accept: /;请求头里没有指定Content-Type,默认视为 null,就是没有。
第四点:
Content-Type若指定了,必须是具体确定的类型,不能包含 *.
备注:上面属于Http规范的范畴,Spring MVC基本遵循上面这几点~~~
Spring MVC默认加载的消息转换器有哪些?
为了更好的理解Spring MVC对消息转换器的匹配规则,先弄清楚Spring MVC默认给我们加载了哪些HttpMessageConverter呢?
首先我们从现象上直观的看一下:
(因为消息转换器都放在了RequestMappingHandlerAdapter里,所以我们只需要关注运行时它里面的这个属性值即可)
开启了@EnableWebMvc: 一共会有8个,只要我们classpath下有jackson的包,就会加载它进来。
理由如下:看代码吧(因为开启了@EnableWebMvc
,所以看WebMvcConfigurationSupport
它):
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { ... protected final List<HttpMessageConverter<?>> getMessageConverters() { if (this.messageConverters == null) { this.messageConverters = new ArrayList<>(); // 调用者自己配置消息转换器 // 若调用者自己没有配置,那就走系统默认的转换器们~~~~~ configureMessageConverters(this.messageConverters); if (this.messageConverters.isEmpty()) { addDefaultHttpMessageConverters(this.messageConverters); } // 不管调用者配不配置,通过扩展接口进来的转换器都会添加进来 // 因为复写此个protected方法也是我们最为常用的自定义消息转换器的一个手段~~~~~ extendMessageConverters(this.messageConverters); } return this.messageConverters; } ... // 大多数情况下,我们并不需要配置。因此看看系统默认的addDefaultHttpMessageConverters(this.messageConverters); protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) { StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316 messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(stringHttpMessageConverter); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new ResourceRegionHttpMessageConverter()); try { messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Throwable ex) { // Ignore when no TransformerFactory implementation is available... } messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build())); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } else if (jsonbPresent) { messageConverters.add(new JsonbHttpMessageConverter()); } if (jackson2SmilePresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build())); } if (jackson2CborPresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor(); if (this.applicationContext != null) { builder.applicationContext(this.applicationContext); } messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build())); } } }
这个逻辑走下来,最终能被添加进去就是我们截图的那8个(当然这里指的我们只导入jackson处理json的这个jar的情况下~~~)
说明一点:jackson2SmilePresent用于处理application/x-jackson-smile,代表类为:com.fasterxml.jackson.dataformat.smile.SmileFactory
jackson2CborPresent用于处理application/cbor,代表类为com.fasterxml.jackson.dataformat.cbor.CBORFactory
(Smile和CBOR就是一种数据格式,只是jackson强大的都给与了支持)当下绝大多数情况下我们只需要处理Json数据,所以只需要导入如下一个包即可:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency>
非常不建议导入jackson-all做这种全量导入,太重~
Smile是二进制的JSON数据格式,等同于标准的JSON数据格式。Smile格式于2010年发布,于2010年9月Jackson 1.6版已开始支持
没有开启@EnableWebMvc: ,情况就不一样了:
我们发现仅仅只有4个,并且它并没有处理返回为Json的数据转换器。因此假如我们有如下两个Handler:
// 返回值为string类型 @ResponseBody @RequestMapping(value = "/hello", method = RequestMethod.GET) public String helloGet() throws Exception { // 请注意:我这里又有中文 又有英文 return "哈喽,world"; } // 返回值是个对象,希望被转换为 @ResponseBody @RequestMapping(value = "/hello/json", method = RequestMethod.GET) public Parent helloGetJson() throws Exception { return new Parent("fsx", 18); }
再看第二个请求:
浏览器会显示报错:
它原理就是初始化RequestMappingHandlerAdapter构造构造函数里默认加入的那4个:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... public RequestMappingHandlerAdapter() { StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316 this.messageConverters = new ArrayList<>(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(stringHttpMessageConverter); try { this.messageConverters.add(new SourceHttpMessageConverter<>()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } ... }
由此可见,当我们使用Spring MVC的时候,强烈建议开启注解:@EnableWebMvc,否则功能是比较弱的。
Spring MVC的转换器匹配原理
涉及到转换器的匹配,其实就有对read的匹配和write的匹配。
因为上面我们已经主要接触到了写的过程(比如String、json转换到body里),所以此处我们下跟踪看看向body里write内容的时候是怎么匹配的。
Response返回向body里write时消息转换器的匹配
此处先以请求:http://localhost:8080/demo_war_war/hello为例
我们知道请求交给DispatcherServlet#doDispatch方法,最终会匹配到一个HandlerAdapter然后调用其ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)方法真正处理请求,然后最终都是返回一个ModelAndView
因为此处处理的是write过程,所以处理的是返回值。所以最终处理的是:RequestResponseBodyMethodProcessor#handleReturnValue():
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { ... @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); // Try even with null return value. ResponseBodyAdvice could get involved. // 这里找到消息转换器,来把返回的结果写进response里面~~~ // 该方法位于父类`AbstractMessageConverterMethodArgumentResolver`中,通用的利用转换器处理返回值的方法 writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } ... }
关于返回值的匹配原理,更多详细请参见:
【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler