【小家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



相关文章
|
1月前
|
运维 持续交付 虚拟化
深入解析Docker容器化技术的核心原理
深入解析Docker容器化技术的核心原理
47 1
|
1月前
|
负载均衡 算法 Java
除了 Ribbon,Spring Cloud 中还有哪些负载均衡组件?
这些负载均衡组件各有特点,在不同的场景和需求下,可以根据项目的具体情况选择合适的负载均衡组件来实现高效、稳定的服务调用。
86 5
|
2月前
|
存储 监控 Shell
docker的底层原理二:容器运行时环境
本文深入探讨了Docker容器运行时环境的关键技术,包括命名空间、控制组、联合文件系统、容器运行时以及分离的进程树,这些技术共同确保了容器的隔离性、资源控制和可移植性。
49 5
|
2月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
65 2
|
3月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
|
2月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
191 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
3月前
|
XML 缓存 Java
spring源码剖析-spring-beans(内部核心组件,BeanDefinition的注册,BeanWapper创建)
spring源码剖析-spring-beans(内部核心组件,BeanDefinition的注册,BeanWapper创建)
63 10
|
3月前
|
XML 存储 Java
spring源码刨析-spring-beans(内部核心组件,beanDefinition加载过程)
spring源码刨析-spring-beans(内部核心组件,beanDefinition加载过程)
|
2月前
|
Java 应用服务中间件 Apache
浅谈Tomcat和其他WEB容器的区别
Tomcat是一款轻量级的免费开源Web应用服务器,常用于中小型系统及并发访问量适中的场景,尤其适合开发和调试JSP程序。它不仅能处理HTML页面,还充当Servlet和JSP容器。相比之下,物理服务器是指具备处理器、硬盘等硬件设施的服务器,如云服务器,其设计目标是在处理能力、稳定性和安全性等方面提供高标准服务。简言之,Tomcat专注于运行Java应用,而物理服务器则提供基础计算资源。
|
2月前
|
前端开发 Java
【案例+源码】详解MVC框架模式及其应用
【案例+源码】详解MVC框架模式及其应用
164 0