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

简介: 【小家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



相关文章
|
2月前
|
缓存 安全 Java
《深入理解Spring》过滤器(Filter)——Web请求的第一道防线
Servlet过滤器是Java Web核心组件,可在请求进入容器时进行预处理与响应后处理,适用于日志、认证、安全、跨域等全局性功能,具有比Spring拦截器更早的执行时机和更广的覆盖范围。
|
2月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
9月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
538 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
9月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
840 0
|
5月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
377 0
|
5月前
|
JSON 前端开发 Java
Spring MVC 核心组件与请求处理机制详解
本文解析了 Spring MVC 的核心组件及请求流程,核心组件包括 DispatcherServlet(中央调度)、HandlerMapping(URL 匹配处理器)、HandlerAdapter(执行处理器)、Handler(业务方法)、ViewResolver(视图解析),其中仅 Handler 需开发者实现。 详细描述了请求执行的 7 步流程:请求到达 DispatcherServlet 后,经映射器、适配器找到并执行处理器,再通过视图解析器渲染视图(前后端分离下视图解析可省略)。 介绍了拦截器的使用(实现 HandlerInterceptor 接口 + 配置类)及与过滤器的区别
466 0
|
5月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
170 0
|
5月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
282 0
|
4月前
|
Kubernetes Docker Python
Docker 与 Kubernetes 容器化部署核心技术及企业级应用实践全方案解析
本文详解Docker与Kubernetes容器化技术,涵盖概念原理、环境搭建、镜像构建、应用部署及监控扩展,助你掌握企业级容器化方案,提升应用开发与运维效率。
840 108