一、前言
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官方网站首页上摆着的:
![图片无法显示](https://img-blog.csdnimg.cn/039ab21d48164255922e6cbddeaf57e7.png)
Spring功能图
更多特性与功能请参考:官方文档-OverView
2.时代背景
“时势造英雄”——SpringBoot成为时下最热门框架的背后,实际上是时代背景所驱,其框架设计之初就考虑到了当下开发中的痛点与难点,并提出了解决方案,从而被广泛应用于高性能服务端程序开发。下面让我们看看当下开发所面临的时代背景与解决方案。
2.1 大数据-背景
大数据时代是一个早已泛滥的词,在我的理解里,对于我们开发者或从业者来说大数据包含两个层面的意思:数据量大、并发量高,特点可概括为“5V”+“3高”。数据量大描述的是需要存储的数据内容和特点,并发量高是伴随用户增多而出现的现象,通常人们说的大数据主要指第一层意思即数据量大,其特点是5V。
![图片无法显示](https://img-blog.csdnimg.cn/36665b46408745dd95f7afe03d6479cd.png)
大数据5V特点
- 数据量大(Volume)
第一个特征是数据量大。大数据的起始计量单位至少是P(1000个T)、E(100万个T)或Z(10亿个T)。 - 类型繁多(Variety)
第二个特征是数据类型繁多。包括网络日志、音频、视频、图片、地理位置信息等等,多类型的数据对数据的处理能力提出了更高的要求。 - 价值密度低(Value)
第三个特征是数据价值密度相对较低。如随着物联网的广泛应用,信息感知无处不在,信息海量,但价值密度较低,如何通过强大的机器算法更迅速地完成数据的价值“提纯”,是大数据时代亟待解决的难题。 - 速度快、时效高(Velocity)
第四个特征是处理速度快,时效性要求高。这是大数据区分于传统数据挖掘最显著的特征。 - 真实(Veracity)
大数据中的内容是与真实世界中的发生息息相关的,要保证数据的准确性和可信赖度。研究大数据就是从庞大的网络数据中提取出能够解释和预测现实事件的过程。
原先既有的技术架构和路线,已经无法高效处理如此海量的数据,而对于相关组织来说,如果投入巨大采集的信息无法通过及时处理反馈有效信息,那将是得不偿失的。SpringBoot框架中可以通过原生和集成第三方技术来处理海量数据。
参考: 大数据5V特点-CSDN
2.2 微服务-架构
微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。这些服务由各个小型独立团队负责。
实际上,微服务的产生是为了应对微服务架构使应用程序更易于扩展和更快地开发,也是适应大数据时代的软件开发架构,从而实现加速创新并缩短新功能的上市时间。
![图片无法显示](https://img-blog.csdnimg.cn/2e8a741fffb4491bb33cf83ad661f2f2.png)
微服务示意图
微服务概念最早是Martin Fowler于2014年的一篇文章《Microservices – the new architectural style》中提出的,其主要特点包括:
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
原文链接:MartinFowler网站——Microservices Guide
2.3 分布式-系统
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。
![图片无法显示](https://img-blog.csdnimg.cn/588cf01439174f288a11fc9c0be6a73d.png)
分布式系统示意图
分布式系统本质上有两个目的,一是增强系统实时处理所需的算力,二是扩展系统数据存储的容量。但随之而来的,是系统结构复杂化导致的诸多问题与解决方案,包括以下几点(注解了自己对该问题和解决方案的理解):
● 远程调用:RPC,通过NIO方式处理封装调用方法参数的数据并相应执行结果
● 服务发现:使用一个注册中心来记录分布式系统中的全部服务的信息,以便其他服务能够快速的找到这些已注册的服务
● 负载均衡:通过轮询或Nginx等服务器策略分配请求降低单服务器QPS负载压力
● 服务容错:当发生网络异常或代码异常时,服务的返回结果和逻辑处理机制
● 配置管理:统一管理部署在多台服务器上的某服务的配置
● 服务监控:通过心跳等机制监控服务是否存活(网络及CPU内存等运行状态)
● 链路追踪:还原分布式服务调用过程的链路
● 日志管理:针对海量日志使用流处理,涉及Flink、kafka等大数据框架
● 任务调度:求解多任务多站点下执行时间最小化问题
以上是我自己对这些概念的简单解释,若想深度理解分布式系统可参考博客: 分布式系统概念详解及学习方法
2.4 云原生-构建
正是由于分布式应用系统使结构复杂化,才会面临以上诸多问题,解决方案也相应的较为繁琐,因此衍生了服务上云的需求。而Spring也提出了相应的框架来响应这一需求,即SpringBoot + SpringCloud。说白了,就是分布式系统服务上云,以便更加便捷的构建和管理结构复杂的高性能服务器应用。
![图片无法显示](https://img-blog.csdnimg.cn/162555e2a8d8468ca59dd909f4d5e9a3.png)
Spring云原生方案
云原生是一种构建和运行应用程序的方法,是一套技术体系和方法论。云原生(CloudNative)是一个组合词,Cloud+Native。Cloud表示应用程序位于云中,而不是传统的数据中心;Native表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。
![图片无法显示](https://img-blog.csdnimg.cn/09f3519db1fd48b9a9d590ea91fb89e9.png#pic_center)
云原生概念图
3.技术升级
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等
3.2 默认软件替换和优化
-
数据库连接池
默认连接池已从 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 有效负载得到了改进。
3.3 新技术的引入
- 支持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文件,会发现里面的很多依赖是没有写版本号的!
<!-- SpringBoot POM.xml中的依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
根据Maven规则,坐标必须由groupId,artifactId,version才能唯一确定,那为什么SpringBoot中可以省略版本呢?省略后我们又怎么知道项目中引入的redis、mybatis和lombok版本的呢?这就是Springboot中的版本仲裁机制。
原理解析:
为什么Spring Boot导入dependency时不需要指定版本?首先,在POM.xml文件的顶层声明了一个父容器,该项目下的所有模块默认继承父容器中的依赖配置。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
</parent>
tip: 按Ctrl+左键单击查看源文件/源码
这个父容器中添加的spring-boot-starter-parent依赖是什么呢?我们可以查看其底层源文件,发现该依赖又有一个顶层父容器依赖:spring-boot-dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.7</version>
</parent>
这就是版本自动控制的根源所在了,查看spring-boot-dependencies的源文件如下:
<!-- 以property属性标签的方式写死了常用组件的版本 -->
<properties>
<activemq.version>5.16.4</activemq.version>
<antlr2.version>2.7.7</antlr2.version>
<appengine-sdk.version>1.9.96</appengine-sdk.version>
<artemis.version>2.19.1</artemis.version>
……
<spring-amqp.version>2.4.4</spring-amqp.version>
<spring-batch.version>4.3.5</spring-batch.version>
<spring-framework.version>5.3.19</spring-framework.version>
<spring-kafka.version>2.8.5</spring-kafka.version>
……
</properties>
<!-- 在依赖坐标的version中引入了属性标签 -->
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-amqp</artifactId>
<version>${activemq.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-blueprint</artifactId>
<version>${activemq.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-broker</artifactId>
<version>${activemq.version}</version>
</dependency>
……
</dependencies>
其配置文件中,通过properties属性的方式写死了所有常用组件的版本号,而其底层依赖dependency中的在坐标的version中引入了上面写死的properties属性标签,从而唯一控制了所有常用组件的版本号。这就是版本仲裁的根本原理。
假设对版本仲裁的结果不满意如何修改呢?只需要在POM.xml中通过标签指定对应的依赖版本即可,原理就是根据Maven依赖的就近原则,加载时会优先使用我们声明的属性标签决定依赖版本。例如修改mysql的版本:
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
原理图:
![图片无法显示](https://img-blog.csdnimg.cn/f6af846cfa1242af906904db7d76be83.jpeg#pic_center)
版本仲裁机制原理图解
1.2场景启动器(Starter)
概念:
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源文件:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.19</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.19</version>
<scope>compile</scope>
</dependency>
</dependencies>
可以看到Web场景启动器中引入了很多新的启动器,这些启动器底层就是响应的依赖,包括json、tomcat、Spring-web和webmvc等,即包含几乎所有Web开发场景下所需的依赖,并管理了其版本和作用范围。因此,我们只需在POM.xml中引入对应场景的启动器,就可以轻松进行该场景下的开发工作,而不用关心其底层依赖。
值得关注的是,当你点开任意一个Starter(官方发布的原生starter)会发现其内都依赖spring-boot-starter,即部分场景启动器依赖于底层启动器,如spring-boot-starter,我们可以称其为“底层场景启动器”,如果我们查看Spring-boot-starter源文件:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>1.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.19</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
<scope>compile</scope>
</dependency>
</dependencies>
原来,web场景启动器的底层依赖spring-boot-starter中声明了开发所需的必要依赖,包括spring-core核心代码、yaml支持、注解API、自动配置等。那么具体哪些启动器依赖于更底层的启动器呢?我们可以通过Maven打印依赖树的方式查看,如图所示:
原理图:
![图片无法显示](https://img-blog.csdnimg.cn/9be775403aaf4721a0256990a04c28d3.jpeg#pic_center)
场景启动器原理图
更多可用的场景启动器,请参考: 官方文档:所有支持的场景启动器
2.自动配置原理
任何一个组件或依赖想要生效都分为两步:首先引入jar包,之后需要对引入的组件进行配置。上文介绍的依赖管理实现的其实就是引入并管理jar包,这一小结我们讲一讲SpringBoot为我们提供的自动配置功能。
2.1 默认扫描包结构
首先要介绍的是默认扫描的包结构。在以往的SSM框架中我们需要在web.xml中配置DispatcherServlet等组件,同时在SpringMVC.xml中配置组件扫描的包结构。而在SpringBoot中,会默认扫描主程序所在的包及其下面的所有子包中的组件,官方文档中的默认扫描包结构示意图如下图所示
![图片无法显示](https://img-blog.csdnimg.cn/9dd2f18fee5742bbbe4cf5ec4ba9f99d.png)
官方提供的默认包结构示意图
主程序即配置了 @SpringBootApplication注解的类,如果想要修改默认扫描的包结构,有两种方式:
①@SpringBootApplication(scanBasePackages=“指定包路径”)
②@ComponentScan (“指定包路径”)
需要注意的是@SpringBootApplication是一个合成注解,其内包含三个注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(“默认包路径”)因为@ComponentScan是一个不可重复的注解,因此无法在主程序类上注释@ComponentScan。
原理解析:
默认扫描包结构的原理涉及到合成注解中的@EnableAutoConfiguration注解的底层注解 @AutoConfigurationPackage,其内指定了对MainApplication主程序所在包及其子包下的所有组件进行扫描并注册到容器中,具体源码可查看2.2小结中的详细解释。
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<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
② @Import(AutoConfigurationImportSelector.class)
该注解导入AutoConfigurationImportSelector.class,该类源码较长我就不复制粘贴过来了,其功能就是使用指定路径的自动配置类向IOC容器中加载组件。简述该类的主要方法从而解析原理,该类中逐级调用方法如下:
(a)该类里核心方法是String[] selectImports(),该方法内通过调用getAutoConfigurationEntry(annotationMetadata)给容器中批量导入一些组件
(b)所有要加载到容器中的组件的全类名是通过调用如下方法获取:
List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类全类名List
(c)获取到要加载的全类名后,通过Spring工厂加载器去加载这些组件SpringFactoriesLoader.loadFactoryNames(…)
设计模式:工厂模式; 原理:反射
(d)具体是利用工厂加载器中的加载方法 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);,该方法会去获取一个指定位置的资源文件:META-INF/spring.factories
(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类的角度绘制了自动配置底层原理图,那我就换个角度,并且简略一些,从加载顺序的角度绘制一张自动配置过程原理图吧!
![图片无法显示](https://img-blog.csdnimg.cn/7308e76d9fe54c48b82f8211b9ce7477.jpeg#pic_center)
自动配置过程原理图
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<HttpMessageConverters> messageConvertersProvider;
private final ObjectProvider<DispatcherServletPath> dispatcherServletPath;
private final ObjectProvider<ServletRegistrationBean<?>> 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/**", "classpath:/META-INF/resources/webjars/");
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations())