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



相关文章
|
6月前
|
域名解析 网络协议 API
【Azure Container App】配置容器应用的缩放规则 Managed Identity 连接中国区 Azure Service Bus 问题
本文介绍了在 Azure Container Apps 中配置基于自定义 Azure Service Bus 的自动缩放规则时,因未指定云环境导致的域名解析错误问题。解决方案是在扩展规则中添加 `cloud=AzureChinaCloud` 参数,以适配中国区 Azure 环境。内容涵盖问题描述、原因分析、解决方法及配置示例,适用于使用 KEDA 实现事件驱动自动缩放的场景。
161 1
|
3月前
|
缓存 安全 Java
《深入理解Spring》过滤器(Filter)——Web请求的第一道防线
Servlet过滤器是Java Web核心组件,可在请求进入容器时进行预处理与响应后处理,适用于日志、认证、安全、跨域等全局性功能,具有比Spring拦截器更早的执行时机和更广的覆盖范围。
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
236 4
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
832 7
Spring Boot 入门:简化 Java Web 开发的强大工具
|
前端开发 Java Docker
使用Docker容器化部署Spring Boot应用程序
使用Docker容器化部署Spring Boot应用程序
|
Java Docker 微服务
利用Docker容器化部署Spring Boot应用
利用Docker容器化部署Spring Boot应用
304 0
|
负载均衡 网络协议 应用服务中间件
web群集--rocky9.2源码部署nginx1.24的详细过程
Nginx 是一款由 Igor Sysoev 开发的开源高性能 HTTP 服务器和反向代理服务器,自 2004 年发布以来,以其高效、稳定和灵活的特点迅速成为许多网站和应用的首选。本文详细介绍了 Nginx 的核心概念、工作原理及常见使用场景,涵盖高并发处理、反向代理、负载均衡、低内存占用等特点,并提供了安装配置教程,适合开发者参考学习。
323 1
|
消息中间件 NoSQL 安全
(转)Spring Boot加载 不同位置的 application.properties配置文件顺序规则
这篇文章介绍了Spring Boot加载配置文件的顺序规则,包括不同位置的application.properties文件的加载优先级,以及如何通过命令行参数或环境变量来指定配置文件的名称和位置。
777 0
|
Java Spring 开发者
Java Web开发新潮流:Vaadin与Spring Boot强强联手,打造高效便捷的应用体验!
【8月更文挑战第31天】《Vaadin与Spring Boot集成:最佳实践指南》介绍了如何结合Vaadin和Spring Boot的优势进行高效Java Web开发。文章首先概述了集成的基本步骤,包括引入依赖和配置自动功能,然后通过示例展示了如何创建和使用Vaadin组件。相较于传统框架,这种集成方式简化了配置、提升了开发效率并便于部署。尽管可能存在性能和学习曲线方面的挑战,但合理的框架组合能显著提升应用开发的质量和速度。
475 0
|
3月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
311 4