本文假设读者已经有了Spring和SpringBoot的使用基础。全文一万多字,深入探讨了源码的设计模式、模块之间的关系以及容易混淆的细节。
通过剖析自动装配机制的前因后果,由浅入深的看看SpringBoot主要做了些什么事。【期间还发现了一个让人忽视的Maven特性】
都是干货,无关紧要以及说了也让人记不住的繁琐过程基本都被省略了。如果觉得文章太长,可以只看总结。如果觉得总结的还有点意思,再往前看。
概念很重要,但也很抽象空洞,在深入细节之后,再回过头看,会有新的理解。
经过对比之后,其实最让人困惑的就是这个问题。
加载类、实例化对象和注入容器这些操作是由classloader和Spring来完成的,那么没有POM文件的情况下,依赖的jar包从何而来呢?
SpringBoot当然不会无论是否需要,统统将其依赖进来。我们通过实验,也可以明显发现:加入starter,重新构建,starter依赖的包就能加进来,否则就会被删掉。
经过网上查(没查到)/和人交流/问chatgpt/做实验,得出一个结论:
是因为jar包外的xxx.pom文件(如spring-boot-starter-data-redis-2.2.6.RELEASE.pom),starter才能依赖的其他包。
chatgpt给出解释是:Maven首先看jar包内的pom.xml,如果jar包内没有,则使用和jar包同名的xxx.pom文件。【虽然不能确认是否正确,但确实可以解释现象】
所以我总结xxx.pom文件的作用:
在使用IDEA时,当我们点击依赖包的名称时,IDEA会显示该依赖包对应的pom文件,这个pom文件就是xxx.pom文件。如果把这个文件删除,那么IDEA就无法找到该文件,从而无法显示依赖包的信息【之前一直以为idea打开的是jar包里pom.xml】
当jar包内没有pom.xml时,maven以它为依据获取依赖【本次学到的】
猜想:由于存在外部的xxx.pom文件,因此并不需要使用jar包中的pom.xml文件。这样做可以省去解压jar包的步骤,从而使下载更加快速和方便。
那么,Maven是否使用的正是xxx.pom文件,而不是pom.xml文件?
由于缺乏官方的证据和参考资料,我对之前ChatGPT所提供的回答表示怀疑。【如果您能够提供官方的参考资料,请留言分享】
为了拓展性。
是的,如果所有组件都是SpringBoot管理,这个设计确实没问题。Spring扫描包的时候,注解@ComponentScan(basePackages = {"com.*"})
只要覆盖到starter包里的配置类即可。
但是,Spring的很多“好东西”都会考虑可扩展:我能用,用户也要可以用。
如果用户也想使用这套机制,要引入的组件包路径就不是Spring的包路径了,Spring就可能扫描不到。
所以提供一个配置文件,在其中指定自己配置类路径,这样做能最大程度提高灵活性,让用户也可以轻松使用并扩展自己的组件。
看一下spring的这个配置文件长什么样:
文件采用key-value形式存储。
只在一部分jar包里有这个文件,如spring-boot, spring-boot-autoconfigure等包内。
读的时候会把classpath下所有存在的spring.factories统统读进来。
其中自动装配需要的就是key为org.springframework.boot.autoconfigure.EnableAutoConfiguration
注意右侧的value值,该值通常很长,并用逗号隔开,而且名字一个个都叫“xxxAutoConfiguration”,表示这都是配置类(带注解@Configuration的)。
那文件里其他的key呢?
SpringBoot启动过程中还有很多流程,也会用到这个文件。比如各种Listener。也都是读配置,然后反射加载然后实例化的。
找到SpringBoot的启动类Application上的注解@SpringBootApplication
,点进去。
找到@EnableAutoConfiguration
,点进去。
最后就可以看到注解@Import(AutoConfigurationImportSelector.class)
注解和普通类一样具有继承特性,所以相当于Application也使用了注解@Import,并注入了AutoConfigurationImportSelector。
AutoConfigurationImportSelector
不是一个普通类,它继承了DeferredImportSelector
。
实现了其中的方法selectImports
可以看到selectImports返回了一个String[],其实返回的就是上图中的A(配置文件spring.factories)里的内容。
那么,这个selectImports方法是被谁调用的呢?就是上图中所示[3]的位置。简单来说,在SpringBoot启动过程中被调用,但这个过程非常复杂。因此,我们需要逐层剖析SpringBoot和Spring的启动过程,直到找到调用该方法的位置。
这部分,我们从一个大家都熟悉的地方开始讲起,比较容易让读者理解。
这里之所以还要讲Spring的启动流程,是因为讲SpringBoot的启动流程,绕不开Spring的启动流程。从某种程度上说,SpringBoot并没有什么独创性的功能,即便是核心的自动装配,也是依赖Spring才得以实现的。
SpringBoot这种非常不独立,以及存在感弱的特点。也就导致很多人用了许久,也说不清楚SpringBoot到底是个什么东西。
比起Spring,它似乎就是多了一个main方法,配置比较方便而已(事实上,这确实就是SpringBoo的全部功能)。但功能简单并不代表意义平凡。Spring说起来,实现机制那么复杂,但目的无非也是简化开发。
两个核心方法:
Spring启动核心方法:org.springframework.context.support.AbstractApplicationContext
类里的refresh()。
AbstractApplicationContext也是Spring的核心类之一,属于spring-context包。
启动Spring容器的时候,无论是用xml还是注解,都会走到refresh()方法。
SpringBoot启动核心方法:org.springframework.boot.SpringApplication
类里的run(String… args)。
SpringApplication是SpringBoot核心类,属于spring-boot包。
启动springBoot时,执行SpringApplication.run(Application.class, args),很快就会进入run方法。
先直观的看一下着两个核心方法,左边的是SpringBoot的,右边是Spring的
注意看黄色箭头标注的位置,就是SpringBoot通向Spring核心启动方法的位置。
SpringBoot属于spring的子项目。如果按照java对象的父子关系,子类包含父类来类比。
放在这里,就是SpringBoot内包含了spring。 启动过程也是这样的包含关系。
后面再说源码的时候,我都会以这两块代码为“坐标”来说,这样就不至于出现“搞不清身在何处”的问题了。
右边箭头标注的invokeBeanFactoryPostProcessors(beanFactory);
就是处理selectImports方法的入口。
所以:spring.factories里的AutoConfiguration配置类是Spring容器启动过程中的PostProcessor注入的。
如果要继续深入下去,找具体的调用点,请看后面第四节[invokeBeanFactoryPostProcessors]。
因为PostProcessor代码很复杂,我单独分出一块来讲。这里不再赘述。
SpringBoot费那么大的劲,通过SPI机制,实例化出来个什么东西呢。
以spring-boot-starter-data-redis(用redis时需要加的starter)为例
这是在spring.factories里,"RedisAutoConfiguration"是value里一员。
为什么默认就在配置里了,我还没想用redis呢。
因为要让一个starter真正用起来,spring.factories的配置和pom.xml里的依赖缺一不可(SpringBoot就等着你在pom.xml里加redis的starter呢)。
当这个配置类被实例化并注入Spring容器之后。其中配置的两个Bean(Redis组件):RedisTemplate
其中注解@EnableConfigurationProperties是用来将properties或yml配置文件属性转化为Bean对象。括号里的属性写要封装的Bean类型。打开这个类
通过这个@ConfigurationProperties(prefix = "spring.redis")
注解,就可以自动从配置文件中读配置并封装到当前对象中。redis的配置规则就是以spring.redis开头。
注意:什么叫配置类?前面展示两个类,哪个是配置类?
在Spring中,前者(注入Bean的)叫配置类。就是使用@Configuration或者@Component、@ComponentScan、@Import、@ImportResource等叫配置类(一般我们习惯只把@Configuration叫配置类)。
而@ConfigurationProperties的类是用来封装配置信息的,却不叫配置类。
这部分主要是讲Spring的重要组成部分:Bean后置处理器PostProcessor的源码。
这张图只是抽象出了一个非常简单的过程(实际过程比这复杂的多):
在PostProcessorRegistrationDelegate这个类中
看一下这两个接口的注释:
先看第二个接口的,BeanFactoryPostProcessor
注意看标注的位置,核心就是说这个单词:hook。
很常规的扩展手段,通过钩子方法来让用户来插入自己自定义内容。也是PostProcessor功能的核心。
然后看BeanDefinitionRegistryPostProcessor
首先,它继承了前面那个BeanFactoryPostProcessor。
关键单词:SPI。就是说这是SpringBoot实现的SPI机制。通过一个配置文件(spring.factories)来加载其他类。
意思就是说:这个接口也是一个处理Bean的钩子(因为继承了那个钩子),只不过功能相对更加单一。具有通过SPI机制,读配置批量加注入Bean的功能。
过程相对复杂。尤其是最终解析配置类的类ConfigurationClassParser(上图下半部分)。递归调用(为了一层层往下找注解),代码绕来绕去的。【这个图仅供看源码时参考,这也仅仅是一部分关键的节点】
其实就是说的Web容器。对于普通组件,就是new一下,set一些参数就能用了。但Web容器还多了一步:启动。而且要包含在SpringBoot的启动过程中。
我们猜想,Tomcat的创建和启动应该是在SpringBoot代码里,毕竟Tomcat太特殊了。
但实际上Tomcat的创建和启动都是在Spring容器创建过程中。
在Spring启动的核心方法里,进入onRefresh()里,就能找到创建Tomcat对象的位置。
很多人认为,前面代码执行完之后就算Tomcat启动了(相关日志也打印出来了),但实际并没有。前面只是创建了Tomcat实例并初始化。
启动代码在这里面
是不是很意外,Tomcat的创建启动不但放在Spring里,而且还拆的那么散。
还有更意外的。Tomcat启动了,我们还有考虑关闭的问题。SpringBoot关闭之后,还要考虑连带着把Tomcat一并关掉。
这个就涉及到JVM关闭前的钩子函数。位置在启动Spring容器之后
Runtime.getRuntime().addShutdownHook(进程);
作用:在jvm关闭前,执行这个线程。
问题:强制杀进程,这个进程也会被执行吗?
实验结果:不会。强制执行,这个进程不会被执行。
实验方式:
实验结果:
正常关闭的有这样的日志:
2023-03-13 12:28:06.440 [SpringContextShutdownHook] INFO o.s.scheduling.concurrent.ThreadPoolTaskExecutor-Shutting down ExecutorService 'applicationTaskExecutor'
而强制kill的没有(自己写demo也能验证)。
思考:那所谓正常关闭,是怎么关的?
其实就和SpringBoot的启动方法run一样,都在SpringApplication类里,叫exit
ps -ef | grep java
可以看一下,只有SpringBoot进程,并没有多出来一个Tomcat进程。思考:Tomcat那么特殊的组件放在Spring里真的合适吗?不会把Spring正常的流程搞的一团糟吗?
现象:有些人可能找不到启动Tomcat的代码在哪里。因为在refresh方法里的finishRefresh(),点进去,发现并不是我上面截图的那个方法。
以上两件事共同引出了:Spring启动过程采用了策略模式。
先说找不到方法的问题。那个方法其实在ServletWebServerApplicationContext类中。如果你是debug,就会自然点进这个方法里。否则就会点进AbstractApplicationContext自己的finishRefresh()。看下面这个继承关系
也就是说,父类调用了子类的重写的finishRefresh()方法(这个说法其实不准确。如果你对这种现象比较困惑,请看我的另一篇博客复杂父子继承相互调用的深入理解)
再回头看一下SpringBoot的启动核心代码
在这里,也就是把context传入Spring启动流程之前。context的真实类型就已经是子类ServletWebServerApplicationContext。所以进入Spring流程,后面的很多Web相关的个性化代码都是在这个子类里执行的。
这是一个策略模式应用方式:
这样子类的个性化行为就不会影响到公共父类的代码了。
前面看似“自圆其说”了,但其实并不对。
事实上,Tomcat启动的过程不但使用了策略模式,单独另外写了一个子类。而且这个子类实际上属于SpringBoot的代码。这是全类名
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
现在是不是就很圆满了:SpringBoot没有动Spring代码。只是在自己的包下重写了一个AbstractApplicationContext的子类。看起来就成功侵入了Spring的流程,把Tomcat启动等一众Web特殊操作给塞进去了。
这才是策略模式的正确打开方式。
SpringBoot自动装配原理分析二
spring注解之@Import注解的三种使用方式
Spring 使用 @Import 的好处
spring解析配置类
Runtime.getRuntime().addShutdownHook()