- 一、前言
- 二、SpringBoot2应用背景与技术升级
- 1.简介&生态
- 2.时代背景
- 2.1 大数据-背景
- 2.2 微服务-架构
- 2.3 分布式-系统
- 2.4 云原生-构建
- 3.技术升级
- 3.1 基础环境升级
- 3.2 默认软件替换和优化
- 3.3 新技术的引入
- 三、SpringBoot功能源码&原理图解
- 1.依赖管理原理
- 1.1版本仲裁机制
- 1.2场景启动器(Starter)
- 2.自动配置原理
- 2.1 默认扫描包结构
- 2.2 自动配置类
- 四、SpringMVC功能源码&原理图解
- 1.静态资源配置原理
- 1.1静态资源访问路径
- 1.2欢迎页配置
- 2.请求处理
- 2.1请求映射
- 2.2原生参数&注解注入
- 2.3自定义类型参数处理
- 3.响应处理
- 3.1数据响应
- 3.2内容协商
- 3.3视图解析
- 4.异常处理(待更新)
- 五、参考资料
一、前言
Summary:本章介绍为什么要学习源码并分享个人学习感悟,想看干货的朋友可以直奔三四章~
- 框架架构师体验卡!
相信很多朋友和我一样,本身从事开发工作已久,期间也应用Spring全家桶开发了无数优秀的JavaEE项目,却一直对底层原理不甚了解,框架到底帮我们配置了什么?底层如何封装?应用了哪些设计模式?今天,就让我们当一回框架架构师,以SpringBoot框架为例探究框架源码与执行流程!
- 何谓框架的框架?
所谓“框架”就是可被应用开发者定制的应用骨架,是整个或部分系统的可重用设计。(把百度关掉,说人话!)简言之,框架就是底层封装好大量核心代码的程序,使开发者可以在其上继续开发自己所需的业务,避免重复造轮子嘛。为什么要使用框架?因为框架可以有效提升开发效率、程序的健壮性、服务性能、后续功能的可维护、可扩展性等。实际上很多公司都在创建或使用自己的框架,阿里等互联网巨头公司也都有许多开源的框架。
那么什么是“框架的框架”?一个框架往往可以用来开发一种服务,实现一项业务功能,但一个完整的项目是包含多个服务多个业务功能的,实际开发中我们往往需要用到多个框架,从而产生了一个“框架整合”的问题!框架整合其实是一个很折磨的过程…你并不知道整合过程中各个框架之间是否会产生不兼容性或依赖冲突,即便兼容你也要为每个框架都能正常运行做无数的配置工作,就连Spring原生框架之间的SSM整合都需要较多步骤,其他框架整合可想而知。因此,SpringBoot应运而生,它就是“框架的框架”,专门负责解决“框架整合”问题的,通过依赖管理、场景启动器、自动配置等方式实现,其核心思想就是“约定大于配置”,通过约定好的默认配置取代手动配置,简化框架整合流程。
- 封装过深的代价?
原生JavaWeb开发——>Spring横空出世——>SSM整合——>SpringBoot全自动~
技术演进的过程中,开发人员需要写的重复冗余的代码越来越少,配置过程也越来越简化,但是这种便捷是有“代价”的。实现同样的功能,并不是代码总量减少了,而是框架底层帮我们写好了大量的代码,做好了无数的配置。众所周知,“力量是要付出代价的!” "封装过深"也有代价:
① 对业务逻辑的每一个细节不如以前熟悉了
② 难以精通理解底层配置原理
③ 调试与检错难度直线上升
不过不用担心!我们作为框架的使用者、“资深”从业者、互联网时代的建造者!(逐渐离谱….)我们要真正“掌握雷电”,而不是成为“锤子之神”~这些“代价”都是可以优化甚至彻底解决的。我们通过深入学习框架源码,理解其封装过程,就能成为真正掌握框架的开发者,而不是被框架所限制!
- 先验知识
想要完全掌握SpringBoot2的源码需要很扎实的编程基础和深入的设计思想,坦白说着手写这篇博客的我也没有足够深厚的底蕴精通SpringBoot2的源码,只是将自己掌握的部分以流程图和文字详解的方式分享给大家。总体来讲,无论是想通过本文学习SpringBoot源码还是想深入理解设计原理,都应该具备以下基础的先验知识储备:
① 了解原生Web开发流程,熟悉Servlet、HTTP请求响应格式、Session等域对象
② 熟悉Spring基础框架,包括IOC、AOP原理,Bean对象生命周期等
③ 熟悉SpringMVC框架,包括DispatcherServle请求分发原理、ModelAndMap模型、拦截器原理等
④ 理解常见设计模式,如适配器模式、装饰器模式、代理模式、责任链模式等
掌握了以上知识,才能流畅的进行SpringBoot源码学习并避免遇到理解上的障碍,同样在讲解过程中我也会穿插各种设计模式,加深大家对源码中设计思想的理解。
- 感想&交流
“程序员是最乐于分享的团体”。首先声明,本文创作的内容基本源于个人理解,并非照搬其他博客,原理图和执行流程也都是自己分析并创作的,付出了一定的心血~写作过程中当然也借鉴了多方资料,包括视频教程、博客论坛、官方文档,这些资料在文末“参考资料”章节中都有展示,分享给大家一起学习。
这是我第一次提笔写文,目前在读研并有过一些中大型企业项目开发经历,可能还只能算作半个从业者吧(因为没毕业嘛),因此创作过程中心情是非常忐忑不安的,写下的内容都会反复理解检查几遍,生怕误人子弟。不过话说回来,我一届学生,凭借自己的学习理解去解析时下最热门的开发框架底层源码,可能难免会有纰漏与错误,如果各路大神在本文中看到任何理解不当或有误的内容,请及时指正,感谢大家的包容与指导!如果有想与我交流的朋友可以留言或私信,希望与大家共同进步! 早日成为人均架构师(笑)
Summary:本章以时代需求为背景介绍了技术演进过程,时代的需求与技术背景正是SpringBoot2升级的原因,从而让读者更好的理解SpringBoot2在这些新需求和新技术下衍生的新特性
二、SpringBoot2应用背景与技术升级 1.简介&生态
Spring Boot是Pivotal团队在Spring的基础上提供的一套全新的开源框架,其目的是为了简化Spring应用的搭建和开发过程。Spring Boot去除了大量的XML配置文件,简化了复杂的依赖管理,它具有Spring一切优秀特性且使用更简单、功能更丰富,性能更稳定更健壮。此外Spring Boot集成了大量常用的第三方库配置,Spring Boot应用中这些第三方库几乎可以是零配置的开箱即用(out-of-the-box),大部分的 Spring Boot应用都只需要非常少量的配置代码(基于Java的配置),开发者能够更加专注于业务逻辑。 其具体优势可概括为以下几点:
- 独立运行的 Spring 项目
Spring Boot 可以以jar包的形式独立运行,Spring Boot项目只需通过命令“ java–jar xx.jar” 即可运行。- 内嵌 Servlet 容器
Spring Boot 使用嵌入式的 Servlet 容器(例如Tomcat、Jetty 或者Undertow等),应用无需打成WAR包 。- 提供 starter 简化Maven配置
Spring Boot 提供了一系列的“starter”项目对象模型(POMS)来简化 Maven 配置。- 提供了大量的自动配置
Spring Boot 提供了大量的默认自动配置,来简化项目的开发,开发人员也通过配置文件修改默认配置。- 自带应用监控
Spring Boot可以对正在运行的项目提供监控。- 无代码生成和 xml 配置
Spring Boot不需要任何xml配置即可实现Spring的所有配置
虽然我们常使用SpringBoot来做Web开发,但实际上SpringBoot打造的功能生态非常丰富,包括:
- web开发
- 数据访问
- 安全控制
- 分布式
- 消息服务
- 移动开发
- 批处理
这些功能可不是我编的~是Spring官方网站首页上摆着的:
Spring功能图
更多特性与功能请参考:官方文档-OverView
2.时代背景“时势造英雄”——SpringBoot成为时下最热门框架的背后,实际上是时代背景所驱,其框架设计之初就考虑到了当下开发中的痛点与难点,并提出了解决方案,从而被广泛应用于高性能服务端程序开发。下面让我们看看当下开发所面临的时代背景与解决方案。
2.1 大数据-背景大数据时代是一个早已泛滥的词,在我的理解里,对于我们开发者或从业者来说大数据包含两个层面的意思:数据量大、并发量高,特点可概括为“5V”+“3高”。数据量大描述的是需要存储的数据内容和特点,并发量高是伴随用户增多而出现的现象,通常人们说的大数据主要指第一层意思即数据量大,其特点是5V。
大数据5V特点
- 数据量大(Volume)
第一个特征是数据量大。大数据的起始计量单位至少是P(1000个T)、E(100万个T)或Z(10亿个T)。 - 类型繁多(Variety)
第二个特征是数据类型繁多。包括网络日志、音频、视频、图片、地理位置信息等等,多类型的数据对数据的处理能力提出了更高的要求。 - 价值密度低(Value)
第三个特征是数据价值密度相对较低。如随着物联网的广泛应用,信息感知无处不在,信息海量,但价值密度较低,如何通过强大的机器算法更迅速地完成数据的价值“提纯”,是大数据时代亟待解决的难题。 - 速度快、时效高(Velocity)
第四个特征是处理速度快,时效性要求高。这是大数据区分于传统数据挖掘最显著的特征。 - 真实(Veracity)
大数据中的内容是与真实世界中的发生息息相关的,要保证数据的准确性和可信赖度。研究大数据就是从庞大的网络数据中提取出能够解释和预测现实事件的过程。
原先既有的技术架构和路线,已经无法高效处理如此海量的数据,而对于相关组织来说,如果投入巨大采集的信息无法通过及时处理反馈有效信息,那将是得不偿失的。SpringBoot框架中可以通过原生和集成第三方技术来处理海量数据。
参考: 大数据5V特点-CSDN
2.2 微服务-架构 微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责。
实际上,微服务的产生是为了应对微服务架构使应用程序更易于扩展和更快地开发,也是适应大数据时代的软件开发架构,从而实现加速创新并缩短新功能的上市时间。
微服务示意图
微服务概念最早是Martin Fowler于2014年的一篇文章《Microservices – the new architectural style》中提出的,其主要特点包括:
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
原文链接:MartinFowler网站——Microservices Guide
2.3 分布式-系统 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。
分布式系统示意图
分布式系统本质上有两个目的,一是增强系统实时处理所需的算力,二是扩展系统数据存储的容量。但随之而来的,是系统结构复杂化导致的诸多问题与解决方案,包括以下几点(注解了自己对该问题和解决方案的理解):
● 远程调用:RPC,通过NIO方式处理封装调用方法参数的数据并相应执行结果
● 服务发现:使用一个注册中心来记录分布式系统中的全部服务的信息,以便其他服务能够快速的找到这些已注册的服务
● 负载均衡:通过轮询或Nginx等服务器策略分配请求降低单服务器QPS负载压力
● 服务容错:当发生网络异常或代码异常时,服务的返回结果和逻辑处理机制
● 配置管理:统一管理部署在多台服务器上的某服务的配置
● 服务监控:通过心跳等机制监控服务是否存活(网络及CPU内存等运行状态)
● 链路追踪:还原分布式服务调用过程的链路
● 日志管理:针对海量日志使用流处理,涉及Flink、kafka等大数据框架
● 任务调度:求解多任务多站点下执行时间最小化问题
以上是我自己对这些概念的简单解释,若想深度理解分布式系统可参考博客: 分布式系统概念详解及学习方法
2.4 云原生-构建正是由于分布式应用系统使结构复杂化,才会面临以上诸多问题,解决方案也相应的较为繁琐,因此衍生了服务上云的需求。而Spring也提出了相应的框架来响应这一需求,即SpringBoot + SpringCloud。说白了,就是分布式系统服务上云,以便更加便捷的构建和管理结构复杂的高性能服务器应用。
Spring云原生方案
云原生是一种构建和运行应用程序的方法,是一套技术体系和方法论。云原生(CloudNative)是一个组合词,Cloud+Native。Cloud表示应用程序位于云中,而不是传统的数据中心;Native表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。
云原生概念图
2018 年 3 月 1 号 Spring Boot 2.0.0.RELEASE 正式发布。众所周知,SpringBoot号称“版本帝”,小版本更新时都会不断更新新功能和底层源码实现,这次2.0大版本究竟更新了什么呢?
3.1 基础环境升级- JDK版本升级
最低 JDK 8,支持 JDK 9,不再支持 Java 6 和 7。
这是因为SpringBoot2内部源码设计是基于JDK8的很多新特性的,包括:接口方法的默认实现、函数回调以及一些新的 API(如 javax.time)等。 - 第三方依赖组件升级
主要包括以下组件版本:Jetty 9.4、Tomcat 8.5、Hibernate 5.2、Gradle 3.4、Thymeleaf 3.0、Flyway 5等
-
数据库连接池
默认连接池已从 Tomcat 切换到 HikariCP。
HikariCP 是一个高性能的 JDBC 连接池,号称是 Java 业界最快的数据库连接池,官网提供了 c3p0、dbcp2、tomcat、vibur 和 Hikari 等数据连接池的性能对比。 -
Spring Security
作为原生的Security推荐组件,SpringBoot2中对其进行了更好的集成优化。在Spring Boot 2.0中极大地简化了默认的安全配置,并使添加定制安全变得简单。 -
OAuth 2.0
OAuth 2.0 是 OAuth 协议的延续版本,但不向后兼容 OAuth 1.0,它可以使第三方应用程序或客户端获得对 HTTP 服务上(如 Google、GitHub )用户帐户信息的有限访问权限。
Spring Boot 2.0 将 Spring Security OAuth 项目迁移到 Spring Security。不再提供单独的依赖包,Spring Boot 2.0 通过Spring Security 5提供OAuth 2.0客户端支持。 -
Micrometer
Micrometer 是一款监控指标的度量类库,可以让你在没有供应商锁定的情况下对 JVM 的应用程序代码进行调整。
Spring Boot 2.0 增强了对 Micrometer 的集成,不再提供自己的指标API。依靠 micrometer.io 来满足所有应用程序监视需求。 -
Redis默认使用Lettuce
替代了前的 Jedis 作为底层的 Redis 连接方式。Lettuce 是一个可伸缩的线程安全的 Redis 客户端,用于同步、异步和反应使用。多个线程可以共享同一个 RedisConnection,它利用优秀 Netty NIO框架来高效地管理多个连接,支持先进的 Redis 功能,如 Sentinel、集群、流水线、自动重新连接和 Redis 数据模型。 -
配置属性绑定
修复了部分绑定规则的错误漏洞,并提供了YMAL格式的配置文件绑定。
在Spring Boot 2.0中,使用 @ConfigurationProperties 的绑定机制被重新设计,限制了绑定规则,并修复了 Spring Boot 1.x 中的许多不一致的地方。 -
转换器支持
转换器实现类的源码改进。
Binding使用了一个新的 ApplicationConversionService 类,它提供了一些额外有用的转化。包括转换器的Duration类型和分隔字符串等。
该 Duration转换器允许在任一 ISO-8601 格式的持续时间,或是一个简单的字符串(如 10m,10 分钟)。现有的属性已更改为默认使用 Duration,@DurationUnit 注释通过设置如果没有指定所使用的单元确保向后兼容性。 -
Actuator 改进
在 Spring Boot 2.0 中 Actuator endpoints 有很大的改进,所有 HTTP Actuator endpoints 现在都在该 /actuator 路径下公开,并且生成的 JSON 有效负载得到了改进。
- 支持HTTP/2
HTTP2.0版本相比1.x版本引入了很多新特性,包括:新的二进制格式(Binary Format)、多路复用(MultiPlexing)、header压缩、服务端推送(server push)、优先级数据流响应等。 - Kotlin 的支持
Spring Boot 2.0 现在包含对 Kotlin 1.2.x 的支持,并提供了runApplication,一个使用 Kotlin 运行 Spring Boot 应用程序的方法。我们还公开和利用了Kotlin对其他 Spring项目(如Spring Framework,Spring Data和Reactor)已添加到其最近版本中的支持。 - 响应式编程WebFlux
响应式编程是一种面向数据流和变化传播的编程范式,可以更方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
WebFlux 模块的名称是 spring-webflux,名称中的 Flux 来源于 Reactor 中的类 Flux。Spring WebFlux 有一个全新的非堵塞的函数式 Reactive Web 框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。
Summary:本章是SpringBoot核心功能的源码解析和原理讲解
三、SpringBoot功能源码&原理图解可能有些朋友会有疑问,为什么SpringBoot的源码解析会分为SpringBoot功能的源码和SpringMVC的功能源码呢?实际上,在前言章节中我们说了SpringBoot是框架的框架,其本身的功能只是解决“框架整合”的问题,至于其他开发中的核心业务流程依然是SpringMVC来实现的,因此我们分为两章介绍。
1.依赖管理原理SpringBoot和Spring都是依赖于Maven使用依赖管理的,在Maven中使用groupId,artifactId,version组成的Coordination(坐标)唯一标识一个依赖,通过配置文件中的远程仓库地址下载(只需下载一次后就保存到本地仓库)。SpringBoot在Maven的依赖管理基础上又做了进一步优化,主要包括:通过版本仲裁机制自动控制依赖版本、通过场景启动器批量引入依赖jar包。
1.1版本仲裁机制我们先来看看什么是版本仲裁机制。如果大家仔细观察过SpringBoot的POM.xml文件,会发现里面的很多依赖是没有写版本号的!
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
org.projectlombok
lombok
根据Maven规则,坐标必须由groupId,artifactId,version才能唯一确定,那为什么SpringBoot中可以省略版本呢?省略后我们又怎么知道项目中引入的redis、mybatis和lombok版本的呢?这就是Springboot中的版本仲裁机制。
原理解析:
为什么Spring Boot导入dependency时不需要指定版本?首先,在POM.xml文件的顶层声明了一个父容器,该项目下的所有模块默认继承父容器中的依赖配置。
org.springframework.boot spring-boot-starter-parent 2.6.7
tip: 按Ctrl+左键单击查看源文件/源码
这个父容器中添加的spring-boot-starter-parent依赖是什么呢?我们可以查看其底层源文件,发现该依赖又有一个顶层父容器依赖:spring-boot-dependencies
org.springframework.boot spring-boot-dependencies 2.6.7
这就是版本自动控制的根源所在了,查看spring-boot-dependencies的源文件如下:
5.16.4 2.7.7 1.9.96 2.19.1 ……2.4.4 4.3.5 5.3.19 2.8.5 ……org.apache.activemq activemq-amqp ${activemq.version} org.apache.activemq activemq-blueprint ${activemq.version} …… org.apache.activemq activemq-broker ${activemq.version}
其配置文件中,通过properties属性的方式写死了所有常用组件的版本号,而其底层依赖dependency中的在坐标的version中引入了上面写死的properties属性标签,从而唯一控制了所有常用组件的版本号。这就是版本仲裁的根本原理。
假设对版本仲裁的结果不满意如何修改呢?只需要在POM.xml中通过标签指定对应的依赖版本即可,原理就是根据Maven依赖的就近原则,加载时会优先使用我们声明的属性标签决定依赖版本。例如修改mysql的版本:
5.1.43
原理图:
版本仲裁机制原理图解
概念:
Starter被称为场景启动器,它能将模块/项目所需的依赖整合起来并对模块内的Bean根据环境进行自动配置。开发者只需要引入相应开发场景的Starter,其内就会包含该场景所需的依赖及配置,Spring Boot也会自动扫描并加载Starter下的所有依赖。因此其实际功能总结如下:
①整合引入对应场景需要的依赖库;
②提供对模块的配置项给使用者、提供配置项的默认值(类似版本仲裁机制),使用者不指定配置时使用默认值,也可根据需要指定配置项的值(xxxProperties);
③提供自动配置类(xxxAutoConfiguration)对模块内的Bean进行自动装配
命名:
| 官方启动器 | 第三方启动器 | |
|---|---|---|
| 前/后缀 | spring-boot-starter- | -spring-boot-starter |
| 模式 | spring-boot-starter-模块名 | 模块名-spring-boot-starter |
| 举例 | spring-boot-starter-web、spring-boot-starter-jdbc | mybatis-spring-boot-starter |
原理解析:
我们以spring-boot-starter-web为例,即通过web开发场景启动器讲解一下场景启动器的基本原理。首先还是打开spring-boot-starter-web源文件:
org.springframework.boot spring-boot-starter 2.6.7 compile org.springframework.boot spring-boot-starter-json 2.6.7 compile org.springframework.boot spring-boot-starter-tomcat 2.6.7 compile org.springframework spring-web 5.3.19 compile org.springframework spring-webmvc 5.3.19 compile
可以看到Web场景启动器中引入了很多新的启动器,这些启动器底层就是响应的依赖,包括json、tomcat、Spring-web和webmvc等,即包含几乎所有Web开发场景下所需的依赖,并管理了其版本和作用范围。因此,我们只需在POM.xml中引入对应场景的启动器,就可以轻松进行该场景下的开发工作,而不用关心其底层依赖。
值得关注的是,当你点开任意一个Starter(官方发布的原生starter)会发现其内都依赖spring-boot-starter,即部分场景启动器依赖于底层启动器,如spring-boot-starter,我们可以称其为“底层场景启动器”,如果我们查看Spring-boot-starter源文件:
org.springframework.boot spring-boot 2.6.7 compile org.springframework.boot spring-boot-autoconfigure 2.6.7 compile org.springframework.boot spring-boot-starter-logging 2.6.7 compile jakarta.annotation jakarta.annotation-api 1.3.5 compile org.springframework spring-core 5.3.19 compile org.yaml snakeyaml 1.29 compile
原来,web场景启动器的底层依赖spring-boot-starter中声明了开发所需的必要依赖,包括spring-core核心代码、yaml支持、注解API、自动配置等。那么具体哪些启动器依赖于更底层的启动器呢?我们可以通过Maven打印依赖树的方式查看,如图所示:
原理图:
场景启动器原理图
更多可用的场景启动器,请参考: 官方文档:所有支持的场景启动器
2.自动配置原理任何一个组件或依赖想要生效都分为两步:首先引入jar包,之后需要对引入的组件进行配置。上文介绍的依赖管理实现的其实就是引入并管理jar包,这一小结我们讲一讲SpringBoot为我们提供的自动配置功能。
2.1 默认扫描包结构首先要介绍的是默认扫描的包结构。在以往的SSM框架中我们需要在web.xml中配置DispatcherServlet等组件,同时在SpringMVC.xml中配置组件扫描的包结构。而在SpringBoot中,会默认扫描主程序所在的包及其下面的所有子包中的组件,官方文档中的默认扫描包结构示意图如下图所示
官方提供的默认包结构示意图
主程序即配置了 @SpringBootApplication注解的类,如果想要修改默认扫描的包结构,有两种方式:
①@SpringBootApplication(scanBasePackages=“指定包路径”)
②@ComponentScan (“指定包路径”)
需要注意的是@SpringBootApplication是一个合成注解,其内包含三个注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(“默认包路径”)因为@ComponentScan是一个不可重复的注解,因此无法在主程序类上注释@ComponentScan。
原理解析:
默认扫描包结构的原理涉及到合成注解中的@EnableAutoConfiguration注解的底层注解 @AutoConfigurationPackage,其内指定了对MainApplication主程序所在包及其子包下的所有组件进行扫描并注册到容器中,具体源码可查看2.2小结中的详细解释。
可以想到,SpringBoot之所以能实现自动配置常用组件的功能,一定是在容器启动前就自动加载了常用组件,如DispatcherServlet、viewResolver、characterEncodingFilter等,具体加载了哪些组件可以通过如下代码查看:
//返回IOC容器
ConfigurableApplicationContext run = SpringApplication.run(Boot01Helloworld2Application.class, args);
//查看IOC容器内所有组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
下面我们来解析一下,SpringBoot是如何在容器启动时自动将这些组件加载到IOC容器中的。
原理解析:
在SpringBoot的启动类上有一个@SpringBootApplication注解,上文说过这是一个合成注解@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan,我们依次查看每一个注解。
- @SpringBootConfiguration
首先依次点击查看@SpringBootApplication注解的源码,再查看其内的@SpringBootConfiguration源码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //代表这是一个配置类!
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
可以看到该注解上标注了一个@Configuration注解,代表这是一个配置类。说明主程序也是一个配置类,并且是SpringBoot核心配置类。
-
@ComponentScan
简单说明一下@ComponentScan,该注解的作用就是指定包扫描路径,其底层是通过TypeExcludeFilter和AutoConfigurationExcludeFilter两个过滤器实现具体包扫描规则的。 -
@EnableAutoConfiguration
主程序既然是核心配置类,具体配置了什么内容呢?查看第二个注解,见名知意,该注解是指激活自动配置,查看源代码发现它也是两个注解的合成注解:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
① @AutoConfigurationPackage
见名知意,自动配置包,即指定了默认的包规则。查看该注解源码:
@Import(AutoConfigurationPackages.Registrar.class) //给容器中导入一个组件
public @interface AutoConfigurationPackage {}
//利用Registrar给容器中导入一系列组件
//哪一系列组件?其实是将指定的一个包下的所有组件导入进来,哪个包?MainApplication
//(即@SpringbootApplication组件标注的类)所在包下。
首先通过@Import注解,利用Registrar类给容器中批量导入一系列组件,哪一系列组件?其实是将指定的一个包下的所有组件导入进来,哪个包?MainApplication所在包及其子包。为什么说Rigistra类的作用是导入组件以及导入组件的具体路径为什么是主程序包及子包,参考Rigistra.class源码如下:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set
② @Import(AutoConfigurationImportSelector.class)
该注解导入AutoConfigurationImportSelector.class,该类源码较长我就不复制粘贴过来了,其功能就是使用指定路径的自动配置类向IOC容器中加载组件。简述该类的主要方法从而解析原理,该类中逐级调用方法如下:
(a)该类里核心方法是String[] selectImports(),该方法内通过调用getAutoConfigurationEntry(annotationMetadata)给容器中批量导入一些组件
(b)所有要加载到容器中的组件的全类名是通过调用如下方法获取:
List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类全类名List
(c)获取到要加载的全类名后,通过Spring工厂加载器去加载这些组件SpringFactoriesLoader.loadFactoryNames(…)
设计模式:工厂模式; 原理:反射
(d)具体是利用工厂加载器中的加载方法 Map
(e)加载每一个jar包的META-INF/spring.factories文件(如果存在)
默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
如spring-boot-autoconfigure-2.6.7.jar包里面也有META-INF/spring.factories
打开改文件就会发现,里面写死了所有IOC容器初始化时加载的所有组件的自动配置类如下图所示: 其中xxxAutoConfiguration即相应xxx组件的自动配置类 注意,这些只是组件的自动配置类,自动配置类有些生效有些不生效,生效的自动配置类才会向容器中添加相应组件,不生效的自动配置类则不添加该组件。具体哪些自动配置类生效呢?实际上每一个自动配置类上面的@Conditional注解决定了其是否生效,这就是按需加载。常见的@Conditinal注解如下:
@ConditionalOnBean:当容器里有指定的bean的条件下。
@ConditionalOnMissingBean:当容器里不存在指定bean的条件下。
@ConditionalOnClass:当类路径下有指定类的条件下。
@ConditionalOnMissingClass:当类路径下不存在指定类的条件下。
@ConditionalOnProperty:指定的属性是否有指定的值,比如:@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。
按需加载:
我们以切面的自动配置类AopAutoConfiguration类来讲解一下按需加载的原理。首先看到该类上面注解了@ConditionalProperty,代表了配置文件中有给定值的时候才生效;如果要看容器中是否添加切面组件,则要看其内的方法,例如红框标注的注解标示只有当前工程的上下文路径中存在Advice类时才会生效,而默认情况下我们当前路径中没有配置该类,因此Aop组件不会加载到IOC容器中。
那么何时生效呢?当我们写代码时用到了切面,必然会通过import导入aspectj这个包,此时我们当前类路径下就必然出现Advice类,从而AOP自动配置类就会生效,在IOC容器初始化时加载IOC组件,这就是按需加载。
配置绑定:
如果某一个自动配置类按需加载时,根据@Conditinal判断生效,则就会对该自动配置类对应的组件进行配置绑定。以批处理为例,当我们代码中用到了批处理,就会import相关的jar包或在容器中创建相应的bean组件,从而按需加载BatchAutoConfiguration这一自动配置类。
该自动配置类中有一个@EnableConfigurationProperties注解(如下图所示),该注解有两个作用:①是开启指定xxxProperties类的配置绑定功能②是将该xxxProperties类注册到容器中,相当于在该类上面加了@Component,我们进入对应BatchProperties中查看源码:
可以看到该类中通过@ConfigurationProperties注解,该注解的作用就是将批处理组件的属性与核心配置文件中的字段进行了绑定,从而实现了配置绑定。上面说的比较详细,如果做一个粗略的总结。
自动配置流程总结:
- 主程序入口:Spring Boot启动的时候会先找到主程序上的@SpringBootApplication注解,该注解三个注解的合成注解
- 加载指定路径的配置文件(其内写死了所有组件的自动配置类):其底层是通过@EnableAutoConfiguration注解找到所有jar包的META-INF/spring.factories配置文件中的所有自动配置类(主要是找到spring-boot-autoconfigure jar包内的该文件),并对其进行加载
- 按需加载:而这些自动配置类都是以xxxAutoConfiguration结尾来命名的,它们实际上就是一个个JavaConfig形式的容器组件的自动配置类,这些自动配置类是否生效取决于它们上面的@Conditinal注解(这叫按需加载)
- 配置绑定:针对按需加载中生效的自动配置类,才进行属性绑定。自动配置类上使用@EnableConfigurationProperties注解,激活对应的以Properties结尾命名的类,这些Properties类会通过@ConfigurationProperties注解获得在全局配置文件中配置的属性如:server.port等
- IOC容器初始化,根据上面绑定好的配置,按需加载所有自动配置类生效的组件,从而完成了自动配置。
原理图:
这里我在学习过程中看到了王福强老师的博客,他绘制了一张SpringBoot自动配置的详细流程图,我认为画的比我详细比我好,我放在开头供大家学习参考:
图片出处与博客原文:王富强老师博客:Spring Boot Rock’n’Roll!
既然王老师的上图非常详细的从Spring类的角度绘制了自动配置底层原理图,那我就换个角度,并且简略一些,从加载顺序的角度绘制一张自动配置过程原理图吧!
自动配置过程原理图
Summary:本章是SpringMVC核心功能的源码解析和原理讲解
四、SpringMVC功能源码&原理图解除了自动配置与依赖管理外,SpringBoot作为“框架的框架”,其在开发中实现的具体功能大多是通过SpringMVC实现的,因此本章我们来看一下SpringMVC功能原码。
1.静态资源配置原理 1.1静态资源访问路径 在SpringBoot中,允许直接访问静态资源,但这些静态资源必须放在指定包下:
①classpath:/META-INF/resources/
②classpath:/resources/
③classpath:/static/
④classpath:/public/
这个功能是如何实现?能否禁用?为什么是指定这些包?下面我们来看一下原理。
首先,SpringMVC相关功能是通过WebMvcAutoConfiguration自动配置类来配置的,其加载原理在上文自动配置中已经详述。我们看一下该类上的多个@Conditional注解,依次要求:Servlet类型、DispatcherServlet相关类文件、无用户定制的WebMvcConfigurationSupport类(用来全面接管SpringMVC),满足以上要求则自动配置类生效。
WebMvc自动配置类生效后,我们关注其源码,WebMvcAutoConfiguration自动配置类中有一个静态内部类WebMvcAutoConfigurationAdapter:
@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class}))
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final Resources resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider messageConvertersProvider;
private final ObjectProvider dispatcherServletPath;
private final ObjectProvider> servletRegistrations;
private final WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private ServletContext servletContext;
这里先介绍一下xxxAdapter,在源码中多处应用到了xxxAdapter,这是典型的 设计模式:适配器模式,SpringBoot源码中多用它来兼容多个实现不同接口的类。比如这里就是实现了接口WebMvcConfigurer, ServletContextAware。适配器模式基本介绍如下:
适配器模式
● 背景:已有Target接口,和待适配对象Adaptee,需要使用Adaptee提供的功能,但是无法通过Target接口去调用
● 定义一个适配器类Adapter,实现Target接口,继承Adaptee类(或者令Adaptee成为它的成员变量)
● 实现Target接口提供的方法,实际上调用Adaptee中的方法
● 使用时创建Adapter对象即可通过Target接口调用Adaptee种的方法了
回到WebMvcAutoConfigurationAdapter源码中,可以看到该类也是一个配置类,且其上注解声明了激活两个属性类的属性绑定:WebMvcProperties.class、ResourceProperties,因此我们查看这两个类源码
WebMvcProperties类与核心配置文件的spring.mvc属性进行了绑定
ResourceProperties类与核心配置文件的spring.resources属性进行了绑定(新版中改为了WebPropertie绑定spring.web)
原理解析:
静态资源配置的具体原理是WebMvcAutoConfigurationAdapter内的核心方法:addResourceHandlers(),其源码如下:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
this.addResourceHandler(registry, "/webjars
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
在正式解析原理之前,我们要明确一点,自定义类型参数封装注入的前期流程和一般参数是一样的,只是采用的参数解析器不同。
原理解析:
自定义类型参数所使用的参数解析器是ServletModelAttributeMethodProcessor,我们依然关注该参数解析器的两个接口方法,首先看一下它的supportsParameter()方法,查看它支持处理哪种类型的参数:
public boolean supportsParameter(MethodParameter parameter) {
//关注最后一个判断是否是简单属性,非简单属性则返回true代表能处理
return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
该方法的逻辑为,首先判断是否标注了==@ModelAttribute==注解,如果没标注并且注解不是必须的,则判断是否是简单属性,非简单属性则返回true代表能处理。因此,其supportsParameter()表示该参数解析器(实际上包括ModelAttributeMethodProcessor父类下的所有子类)可以处理自定义类型参数。具体简单类型包括哪些,我们也可以查看源码确认:
public static boolean isSimpleValueType(Class> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
显然我们自定义类型不属于上述简单类型,因此判断可解析。接下来我们关注它的另一个接口方法resolveArgument()是如何进行解析的:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance(核心逻辑在这开始!)
//这里创建了一个初始为空的自定义类型的实例(空Person)
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
//创建数据绑定器,将请求中传入的自定义类型对象参数传入到创建的空attribute中
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
首先尝试获取@ModelAttribute注解,若存在则将注解内容绑定到mavContainer中。若不存在,则会通过createAttribute创建一个空的自定义类型对象(如创建一个空Person对象)attribute = createAttribute(name, parameter, binderFactory, webRequest)。之后,创建了一个数据绑定器WebDataBinder,它的作用就是将请求参数的值绑定到attribute中,进而绑定到JavaBean内。
我们Debug看一下WebDataBinder的结构如下图所示:
WebDataBinder工作原理:WebDataBinder实际上是通过其转换服务conversionService中的诸多转换器converters将请求数据转换成指定类型的。为什么有这么多转换器呢?这是因为,传输中我们默认使用的是HTTP协议,传输的数据默认是字符串类型,需要通过调用ConversionService里的某一个converter方法将协议中的id之类的数据转成Integer等类型。因此GenericConversionService就是在设置每一个值的时候调用canService()方法,该方法遍历所有converter哪个可以将当前数据类型(如请求携带的字符串类型参数ID,值为“10”)转换到指定的类型(如JavaBean中的Integer类型id,值为10),转换后经过复杂的层层封装和反射工具,最终调用自定义bean对象的set方法为对应属性赋值。
设计模式:策略模式:
我们在ArgumentsResolver、WebDataBinder以及ReturnValueHandler(下一章讲)中都见到了遍历所有底层组件(分别遍历了解析器、转换器、处理器)看谁能执行当前处理(处理参数、处理类型转换、处理返回值),就调用对应类的处理方法。这其实是典型的策略模式。此外,接口的设计也是典型的策略模式,不同应用对象实现同一行为可采用不同方式。其基本概念如下:
策略模式
● 背景:在不同场景下使用不同的方法解决同一问题。
● 概念:属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同>接口的独立的类中,从而使得它们可以相互替换。简单的说,策略模式定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换
● 实现:策略模式把对象本身和运算规则区分开来,因此我们整个模式也分为三个部分:
1.环境类(Context):用来操作策略的上下文环境。
2.抽象策略类(Strategy):一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
3.具体策略类(ConcreteStrategy):具体的策略实现。
原理图:
自定义类型参数解析原理图
上文我们介绍了请求映射的过程原理,当浏览器发送一个请求给后端,该请求首先映射匹配到一个处理器方法,又将请求中的参数与处理器方法的传入参数进行了绑定,之后通过反射调用目标方法,最后应当将方法执行结果返还给前端页面,这个过程就是响应处理。
响应处理的处理内容包括两部分:数据和视图,也就是ModelAndView,其中数据响应过程中需要依据客户端(浏览器)接收能力不同和服务端(后端程序)可产生的数据格式进行内容协商,决定数据传输的格式。因此,我们本章分为以下三个小结来介绍。
在正式开始讲解数据相应原理之前,我们先提一下前后端分离的问题,因为在这个问题直接影响到我们响应处理的内容。传统开发中,前端使用html、css、js等技术来显示后台数据,或者使用视图模板如JSP、thymeleaf等,需要我们同时返还数据和视图,或者说返回包含数据的视图;但是在前后端分离的开发模式中,往往前端可能使用Vue、React,甚至可能部署在安卓APP或H5中,此时后端往往只需要返回json格式的数据,前后端约定好接口规范,分别开发。
数据相应指的是调用目标方法后得到返回值如何处理的过程。具体来讲,如果后端只需返回json数据,我们往往可以使用@ResponseBody注解,但其本质也是通过返回值处理器来处理的,如果要返回json格式数据,只需要引入jackson或fastjson的jar包即可。具体代码应用举例,如下图所示:
原理解析:
涉及原理,我们前文说到SpringMVC相关功能的起点都是DispatcherServlet类的doDispatch()方法,其内依次调用handle()、handleInternal()、invokeHandlerMethod() 方法(同请求处理部分),在invokeHandlerMethod()方法中封装了默认的参数解析器和返回值处理器:
我们通过Debug查看一下当前项目中的返回值处理器有哪些:
我们讲解一下常见返回值处理器的功能:
| 类型 | 名称&功能 | 支持的返回值类型 |
|---|---|---|
| 组合模式 | HandlerMethodReturnValueHandlerComposite 组合模式,调用其它返回值处理器 | 内部引用的返回值处理器支持的返回类型 |
| 写入httpHeader | HttpHeadersReturnValueHandler 写入到response的header | HttpHeaders返回类型 |
| 写入httpBody | RequestResponseBodyMethodProcessor 写入到response的body | 1.@RequestBody标注的参数 2.@ResponseBody标注方法 |
| 写入httpBody | HttpEntityMethodProcessor 写入到response的body | 1.HttpEntity和RequestEntity类型的参数 2.HttpEntity和ResponseEntity返回类型 |
| 写入springModel | ModelAttributeMethodProcessor 写入model | 1.@ModelAttribute标注的参数 2.@ModelAttribute标注的方法 3.所有非简单类型/简单类型数组(annotationNotRequired=true) |
| 写入springModel | ModelMethodProcessor 写入model | 1.Model类型参数(没有注解)2.Model返回类型 |
| 写入springModel | MapMethodProcessor 写入model | 1.Map类型参数(没有注解) 2.Map返回类型 |
| 处理异步结果 | AsyncHandlerMethodReturnValueHandler 自定义异步返回值处理器接口 | 支持自定义的异步返回类型 |
| 处理异步结果 | ResponseBodyEmitterReturnValueHandler 封装成DeferredResult,处理异步结果 | 支持自定义的异步返回类型 |
| 处理异步结果 | AsyncHandlerMethodReturnValueHandler 自定义异步返回值处理器接口 | 1.ResponseBodyEmitter/SseEmitter(异步)返回类型 2.Reactive返回类型(Spring5引入的响应式编程) 3.ResponseEntity返回类型(泛型是以上2种类型) |
| 处理异步结果 | DeferredResultMethodReturnValueHandler 封装成DeferredResult,处理异步结果 | 1.DeferredResult返回类型 2.ListenableFuture返回类型 3.CompletionStage返回类型 |
| 处理异步结果 | AsyncTaskMethodReturnValueHandler 执行WebAsyncTask中的Callable异步任务,处理异步结果 | WebAsyncTask返回类型 |
| 处理异步结果 | StreamingResponseBodyReturnValueHandler把StreamingResponseBody封装成Callable,然后执行Callable异步任务,处理异步结果 | 1.StreamingResponseBody返回类型 2.ResponseEntity返回类型(泛型是StreamingResponseBody) |
| 处理异步结果 | CallableMethodReturnValueHandler 执行Callable异步任务,处理异步结果 | Callable返回类型 |
需要注意的是,上述返回值处理器中,除了处理异步结果的返回值处理器和写入HttpHeader的处理器外,其他的写入Model和写入HttpBody的返回值处理器都是符合类型,同时属于返回值处理器和参数解析器。例如RequestResponseBodyMethodProcessor这个处理器,该类(Processor结尾的类)比较特殊,既实现了返回值处理器顶层接口,也实现了参数解析器顶层接口;因此在参数解析器中也有该类,用来处理@RequestBody注解的参数,可直接获取POST请求中封装在请求体内的json数据(字符串);而返回值处理器中也有该类,用来处理@ResponseBody注解的方法,直接将返回值封装到响应体中。
tip:大多数以Processor结尾的同属于参数解析器和返回值处理器
同样我们还是关注一下返回值处理器的顶层接口,其内和参数解析器的结构一模一样,两个接口方法,分别用来判断可接收的返回值类型和具体的处理返回值方法:
设置好默认的参数解析器和返回值处理器后,调用invokeForRequest()方法进行参数注入并执行目标方法,得到方法返回值,如下图所示:
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
//参数处理、执行目标方法,得到返回值returnValue
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
//参数处理、执行目标方法,设置响应状态
this.setResponseStatus(webRequest);
if (returnValue == null) {
if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {
this.disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(this.getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
//数据响应的核心方法!
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception var6) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(this.formatErrorForReturnValue(returnValue), var6);
}
throw var6;
}
}
其核心方法是handleReturnValue(),我们查看方法源码:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//策略模板:遍历所有返回值处理器调用supports方法查找支持处理当前返回值类型的处理器
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
//调用支持的返回值处理器的处理方法
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
首先通过selectHandler()方法判断哪个返回值处理器能处理当前返回值类型,查看源码:
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = this.isAsyncReturnValue(value, returnType);
Iterator var4 = this.returnValueHandlers.iterator();
HandlerMethodReturnValueHandler handler;
do {
do {
if (!var4.hasNext()) {
return null;
}
handler = (HandlerMethodReturnValueHandler)var4.next();
} while(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler));
} while(!handler.supportsReturnType(returnType));
return handler;
}
果不其然,又是典型的策略模式,首先判断一下是不是异步的返回值,如果是则调用异步返回值处理器;然后通过循环遍历所有其余的返回值处理器调用supportsReturnType()方法查看哪一个返回值处理器能处理当前类型的返回值。找到支持的返回值处理器后返回,调用其处理方法:
@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.
//核心方法调用Converter转换为最佳媒体类型
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
该方法的核心是通过调用MessageConverters实现返回值类型转换为最佳媒体类型。所谓媒体类型就是写入到HTTP协议数据域中的数据类型,如json或xml等。这个方法的源码非常长,而且不同返回值处理器的实现也有所差异,因此这里我给大家详细叙述一下其代码逻辑:
- 调用writeWithMessageConverter:
该方法首先会判断响应头中是否有ContentType字段(即标注了响应应该以什么形式返回),如果要实现这一功能会利用拦截器做,若有则直接使用字段指定的媒体类型返回。默认这里响应头为空,为空的话则需要进行内容协商(即寻找最佳媒体类型) - 内容协商:(大致原理,详解在第二小节)
①确定浏览器可接收的媒体类型:getAcceptableMediaType().浏览器会以请求头或携带参数的方式告诉服务器他能接受什么样的内容类型,具体是请求头中的Accept字段,其中q=0.9/0.8代表权重,权重越大则代表优先接受该类型;**代表的是任意格式数据(包括json)。虽然可接收格式有很多,但其中会有一个q值,它代表的是quality被称为品质因数,实际上也就是权重,该值越高代表浏览器更希望接收此类型。例如上图中显示,针对一个方法返回值是自定义类型pojo对象,则在服务器能产生xml和json两种媒体类型的情况下,会优先转换成xml发送。
再来介绍一下基于参数的策略。 由于默认使用的基于请求头策略是由浏览器q值决定浏览器期望媒体类型的,而在实际项目开发中,为了方便内容协商,我们可以使用请求参数来决定客户端优先接收的内容类型,需要开启基于请求参数的内容协商功能。#application.yml中添加: spring: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式并且在发送请求时携带上format字段(默认是format字段,也可自定义更改)如下所示:
http://localhost:8080/test/person?format=json http://localhost:8080/test/person?format=xml
基于参数策略的原理是通过原生request.getParameter() 方法,参数名默认是format,获取该参数后进行解析确认浏览器可接收类型。如果想要修改,可以通过重写ContentNegotiationManager中的策略来实现自定义,或通过setParametersName更改。
经过以上步骤,内容协商管理器ContentNegotiationManager通过某一种内容协商策略ContentNegotiationStrategy确定了浏览器可接收的媒体类型,下一步就是确认服务器可产生的媒体类型,具体方法为getProducibleMediaType():
protected List
getProducibleMediaTypes(HttpServletRequest request, Class> valueClass, @Nullable Type targetType) { Set mediaTypes = (Set)request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList(mediaTypes); } else if (this.allSupportedMediaTypes.isEmpty()) { return Collections.singletonList(MediaType.ALL); } else { List result = new ArrayList(); Iterator var6 = this.messageConverters.iterator(); //遍历messageConverters调用canWriter()方法 while(true) { while(var6.hasNext()) { HttpMessageConverter> converter = (HttpMessageConverter)var6.next(); if (converter instanceof GenericHttpMessageConverter && targetType != null) { //canWriter()方法返回true则添加到List中 if (((GenericHttpMessageConverter)converter).canWrite(targetType, valueClass, (MediaType)null)) { result.addAll(converter.getSupportedMediaTypes()); } } else if (converter.canWrite(valueClass, (MediaType)null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } } } 该方法中遍历了所有messageConverter消息转换器,逐个调用canWriter()方法获取服务器端能产生的所有媒体类型,所谓“能产生”指的其实就是当前方法返回值可转换的,并将其放入一个List中。
接下来,根据获取到的浏览器媒体类型和服务器媒体类型进行双重循环遍历,寻找所有匹配的媒体类型,匹配指的是既属于浏览器可接收同时又属于服务器可产生的媒体类型,具体判断逻辑是当某一项同时存在两个list当中即匹配成功(如json类型既属于*/*,又属于服务器MappingJackson2HttpMessageConverter可输出类型),则将其加入到匹配列表中
最后,由于匹配列表中很可能存在多项匹配的媒体类型,如浏览器请求头支持多种媒体类型,而服务器也有多种返回值处理器可返回多种媒体类型,匹配类型就有多个,需要对匹配列表进行排序!排序的依据取决于内容协商时使用的协商策略。如使用的是基于请求头的策略,则排序策略是请求头header-Accpet字段中的q值(即quality权重),若使用基于请求参数的策略,则直接选择参数指定的媒体类型。至此,获得了最佳媒体类型,内容协商过程结束。原理图:
3.3视图解析
参数解析原理图上文我们说过,在前后端分离的项目中,通常后端只需要返回指定格式的数据,前端使用相关技术进行页面渲染和处理。但是在传统开发或非大型项目中,我们会将数据和视图一起返回或直接跳转到指定页面,此时都需要使用到视图解析技术。
先来看一下视图解析的概念,视图解析其实就是SpringBoot某一个controller的方法执行完成之后,执行页面跳转的操作。需要注意的是,SpringBoot默认打包方式是jar包,jsp不支持在压缩包内编译的方式,因此默认不支持,需要引入第三方模板引擎才可以处理(常用的有模板引起包括:Thymeleaf、FreeMarker、Velocity、JSP等)。原理解析:
再次明确一点,SpringMVC的所有功能都是从doDispatch()方法开始的,依次经过请求映射、参数处理、目标方法执行,之后会得到返回值(这一步同数据处理),只不过视图解析中,我们得到的返回值是一个字符串。通常该字符串的值表示我们要跳转的页面,可能只包含页面名称(如"index"),这种方式一般是配置了视图解析器前后缀,并且以转发形式进行跳转;除此以外,还可以重定向方式来跳转(如“redirect:/index.html”)。得到方法返回之后,依然是去遍历寻找支持的返回值处理器,这里返回值类型是字符串最终找到了ViewNameMethodReturnValueHandler,我们来看一下原因://ViewNameMethodReturnValueHandler返回值处理器的supportsReturnType方法如下: public boolean supportsReturnType(MethodParameter returnType) { Class> paramType = returnType.getParameterType(); return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); }因为该返回值处理器的supportsReturnType方法中判断,只要是字符串类型都支持解析,因此视图解析中会返回该处理器。之后还是调用该处理器的handleReturnValue()方法,这段源码和数据响应一致:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } else { handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } }我们重点关注该处理器的的handleReturnValue()方法源码:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof CharSequence) { //返回值转String String viewName = returnValue.toString(); //返回值存入ModeAndViewContainer中 mavContainer.setViewName(viewName); if (this.isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null) { throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } }首先,将方法返回值转成String,它代表的是目标视图地址,之后将数据和视图地址全部放入ModeAndViewContainer中,视图地址存入其View属性中,数据存入Model属性中。之后会判断当前视图地址是否是重定向地址,查看其判断方法的源码:
protected boolean isRedirectViewName(String viewName) { return PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"); }实际就是解析判断路径是否以redirect起始;如果是的话则设置RedirectModelScenario(直译为转发模型传感器)为true(个人理解相当于一个标志位,看到该标志位后底层调用原生重定向方法response.sendRedirect())。
至此返回值处理器的工作结束,返回到invokeHandlerMethod()方法中,该方法最终会获取ViewNameMethodReturnValueHandler(或其他返回值处理器)封装的ModelAndView对象,如下图所示:
我们查看一下ModelAndView对象的结构:
可以看到,返回值处理器操作的数据和视图地址都放进了mav对象中,如果方法的传入参数包含自定义类型对象pojo(从请求参数中确定的),则会将pojo也放在 ModelAndViewContainer中(封装在model内)。
因此,在invokeHandlerMethod()方法中显示:任何目标方法执行完成以后都会返回ModelAndView,就算目标方法没有返回值底层也会设置默认的数据和视图,默认跳转的视图地址是通过原生request请求中拿到的请求路径。(即跳转回原路径)
至此,doDisptach()方法中ha.handle()方法执行完成,之后进行处理派发结果processDispatchResult(),它决定了页面该如何响应
我们来查看一下处理派发结果的源码:private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { //判断是否处理过程存在失败步骤 boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { this.logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException)exception).getModelAndView(); } else { Object handler = mappedHandler != null ? mappedHandler.getHandler() : null; mv = this.processHandlerException(request, response, handler, exception); errorView = mv != null; } } //ModelAndView不为空,即有实际返回值 if (mv != null && !mv.wasCleared()) { //视图渲染:核心方法 this.render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else if (this.logger.isTraceEnabled()) { this.logger.trace("No view rendering, null ModelAndView returned."); } if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, (Exception)null); } } }该方法首先判断整个处理过程是否存在异常和错误,之后当ModelAndView不为空时进行处理,核心方法为render(),我们查看源码:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { //国际化相关参数 Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale(); response.setLocale(locale); //获取之前存入mv对象的View属性即视图名 String viewName = mv.getViewName(); View view; if (viewName != null) { //解析得到视图对象:核心方法 view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + this.getServletName() + "'"); } } else { view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'"); } } if (this.logger.isTraceEnabled()) { this.logger.trace("Rendering view [" + view + "] "); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception var8) { if (this.logger.isDebugEnabled()) { this.logger.debug("Error rendering view [" + view + "]", var8); } throw var8; } }源码中首先获取到之前存入ModeAndView中的View即视图名,之后由resolveViewName()方法获取到视图对象,查看该方法源码:
protected View resolveViewName(String viewName, @Nullable Map
model, Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { Iterator var5 = this.viewResolvers.iterator(); //遍历所有的视图解析器尝试是否有能根据当前返回值得到View对象的视图解析器 while(var5.hasNext()) { ViewResolver viewResolver = (ViewResolver)var5.next(); View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; } 又是熟悉的遍历过程,这里是遍历所有的视图解析器寻找是否有能解析当前返回值的视图解析器,即根据当前返回值得到View对象的视图解析器。
本项目中有五个视图解析器,如遍历找到了第一个ContentNegotiationViewResolver。它比较特殊,里面包含了下面四个视图解析器,内部还是遍历下面所有视图解析器判断哪个可解析。我们查看下每个视图解析器
分类 名称 功能 内容协商视图解析器 ContentNegotiationViewResolver 用于进行内容协商,其内保存了浏览器可接收的媒体类型,之后遍历其他视图解析器寻找最佳视图 自定义视图解析器 BeanNameViewResolver 用于查找IOC容器中是否存在返回值名称的组件,并返回自定义的视图。通过配置后,控制器返回自定义的视图的bean名,即可返回自定义的视图。 模板引擎视图解析器 ThymeleafViewResolver 配置后用于解析并返回Thymeleaf视图 内部资源视图解析器 InternalResourceViewResolver 把返回的视图名称都解析为InternalResourceView对象,InternalResourceView会把Controller处理器方法返回的模型属性都存放到对应的request属性中,然后通过RequestDispatcher在服务器端把请求forword到目标URL。一般会用来解析JSP视图。 我们查看一下遍历部分的源码:
if (requestedMediaTypes != null) { ListcandidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes); View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } 依次获取候选视图candidateViews和最佳视图bestView,我们依次查看源码:
private List
getCandidateViews(String viewName, Locale locale, List requestedMediaTypes) throws Exception { List candidateViews = new ArrayList<>(); if (this.viewResolvers != null) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); // 遍历视图解析器,上面有张图显示,在第1个视图解析器里面包含另外的4个视图解析器 for (ViewResolver viewResolver : this.viewResolvers) { // 视图解析器处理视图 View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { // 处理结果不为空则存入候选的视图 candidateViews.add(view); } // 内容协商MediaType提取扩展名加载View for (MediaType requestedMediaType : requestedMediaTypes) { List extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType); for (String extension : extensions) { String viewNameWithExtension = viewName + '.' + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); } } } } } // 判断是否添加默认视图 if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews; } 源码显示获取候选视图getCandidateViews() 分为两个步骤:
- 调用各个ViewResolver中的resolveViewName()方法解析获得View对象。
- 根据MediaType提取出扩展名,再根据扩展名去加载View对象
在实际应用中,第二步我们都很少去配置,所以基本上是加载不出来View对象的,主要靠第一步。第一步去加载View对象,其实就是根据viewName,再结合ViewResolver中配置的prefix、suffix、templateLocation等属性,找到对应的 View,方法执行流程依次是 resolveViewName()->createView()->loadView()(返回值是普通字符串)。接下来调用模板引擎解析器的resolveViewName(),其内调用createView(),我们查看一下该方法的源码:
protected View createView(String viewName, Locale locale) throws Exception { if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) { vrlogger.trace("[THYMELEAF] View "{}" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName); return null; } else { String forwardUrl; //视图名以redirect开头,则创建RedirectView重定向视图 if (viewName.startsWith("redirect:")) { vrlogger.trace("[THYMELEAF] View "{}" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName); forwardUrl = viewName.substring("redirect:".length(), viewName.length()); RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible()); return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName); //视图名以forwad开头,则创建InternalResourceView视图 } else if (viewName.startsWith("forward:")) { vrlogger.trace("[THYMELEAF] View "{}" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName); forwardUrl = viewName.substring("forward:".length(), viewName.length()); return new InternalResourceView(forwardUrl); } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) { vrlogger.trace("[THYMELEAF] View "{}" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName); return null; } else { vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName()); return this.loadView(viewName, locale); } } }可以看到createView()方法中针对redirect:/开头和forward开头的字符串都创建了对应的视图并返回,针对都不包含的普通字符串则进一步调用loadView() 方法:
redirect:/开头——>new RedirectView() ;
forward:/开头——>new InternalResourceView()
普通字符串——> loadView()——>new TheameleafView()当返回值结果是普通字符串时会继续调用loadView()方法,我们来看下这个方法:
protected View loadView(String viewName, Locale locale) throws Exception { AbstractUrlBasedView view = buildView(viewName); View result = applyLifecycleMethods(viewName, view); return (view.checkResource(locale) ? result : null); }在这个方法中,View加载出来后,会调用checkResource方法判断View是否存在,如果存在就返回View,不存在就返回null。
小结一下候选视图的获取依据:遍历视图解析器依次调用resolveViewName()。解析视图名、创建视图、加载视图,并检查视图资源存在,即最后解析结果不为空时才将当前视图解析器解析的视图加入到候选列表中。
再来看一下最佳视图getBestView() 方法,从所有的候选View中找到最佳的 View。getBestView方法的逻辑比较简单,就是查找看所有View的MediaType,然后和请求的MediaType数组进行匹配,第一个匹配上的就是最佳 View,这个过程它不会检查视图是否真的存在,所以就有可能选出来一个压根没有的视图,最终导致 404。private View getBestView(List
candidateViews, List requestedMediaTypes, RequestAttributes attrs) { Iterator var4 = candidateViews.iterator(); while(var4.hasNext()) { View candidateView = (View)var4.next(); if (candidateView instanceof SmartView) { SmartView smartView = (SmartView)candidateView; if (smartView.isRedirectView()) { return candidateView; } } } var4 = requestedMediaTypes.iterator(); while(var4.hasNext()) { MediaType mediaType = (MediaType)var4.next(); Iterator var10 = candidateViews.iterator(); while(var10.hasNext()) { View candidateView = (View)var10.next(); if (StringUtils.hasText(candidateView.getContentType())) { MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType()); if (mediaType.isCompatibleWith(candidateContentType)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes); } attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, 0); return candidateView; } } } } return null; } } 起初,我不太理解View视图中为什么会有MediaType,查阅了官方文档和其他博客后初步理解了。其在ContentNegotiatingViewResolver类中配置了一个mediaTypes这个属性,它代表的其实就是和媒体类型对应的View类型!匹配时,根据请求的后缀名和请求头类型来决定返回什么样的View。而mediaTypes这个属性存储了 请求后缀名或者参数所对应的媒体类型。(如要想返回JSON数据所代表的MappingJacksonJsonView ,我们要么在请求头中设置contentType为application/json,要么使用 **.json 或者 **?format=json )
小结一下最佳视图的获取依据:遍历候选视图的MediaType(视图支持的媒体类型)和请求中的MediaType数组,最先匹配上(出现在MediaType数组中)的候选视图即为最佳视图。
(这部分参考了知乎某文介绍候选视图与最佳视图获取
和CSDN某文《解析mediaTypes+viewResolvers+view》)经过以上两步,我们获取到了最佳视图,之后调取最佳视图view.render()方法,这一步也是因视图不同会有所差异的,我们以ThymeleafView为例看一下视图渲染的大致逻辑。首先会创建一个HashMap用来存放Model中的数据:
//创建HashMap存放Model中的数据;创建Map存放静态变量 Map
mergedModel = new HashMap(30); Map templateStaticVariables = this.getStaticVariables(); if (templateStaticVariables != null) { mergedModel.putAll(templateStaticVariables); } if (pathVariablesSelector != null) { Map pathVars = (Map)request.getAttribute(pathVariablesSelector); if (pathVars != null) { mergedModel.putAll(pathVars); } } if (model != null) { //存入所有model数据 mergedModel.putAll(model); } 之后设置响应头内容类型ContentType和编码CharacterEncoding;再调用模板引擎的process()方法,其内通过writer.flush() 使用字节流将页面的内容刷到缓冲区中,进而在浏览器上显示。
那么其他视图如何渲染呢?其实redirect开头和forward开头就是分别调用RedirectView和InternalResourceView的解析方法,其内还是调用原生sendRedirect()和forward() 进行视图跳转的。
视图解析流程总结:
(由于视图渲染设计方法逐级调用太多,所以流程逻辑间有些交错,使用多级列表显示)-
方法返回值为字符串类型
通常该字符串的值表示我们要跳转的页面,可能包含forward:或redirect:或只包含页面名称 -
遍历寻找支持的返回值处理器
依然是逐个处理器调用supportsReturnType,字符串类型最终找到ViewNameMethodReturnValueHandler -
调用对应处理器的handleReturnValue()方法
将方法返回值转成String,它代表的是目标视图地址放入View属性中,数据存入Model属性中,及存入ModeAndViewContainer中- ModeAndViewContainer处理时判断是否为重定向?
实质是判断路径是否以redirect起始。如果是的话则设置RedirectModelScenario为true(个人理解相当于一个标志位)
- ModeAndViewContainer处理时判断是否为重定向?
-
得到ModelAndView
如果方法的传入参数包含自定义类型对象pojo(从请求参数中确定的),则会将pojo也放在 ModelAndViewContainer中(封装在model内)。任何目标方法执行完成以后都会返回ModelAndView,就算目标方法没有返回值底层也会设置默认的数据和视图,默认跳转的视图地址是通过原生request请求中拿到的请求路径。 -
处理派发结果
执行processDispatchResult()方法,该方法决定了页面如何响应- 方法逐级调用:
processDispatchResult()
—>DispatcherServlet.render()
——>获取mv中的View调用resolveViewName() - 遍历寻找可解析的视图解析器
遍历视图解析器尝试是否有能根据当前返回值得到View对象的视图解析器- 优先遍历ContentNegotiationViewResolver
getCandidateViews获取候选视图+getBestView获取最佳视图 - 先获取候选视图
方法内遍历其余视图解析器调用resolveViewName解析方法,若返回view不为空则加入候选视图 - 候选原理:创建视图并返回
其余视图解析器createView()。判断:以redirect开头,则创建RedirectView、以forwad开头,则创建InternalResourceView、普通字符串则进一步调用loadView()——>创建TheameleafView(模板引擎视图) - 再获取最佳视图:
getBestView()从所有的候选View中找到最佳的View:查找看所有View的MediaType,然后和请求的MediaType数组进行匹配,第一个匹配上的就是最佳 View,这个过程它不会检查视图是否真的存在,所以就有可能选出来一个压根没有的视图,最终导致 404
- 优先遍历ContentNegotiationViewResolver
- bestview.render()
调取最佳视图渲染方法- 获取Model中的数据
(如果有需要返回的数据可以通过url地址后?拼接参数的形式返回) - 获取View中目标url地址
- 底层调用页面跳转
方法返回值字符串判断:
①redircet开头:调用response.sendRedirect(encodedURL);
②forward开头:调用request.getRequestDispatcher(encodedURL).forward();
③普通字符串:通过writer.flush()方法将包含model数据的最终页面html代码写入到缓冲区中,发送到浏览器并显示
- 获取Model中的数据
- 方法逐级调用:
原理图:
4.异常处理(待更新)
视图解析原理图待更新…
五、参考资料文中小段引用的文字与图片已在原文引用处注明链接,此处为学习与写作过程中的参考资料:
- 视频资料:雷神SpringBoot2视频(尚硅谷) 雷神SpringBoot2视频(b站)
- 官方文档:SpringBoot官方网站
- 中文文档: 个人翻译版官方文档
- 学习源码:雷神教程配套源码
- 文档笔记:尚硅谷语雀文档-SpringBoot
- 书籍:《深入浅出SpringBoot2.x》
- 实战系列教程:CSDN——《SpringBoot从入门到精通教程》
- 其他博客:
①王老师详解SpringBoot:王富强老师个人博客——《Spring Boot Rock’n’Roll!》
①详解前后端分离:CSDN——《前后端分离架构概述》
②SpringBoot启动过程原理:CSDN——《SpringBoot启动原理及相关流程》
③SpringMVC使用与案例:CSDN——《实例详解Spring MVC入门使用》
④SpringMVC深度探险系列文章:ITeye——《SpringMVC深度探险1-5》
联系作者指正或交流,可点击作者头像私信,或评论留言~



