SpringBoot源码深度解析

weixin_lizhao 2024-08-18 13:35:01 阅读 87

        今天,聊聊SpringBoot的源码,本博客聊的版本为v2.0.3.RELEASE。目前SpringBoot的最新版为v3.3.2,可能目前有些公司使用的SpringBoot版本高于我这个版本。但是没关系,因为版本越新,新增的功能越多,反而对SpringBoot源码的研究带来更多的困难,我觉得没必要刻意追求最新,只要掌握其核心流程即可,万变不离其宗。另外,前面我花了大量的时间,一共写了六篇博客,也是为了讲SpringBoot框架做铺垫,Spring/SpringMVC的原理,地址:Spring源码深度解析(上)、SpringMVC源码深度解析(上),不然直接看SpringBoot源码,会有一定难度。因为我理解的SpringBoot框架,是对Spring FrameWork框架的进一步封装。OK,话不多说,进入正题。

        先看看项目的层级目录:

73876ebf931e4da4b50494c90da3e62b.png

        依赖也很简单,如下:

<code><?xml version="1.0" encoding="UTF-8"?>code>

<project xmlns="http://maven.apache.org/POM/4.0.0"code>

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"code>

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">code>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-project</artifactId>

<version>2.0.3.RELEASE</version>

</parent>

<modelVersion>4.0.0</modelVersion>

<artifactId>my-spring-boot</artifactId>

<properties>

<maven.compiler.source>8</maven.compiler.source>

<maven.compiler.target>8</maven.compiler.target>

</properties>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>code>

<version>2.0.3.RELEASE</version>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<version>2.0.3.RELEASE</version>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-aop</artifactId>

<version>2.0.3.RELEASE</version>

</dependency>

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<version>1.18.24</version>

</dependency>

</dependencies>

</project>

        使用过SpringBoot框架的朋友都知道,SpringBoot会有一个启动类,启动类是被@SpringBootApplication注解修饰的。看看App.class的代码:

9b8194ba623f47909787a5b7c9950b92.png

        那就以SpringApplication这个类最为切入点讲解。先看@SpringBootApplication注解,代码如下:

29702f909c1b4b10a37d5c46b8417b72.png

        可以看出,@SpringBootApplication注解也是可以添加包扫描路径的,最终添加的包扫描路径会设置到@ComponentScan注解中的scanBasePackages或者scanBasePackageClasses属性中去。但是我们一般不会指定,默认扫描的包路径为:App类所在的包及其子包。然后,在@SpringBootApplication注解注解上,还添加了几个注解,分别是:@ComponentScan、@SpringBootConfiguration、@EnableAutoConfiguration等。@ComponentScan自不必说,@SpringBootConfiguration注解实际上又是被@Configuration注解修饰的,如果把@SpringBootConfiguration注解替换成@Configuration注解,也是没有任何问题的。

ae961d414be44ae99777e6b003b74ccd.png

因此,被@SpringBootApplication注解修饰的类可以直接当配置类使用,也就是可以在类中添加其它类到Spring容器中。重点来了,就是@EnableAutoConfiguration注解,这是SpringBoot实现自动装配的关键,代码如下:

a1afafecaf2f4c1caec4fbdc857f7366.png

        可以看出,@EnableAutoConfiguration注解可以排除一些类,除此之外,这个注解上面也被其他注解所修饰,分别是:@AutoConfigurationPackage注解和@Import注解。代码中的注释对@AutoConfigurationPackage注解,我说的很清楚:作用就是往Spring容器中注入BasePackages对象,该类存有扫描的包信息,用于其他框架整合SpringBoot的时候,方便获取到包扫描信息,进行它自己的扫描,需要这样操作的框架还挺多的,如Mybatis、Dubbo、Open Feign等。当然,其他框架用不用这个类是它们的事,但是SpringBoot有提供这样的方式。

3701b35aa25b4cffb2d7c2e7b6c6b2c8.png

        再看看@Import注解,熟悉Spring框架的朋友应该对这个注解很熟悉,它的作用是向Spring容器中注入@Import注解配置的Class。看看AutoConfigurationImportSelector类,代码如下:

79327716d3dc4baf907f27ea3086ab5d.png

        可以看出,AutoConfigurationImportSelector实现了DeferredImportSelector、BeanClassLoaderAware、ResourceLoaderAware、BeanFactoryAware、EnvironmentAware等接口,Spring框架在初始化AutoConfigurationImportSelector的时候,会多次调用回调方法,比如给AutoConfigurationImportSelector设置ConfigurableListableBeanFactory、Environment、ClassLoader、ResourceLoader等对象。其中,DeferredImportSelector接口很重要,根据这个接口的特点:当Spring在解析配置类的时候,当解析完这一轮配置类后,才回调用DeferredImportSelector#selectImports()方法,由于有着一个延迟解析的特点,才能实现这样一个功能:比如Servlet容器有很多种,如Tomcat、Jetty、Undertow等,默认使用Tomcat作为Servlet容器,如果此时开发人员不想用Tomcat,想用Jetty,那应该怎么做呢?很简单,引入Jetty的依赖,排除Tomcat相关依赖即可。这里涉及到ServletWebServerFactoryConfiguration类,代码如下:

6c064b732529453591b2c03cda85199a.png

        除了我刚刚说的,引入Jetty的依赖,再排除Tomcat相关依赖,可以改成使用Jetty服务器;还有一个方法也可以做到,即不用排除Tomcat的依赖,只需要再引入Jetty的依赖,在自己的配置类中添加JettyServletWebServerFactory即可。因为ServletWebServerFactoryConfiguration这个配置类是Spring在解析完程序员自定义的配置类后再解析的,因此通过@ConditionalOnMissingBean注解进行判断的时候会发现,此时Spring容器中已经有了之前注入的JettyServletWebServerFactory对象了,因此,ServletWebServerFactoryConfiguration中配置三个ServletWebServerFactory对象都不会注入到Spring容器中,最后调用ServletWebServerFactory#getWebServer()方法,得到的只有JettyWebServer,代码如下:

e8364e40cb0846059492038fbd0bc413.png

        因此可以知道,AutoConfigurationImportSelector实现DeferredImportSelector接口的作用就是保证程序员的配置大于默认配置!当然,讲到这里,其实还不是SpingBoot的自动装配,自动装配的话,还是要看AutoConfigurationImportSelector#selectImports()方法,代码如下:

ac9c347ea16440d9806dde3864bdbcf0.png

        再看看AutoConfigurationImportSelectorget#CandidateConfigurations()方法,看看它是如何获取配置类的,代码如下:

8c4b49191b5a421b8978a88619b3a37d.png

ea460fef29cb476991c06a477afde3d1.png

可以看出,上面的逻辑是,通过ClassLoader读取classpath下的META-INF/spring.factories文件,获取文件中的内容,看看spring.factories,如下:

b4f25f0df1bd4875a4d8808f323dabdd.png

        其中有一个EnableAutoConfiguration类的全限定名,而且确实方法中也传入了EnableAutoConfiguration类的全限定名,因此可以猜到:程序读取spring.factories文件,并通过EnableAutoConfiguration类的全限定名作为Key,获取对用的value,也就是截图中的一大堆类的全限定名,并返回,这就是所有待解析的配置类。其中也不乏我们熟悉的类,如:RabbitAutoConfiguration、AopAutoConfiguration、ElasticsearchDataAutoConfiguration等等。那是不是把这些配置类全部返回,并进行加载解析就行了呢?当然不行,准确的说是没必要,因为这些配置类都是要有相关的依赖,才会起作用,因此需要过滤,当然,如果通过@SpringBootApplication配置,排除配置类或者配置类名,这种也需要过滤。

b7f04f563be04a108fabf315e99997db.png

37f57ca5bced4c0e8547d5a7d798c938.png

        以上就是SpringBoot自动装备的原理。如果我们自己要写一个工具,怎么与SpringBoot整合呢?其实很简单,自己写一个项目,在META-INF目录下建一个spring.factories文件,目录为:

61fb9746205c4c778bc4f230a4deabe2.png

在该文件中内容为:org.springframework.boot.autoconfigure.EnableAutoConfiguration=你的配置类的全限定名。如果有多个配置类,就用","隔开。这样在pom文件中,引入你写的工具的依赖,SpringBoot就会加载这个配置类,再配合配置上加的条件注解即可。

        回到App类中,看看main方法:

b2ba4b30c4724b8b90d96fc37a7ae382.png

        在该方法中,核心的类是SpringApplication,先看看它的有参构造方法,传入的是App.class,代码如下:

7bf9809c710c4bea9ae1b6077f786a16.png

        将传入的App.class存入LinkedHashSet,并赋值给primarySources属性。然后调用SpringApplication#deduceWebApplicationType(),推断应用类型,代码如下:

380867e107cf4140899e3b7570164727.png

        再调用SpringApplication#getSpringFactoriesInstances()方法,传入ApplicationContextInitializer.class(SpringBoot扩展点之一),加载ApplicationContextInitializer.class接口的实现类,代码如下:

c99eac291d94441bb871e617541e647d.png

cfdb178f951e4908814f238d4707801e.png

        并将获取到的ApplicationContextInitializer对象的集合,赋值给SpringApplication的initializers属性,代码如下:

a0813894226f4a7980b52e55aef53836.png

        同理,从spring.factories中获取到所有ApplicationListener对象的集合,赋值给SpringApplication的listeners属性中。最后调用SpringApplication#deduceMainApplicationClass()方法,推断主类,代码如下:

f4a3f99bdb2d4d2d9661112b89b80399.png

         最终获取到的也是App.calss,并赋值给SpringApplication的mainApplicationClass属性。

021764c16c594c39b10e0d6ecde756eb.png

        以上,就是SpringApplication的有参构造方法。这也这只是完成了SpringApplication初始化工作,但是要让服务跑以来,核心的就是调用SpringApplication#run(String[] args)方法,代码如下:

025004b26b0a440ca80186cd752d359e.png

        其实我的注释写的很详细的,不过我还是带着大家看看。首先是看看SpringApplication#getRunListeners()方法,代码如下:

6ea0fad5153f485ab3d089cd87602861.png

        还是通过通过spring.factories文件获取SpringApplicationRunListener对象的集合,实际上只有一个实现类,就是 EventPublishingRunListener,在创建这个对象的时候,会调用它的有参构造,传入 SpringApplication对象,有参构造的代码为:

29ac7349f19845aa8822734b7a837efb.png

        最终将EventPublishingRunListener对象在设置到SpringApplicationRunListeners对象中,后续在进行时间发布的时候,调用的是SpringApplicationRunListeners的某些方法,代码如下:

a6bcfe46d0ee40f7bf16191b9a766357.png

b021c1b732704381988cfa10a28c507c.png

        回到SpringApplication#run()方法,接着就是调用SpringApplicationRunListeners#starting()方法,发布ApplicationStartingEvent,调用ApplicationListener#onApplicationEvent()方法,调用之前会先判断,哪些ApplicationListener对象是对ApplicationStartingEvent事件“感兴趣”的。这里没有太多好说的,就不说了,继续往下看,再调用SpringApplication#prepareEnvironment(),这里是处理环境变量,配置就是在这个方法中解析读取的,需要重点看看,代码如下:

2470134721d448deba891e598e23475e.png

        先看看SpringApplication#getOrCreateEnvironment()方法,代码如下:

c939feda4d464514ad28adebb5fa2ac4.png

        看看StandardServletEnvironment的类继承图,如下:

739f224d0f1d4d78bc5957f5b5a0a470.png

        看看父类的构造,发现AbstractEnvironment父类构造中有做一些初始化的操作,代码如下:

6ccc501e9f9842ff8790a1c9557cb66c.png

af69367dd3a64eb188755fc82c9d9925.png

3082f91d659d4095a3872526d1b730f1.png

        到这里,可以知道,此时在环境变量中,应该设置了四种属性,顺序(顺序代表着优先级)分别是:StubPropertySource(servletConfigInitParams)、StubPropertySource(servletContextInitParams)、MapPropertySource(systemProperties)、SystemEnvironmentPropertySource(systemEnvironment),只不过前两个,此时还没有任何值,毕竟还没有设置值。打断点看看,我说的是否正确:

7628779fecda48f09cd6e58d4a5bdd95.png

dea58866de8d4202b77a0c7e3ac0415e.png

        要想获取main方法中的args参数,需要先设置在Idea中设置,设置如下:

3589ca8fb938440faf1e0bc198b96164.png

9890c0f1007544b5bc076473693f3393.png

        可以知道,最终通过main方法传入的args参数,封装成SimpleCommandLinePropertySource对象,并放入环境变量属性的最前面,此时环境变量有五种属性了。打断点看看:

1a9226b2d43f46b6b21fc7079fe98a3d.png

        再看看SpringApplication#configureProfiles()方法,代码如下:

f4169c840d664de593e21e5155452222.png

5c16334865ce4efda1c84415ba49a31a.png

        重点看看SpringApplicationRunListeners#environmentPrepared()方法,代码如下:

4f6c8bb9a648421082a82bbef420d7d0.png

3c8911c462854d19b10819c241a1ba0e.png

        看这个事件名,可以猜到是处理配置相关的,继续往下看,代码如下:

8df30f0437e246d48e6530392a7b4ba2.png

35c05fd0f5ee48cbb184420809a62cec.png

        这里我可以明确的告诉你,调用的是ConfigFileApplicationListener#onApplicationEvent()方法(我看过SpringBoot v2.6的版本,这个版本中没有使用ConfigFileApplicationListener来解析配置文件了,最低是什么版本就没有再使用ConfigFileApplicationListener类了,这我就不确定了),这个方法会处理配置文件,该方法的代码如下:

9e3239ac74fa422aae6f0845da06a723.png

9ad7be9be3674dd985adffbed547b511.png

14f6c0dc0f5843adaa7712750673fdaa.png

        可以知道,会创建RandomValuePropertySource(random)对象,放在SystemEnvironmentPropertySource(systemEnvironment)后面,到目前为止,环境变量一共有六种属性了,打断点看看,代码如下:

8803aa021889465db12b765aa2028778.png

        OK,再看看Loader#load()方法,代码如下:

d37c6080b3fd40d0a749facbb4f70371.png

        再看看Loader#initializeProfiles()方法:

f6afd1747ae541cb80ce77efd86f474b.png

af002bfbf6024ca5a40c223f39f2bb63.png

        可以知道,此时在profiles中有两个对象,也是个空的Set对象,另一个是Profile(default)。回到Loader#Loader()方法继续往下看,接着就是遍历profiles对象,代码如下:

7a0da0fbf7694886acb47ab9c9584d3c.png

        核心是调用重载方法 Loader#Loader(),代码如下 :

2b2867e3ce464bc3b16e14b63540161c.png

0a68b1c778774df68c1a43e0c55f7ff1.png

fa5b93f8cace4f1f9cb2b2ced3e476a6.png

d217a16b611246fcad87b443e38622e7.png

2445555e381d4ed8bf6b160ef4070f8e.png

        因此从源码可以知道,环境变量设置:spring.config.location、spring.config.additional-location,可以指定读取文件的路径,如果没有设置的话,默认读取的路径为:file:./config/ 、file:./、classpath:/config/、classpath:/ 等四个路径(顺序即为读取路径的优先级)。并且设置 spring.profiles.active,可以设置文件后缀,如设置为 dev,最后读取的文件为:xx-dev。

然后就是对这四个路径进行遍历,判断那个路径下,有配置文件。除了知道文件路径外,还要知道读取的文件名叫什么,这个也有默认值,当然也可以通过配置去修改默认的文件名,代码如下:

f62fa7094d9548ceb8c061adbe1552b9.png

        有了路径和文件名,就可以准备读取了,代码如下:

f27cef344a1a4e72af84975e637bf632.png

        看看PropertySourceLoader是如何赋值的,代码如下:

f1b2a42c5c43495db372384c0cf9fb6f.png

        可以知道,也是从spring.factories文件中读取PropertySourceLoader接口的实现类并实例化,一共有两个,分别是PropertiesPropertySourceLoader 和 YamlPropertySourceLoader。前者用于解析xml和propeties后缀的文件,后者解析yml和yaml后缀的文件,如下:

70691e6536ae45a1a83861f7f6de3529.png

8e60c3bdf80a413db4232651ecd16871.png

        继续往下看,代码如下:

d0b721bfca4447da9203cdb47c1c8c0f.png

1c1a769f6bc443cdbd6a88148c5ac201.png

c83a0f7c76474bb2a0eac400865f1f44.png

        看看Loader#loadDocuments()方法,代码如下:

2e9ce24adb24458b810f0f1b6d58750c.png

5c9d513adb0543f0a0c376a290c24e40.png

7ad5ed5b3faf4dc0b437e6372547e06b.png

5c024808b2694e60adcca286604379bb.png

        到这里就行了,感兴趣的可以自己研究,回到Loader#load()方法,代码如下:

a9c2ab64768a47ca97c9a16e9a88c2fb.png

        调用consumer#accept()方法,也就是前面传入的λ表达式,即:

d1f1065fb1c24d66817f21f8c5c2539b.png

d89d1bae73c84fa48858f63df2c92e84.png

        到目前为止解析的还是application,由于我在application.yml配置了profile,因此还会继续读取:

3313b9948d3b4798a674785d7914521e.png

85b17019316c4f349632ddee73d97672.png

        回到Loader#load()方法,由于在前面已经读取到application.yml中设置的dev了,并放入Loader的profiles属性中,而且还是在遍历profiles,因此最终会解析application-dev.yml文件,代码如下:

b54fba0ead4a441e953f6f6880553b0d.png

3f3eb2e66fb84c599c9fbce7e2a7ecfa.png

        读取配置的逻辑一样,这里不再赘述,到现在为止,读取的配置还只是存在Loader的loaded属性中,需要放如环境变量中,也就是调用下面的代码,代码如下:

37f94aa7274a4f85b1071cd798658a63.png

6f94a1a8858d4e82be14314269428d44.png

f2c90cc0311446209229bf594ac04c4d.png

        在读取配置的时候,会多次调用Collections.reverse()方法,改变顺序,其实这就是配置优先级的关键,继续往下看:

eba3795dc0c04584b49146036dd62255.png

895ff1301c144406b201fa67cf5ff6b8.png

        到现在为止,环境变量中已经有八个配置了,其中application-dev.yml的配置在application.yml之前。如果通过环境变量取值的话,就是按照这个顺序来取值的,也就是说,只有前面七个配置中找不到,才会到第八个配置中找!到目前为止,我觉得SpringBoot配置读取这块,应该是讲的很详细了。

        回到SpringApplication#run()方法,继续往下看,代码如下:

6b64e4cb21804e5e95ed4188797eacac.png

1a1ce8b82fed4a7eb6ec2d1a2d096dd1.png

        再看看SpringApplication#prepareContext()方法,代码如下:

1a7f00fda5e2473ebd836154cceb2127.png

968bdd1e0af24e95af7d9a7deb5fa433.png

7148273294f845009459e58ee9ebd398.png

31f276be1ebc4338907355f2425fc55a.png

        看看BeanDefinitionLoader#load()方法,代码如下:

d87eb26a92694f31996860a493f485e2.png

        回到SpringApplication#run()方法,再看看SpringApplication#refreshContext()方法,代码如下:

d52699a794fc4dae98958dec23ae127f.png

842abcb5393b451aba37d3102b1e8261.png

3eb5d51a8027486dae241ce43407906b.png

        看看它的继承关系图:

45bd142f33fa483799927b5af4068012.png

3a74af3d3c1d4c67bcb74c6b8e9bea61.png

        AbstractApplicationContext#refresh()方法有多重要,想必就不用我多说了吧,这块的代码在我之前的博客(《Spring源码深度解析(上)》)讲的很详细了,有兴趣的可以看看,其中有两个方法,即onRefresh()方法和finishRefresh()方法,需要我说一下,先看看onRefresh()方法,代码如下:

8d7837e10df64ee7a3b19761cd3372a5.png

86b38661902649cd8fbe03f156aca9ce.png

836e17b1a4fc483d86fe33725fed4f26.png

        其中ServletWebServerApplicationContext#initPropertySources()方法,会将ServletContext属性值设置到环境变量中,代码如下:

c72921caf3c04cf19db66fd0bb355e86.png

        再看ServletWebServerApplicationContext#createWebServer()方法,代码如下:

67d9a36de8f446408505b3541376a53d.png

781637f5fd5e4232962c7e99bd4b3957.png

9c0eacfe458e47509d04d76c3a6f7220.png

5bc31a00c25d4218ac404eeadd0f475b.png

dc58565ba8254ccab682bbb4ff4cad86.png

        再看看finishRefresh()方法,代码如下:

9819852387f14c8ea4b7468c2252fb33.png

a4b2b9a0eea04ef28cba4853afff4b3d.png

24fa406b6dc0435fb9dd302216926e32.png

        最后再回到SpringApplication#run()方法看看剩下的代码,如下:

e983c0669baf489b849d4fda14eb3c0b.png

        到这里位置SpringBoot框架的源码算是讲完了,我个人觉得应该是讲的很全面的,如果在讲解的过程中,有漏讲或者讲错的,欢迎指出,感谢~



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。