【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter的匹配规则(选择原理)(上)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---HttpMessageConverter的匹配规则(选择原理)(上)

前言


在前一篇文章:

【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—HttpMessageConverter 消息转换器

介绍Spring MVC中消息转换器的关键作用,并且也知道Spring MVC其实是内置了非常非常多的转换器来处理各种各样的MediaType。绝大多数情况下我们并不需要自己去定义转换器,全都交给Spring MVC去处理就够了~


但是Spring MVC既然帮我们内置了这么多的转换器,它默认都给我们加载进去了哪些了?若不是全部都加载进去,那我们遇到特殊的需求怎么自己往里放呢?


另外,我们一个请求request进来,Spring MVC到底是运用了怎么样的匹配规则,匹配到一个最适合的转换器进行消息转换的呢?带着这个问题,通过这篇文章来找找来龙去脉~


HTTP MediaType的基本知识(建议先了解,若很熟悉了可跳过)


配上一张经典的Http请求详情图,方便下面的讲解


image.png

第一点:

从上图可以看出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的包,就会加载它进来。


image.png


理由如下:看代码吧(因为开启了@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: ,情况就不一样了:


image.png



我们发现仅仅只有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);
    }

image.png


再看第二个请求:


image.png


浏览器会显示报错:


image.png


它原理就是初始化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



相关文章
|
18天前
|
Java 应用服务中间件 Nacos
Spring Cloud 常用各个组件详解及实现原理(附加源码+实现逻辑图)
Spring Cloud 常用各个组件详解及实现原理(附加源码+实现逻辑图)
44 0
|
18天前
|
前端开发 Java 测试技术
Java一分钟之Spring MVC:构建Web应用
【5月更文挑战第15天】Spring MVC是Spring框架的Web应用模块,基于MVC模式实现业务、数据和UI解耦。常见问题包括:配置DispatcherServlet、Controller映射错误、视图解析未设置、Model数据传递遗漏、异常处理未配置、依赖注入缺失和忽视单元测试。解决这些问题可提升代码质量和应用性能。注意配置`web.xml`、`@RequestMapping`、`ViewResolver`、`Model`、`@ExceptionHandler`、`@Autowired`,并编写测试用例。
306 3
|
18天前
|
Java 测试技术 开发者
Spring IoC容器通过依赖注入机制实现控制反转
【4月更文挑战第30天】Spring IoC容器通过依赖注入机制实现控制反转
22 0
|
9天前
|
Java 容器 Spring
Spring的加载配置文件、容器和获取bean的方式
Spring的加载配置文件、容器和获取bean的方式
20 3
Spring的加载配置文件、容器和获取bean的方式
|
18天前
|
XML Java 程序员
什么是Spring的IoC容器?
【4月更文挑战第30天】什么是Spring的IoC容器?
25 0
|
18天前
|
前端开发 开发者
【Web 前端】CSS 样式覆盖规则?
【4月更文挑战第22天】【Web 前端】CSS 样式覆盖规则?
|
18天前
|
XML Java 数据格式
【spring】01 Spring容器研究
【spring】01 Spring容器研究
12 0
|
18天前
|
Python 数据可视化 索引
PYTHON用GARCH、离散随机波动率模型DSV模拟估计股票收益时间序列与蒙特卡洛可视化
PYTHON用GARCH、离散随机波动率模型DSV模拟估计股票收益时间序列与蒙特卡洛可视化
PYTHON用GARCH、离散随机波动率模型DSV模拟估计股票收益时间序列与蒙特卡洛可视化
|
18天前
|
机器学习/深度学习 算法 数据可视化
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析3
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析3
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析3
|
18天前
|
数据挖掘
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析
用SPSS Modeler的Web复杂网络对所有腧穴进行关联规则分析