SpringBoot的模块及结构
跟大家聊聊我们为什么要学习源码?学习源码对我们有用吗?(源码感悟)
1 前言
由于现在微服务很流行,越来越多企业采用了SpringCloud微服务架构,而SpringBoot则是快速构建微服务项目的利器。于是源码笔记以此为切入点,将SpringBoot作为我们源码分析的第一个开源项目,之后还会对更多开源项目进行源码分析。要进行源码分析,笔者结合自身经历来跟大家聊聊我们为什么要学习源码这个话题,大家一起探讨学习。
我们程序员在开发代码时每天都在使用别人写好的框架,无论你是在使用Spring生态的Spring核心,SpringMVC,SpringBoot和SpringCloud等框架,还是在使用阿里系的Dubbo,RocketMQ,Seata,Druid等中间件框架,亦或你是搞大数据的,在使用Apache组织的Zookeeper,Hadoop,Hive,Spark等大数据组件框架,这些开源框架都给我们的项目编码带来了极大的方便,促进了我们的开发效率。是的,这些都是开源大神们帮我们造好的轮子,我们直接使用即可,而且用起来很少出bug,因为这些框架一般都是经过考验的才能流行起来。
可能大部分人会认为,这些轮子都已经造好了,我们直接用即可。是的,这些开源框架对我们使用来说是透明的,它们就像一个个黑盒子。至于这些黑盒里面装的是什么东西,里面是怎样构造的,如果没有去探究这些黑盒的话,我们无从得知。但是,我们做开发的天天跟这些黑盒打交道,难道你就没有一种强烈的欲望想知道这些黑盒里面装的是什么东西,黑盒里面是怎样构造的么?难道你就不想知道我们天天使用的黑盒子里面的原理么?
那么今天就跟小伙伴们聊聊“我们为什么要学习开源框架源码?学习开源框架源码对我们有用吗?”这个话题。
2 我们为什么要学习源码?花那么多时间去学习源码值得么?
可能有些同学会问:我们为什么要去学习源码?花那么多时间去学习源码值得么?
对于这个问题,应该很多小伙伴在没读源码前深有感触。当时应该就是这种观念,反正开源框架一些外面的大牛帮我们封装好了,自己开箱即用即可,管它里面黑盒机制是啥。应该很多小伙伴也是这种观念,导致自己在开发项目时若遇到bug时一筹莫展,百度了很多解决方案都无效,故而浪费了很多时间。于是才会决心钻研一些常用框架的源码。可见,没必要学习源码的观念是可能是最错误的观念吧,个人观点(仅供参考)。
我们学习源码无非有以下几个原因:
1)开发项目需求需要
一方面,通常我们在开发项目的时候,遇到Bug是再正常不过的事情。比如某个工作项目采用了Spring生态系列的框架比如SpringBoot,SpringCloud等,当出现问题时自己要会解决,如果我们不懂框架里面的黑盒机制,当出现问题我们肯定会一头雾水,不知如何着手解决开源框架出现的问题,此时我们肯定会去百度,但百度的文章质量参差不齐,最坏的结果可能就是我们根据百度的解决方案,一个一个去试了,但仍然没有解决问题。这就是对框架黑盒机制不熟悉的原因导致的。
另一方面,我们在开发项目的时候,有时候开源框架不能完全符合我们的业务需求,此时我们需要对开源框架进行扩展甚至是改造,比如我们正在使用dubbo框架,若dubbo自带负载均衡策略不能满足我们的业务需求,此时我们是不是得要对dubbo的负载均衡策略进行替换或扩展。幸好,dubbo提供了SPI接口给我们即插即用,此时我们不用研究dubbo里面的黑盒也可以做到替换现有的负载均衡策略。那假如有一天,我们要对某个开源项目进行改造呢?此时是不是也需要我们知道开源框架的黑盒机制,若不懂框架黑盒原理,此时我们是无法下手的。
由于项目开发需要的以上原因,所以我们平时有空时就要多学习源码,多探究里面的黑盒机制,磨刀不误砍柴工。
这个就是典型的项目开发需求驱动我们不得不去研究开源框架的源码机制的原因吧。
2)对技术饱含热爱,不断深入学习黑盒机制
是的,除了项目需求开发需要去研究源码外,应该很多小伙伴都是出于对技术的追求去研究开源框架源码。他们为了不断提高自己的编码能力,去不断学习外面大牛们的优秀作品。是的,大家都知道,闭门造车可以说是造不出名车的,此时,我们必须去学习大牛们优秀的开源作品,学习他们是怎么面向对象编程的,学习他们是如何熟练运用设计模式的,学习他们是怎样设计接口的等等,真的有太多需要我们学习了。如果我们走的是技术路线,对技术还有更高的追求,学习源码可以说是我们绕不过去的路。因此,行动起来吧,源码搞起来,我们只有不断学习源码,以后再阅读其他项目的源码时才会游刃有余。
3)有些人学习源码,可能是为了面试
当然,有些人学习源码,可能是为了面试。因为现在很多面试若面试官稍微问难一点的问题都会涉及源码,往往很多人就是对源码不熟悉而挂掉。因为一部分小伙伴往往就是为了面试才去学习源码,但是往往这种为了面试才去学习源码的方式效果没有前面的项目驱动方式和技术追求方式的效果那么好(个人观点,不喜勿喷)。因为,有些同学为了面试,通常都是短期突击源码,为了某个面试问题而背诵一些答案,因为死记硬背的居多,所以这种方式往往深入不了源码的精髓。当然,不管出于什么目的学习源码都理应鼓励,因为毕竟去学习源码了肯定就会有提高,就踏出了与众不同的一步。
3 学习源码,我们能得到什么?
是的,既然我们花费了那么多时间去学习源码,去研究源码?我们的收益有多少,我们究竟能得到多少回到回报呢?可以先肯定的回答,当然有很大的回报。前面也说过,学习开源框架源码能帮助我们解决项目遇到的bug,扩展我们的项目需求;通过学习源码,学习开源大牛们是如何运用设计模式的,然后运用到我们开发的项目中,使我们的项目的模块更易于扩展;通过学习源码,我们能编码更高效。为什么呢?如果我们对某个开源项目源码很熟悉,那么我们就可以对这个项目的源码信手拈来应用到自己项目中,不是么?总之学习源码好处多多,这也是成为大牛们的必经之路吧,只有学习和借鉴别人优秀的作品,自己才能造出更优秀的作品。说到这点,不得不说下RocketMQ,RocketMQ是阿里的一款优秀的开源中间件,RocketMQ之所以性能高吞吐,抗得住阿里双十一的考验,其正是很多方面借鉴了Kafka的设计;此外,记得Dubbo的一个缓冲类也是借鉴了Netty的ByteBuf类。
是吧,要造出一些优秀的作品,必须去学习借鉴别人的优秀作品。
4 要想不被淘汰,必须学习源码
最后要说的是,要想不被淘汰,我们必须学习源码。
现在程序员越来越多,可以说竞争尤其激烈。如果我们想不被淘汰,则我们时刻要保持竞争力,那么必然要时刻学习,终生学习,生而有涯而学无涯。因为CRUD大家都会,要提高自己的技术能力,此时学习开源项目的源码就尤为关键了。如果我们不想成为CRUD工程师,还有更高的技术追求,那么我们就要有更高的要求。当然,学习开源项目源码的目的是借鉴大牛们的优秀作品,最终要学以致用。另外,个人观点:学习源码是提高自己编码能力的最好方式。
如果我们走的是技术路线而非管理路线,若还有更高的技术追求,那么学习源码是我们绕不过去的砍。
真的,学习源码实在是太重要了。
只要我们还有更高的技术追求,或许你想成为技术大牛,但是学习源码是成为大牛的必经之路。不是每个人都能成为大牛,但不学源码就肯定成为不了大牛,因为学习源码是提高自己的最好方式,不论何时,学习源码都不会晚,共勉!
Are u ready?
分析开源项目源码,我们该如何入手分析?(授人以渔)
1 前言
本文接上篇文章跟大家聊聊我们为什么要学习源码?学习源码对我们有用吗?,那么本篇文章再继续跟小伙伴们聊聊源码这个话题。
在工作之余开始写SpringBoot源码分析专栏前,跟小伙伴们聊聊“分析开源项目源码,我们该如何入手分析?”这个话题,我们就随便扯皮,反正是跟小伙伴们一起学习交流,没必要太正式。
小伙伴们看完本文后,若有自己的源码阅读心得可以在下面进行评论或私聊我进行分享,让我从小伙伴们身上GET多点源码阅读的一些技巧,嘿嘿。
2 学习开源框架源码到底难不难?
那么,先跟小伙伴们聊聊学习开源框架源码的感受,请问你们认为学习开源框架源码到底难不难?这是一个开放的话题,可谓仁者见仁,智者见智。有一些开源大牛们会说,So easy!;有一些有源码阅读习惯且工作多年的小伙伴们会说,还好。;有一些刚开始学习源码的小伙伴们会说,太难了!。是的,不同工作经验不同技术层次的人的回答是不一样的。
那么刚开始学习开源项目源码难不难呢?应该对绝大部分小伙伴们来说应该是偏难的。为什么呢?可能有以下四点原因:
-
一个能流行起来的成熟的开源项目必定功能齐全,可扩展,而功能齐全可扩展的开源项目必定很复杂,代码量大。比如Spring5框架的源码行数达到了六七十万行,SpringBoot的源码行数达到了25万行左右,Dubbo和RocketMQ的源码行数达到了10万行。一个成熟的开源项目代码量这么多,可以想象其有多复杂。
-
阅读源码时,我们有时候无法猜透源码作者当时编码时的想法。因为在刚开始阅读源码的过程中,我们肯定会遇到很不懂的代码,不知道作者为何这么写,为何在这个位置写代码,这些都是很正常的,因为当初作者为啥这么写,可能是针对一些比较特殊的业务场景,或者为了某方面的性能等等,我们根本无法猜透。打个不太恰当的比喻,阅读源码猜测作者的心思就像当初遇到一个自己喜欢的姑娘,猜测她的心思一样,比如猜测她喜欢什么,她的兴趣爱好是什么。其实刚开始阅读源码也一样,有些地方我们一开始是无法猜透作者的心思的。
-
有些开源框架可能集操作系统知识,数据结构,算法和设计模式于一身。是的,优秀的框架必定是集成了很多设计模式于一身,目前为止笔者还没见过哪种流行的又没有应用设计模式的框架哈。比如很多框架运用了单例模式,工厂模式,责任链模式,装饰器模式和模板方法模式等,因为使用设计模式能让框架易于扩展。同时,不乏一些框架应用了一些操作系统层面的知识,这一块比较底层,相信很多学java的小伙伴没接触过。此外,开源框架某些地方会用到数据结构和算法,举个栗子,比如Dubbo默认有四种负载均衡策略,而每种策略又对应一种算法,其中又数RoundRobinLoadBalance负载均衡策略最复杂,一开始实现RoundRobinLoadBalance负载均衡的方式并不太完美或者说有bug,Dubbo也是重写过RoundRobinLoadBalance几次,最终借鉴了Nginx的RoundRobinLoadBalance负载均衡算法。
上篇文章《跟大家聊聊我们为什么要学习源码?学习源码对我们有用吗?》也说过优秀框架之间的思想都是互相借鉴的,这就是我们要学习源码的原因之一。
这里好像扯的有点远了,总之这里要说明的是阅读优秀框架是有一定难度的。
-
有些开源框架注释太少也增加了阅读源码的难度。说到开源项目注释,如果我们阅读老外写的的框架源码可能还好,一般都会有大量注释,比如Spring框架,可以说几乎每个方法都有注释,这个就给我们阅读源码起了很大的帮助。不过唯一不好的可能就是英文注释,阅读对英语有一定的要求。其实英文注释还好,遇到不懂的,百度翻一下就好了。其实比较头疼的就是一些国内优秀的开源框架,其注释可以说是很少的,这无疑大大增加了阅读的难度,甚至有些框架的文档也不齐全,那就更加GG了。
3 该如何入手去分析开源框架源码?
前面跟小伙伴们聊了阅读源码的难度,千万不要被吓慌了。伟大的毛主席说过,我们要在战略上藐视敌人 战术上重视敌人。不可否认,刚开始阅读某个开源项目的源码是有一定的难度。注意,前面的用词是刚开始,刚开始哈。也就是说如果我们坚持阅读源码的话,养成阅读源码是陶冶情操的习惯的话(网上看到的这句话,这里引用装装逼,嘿嘿),长期坚持下来再去阅读其他项目的源码,游刃有余不敢说,但肯定可以很快入手。
那么,我们该如何入手去分析开源框架源码呢?
首先,结合前面所说的阅读源码之所以难的原因,我们就要有针对性的去克服解决。比如有空多学学设计模式,算法和英语。这些软实力确实对阅读源码有很大帮助。
其次,阅读源码的前提是什么?当然,阅读源码是要建立在会使用的基础上,就像若还不会走路就学骑单车一样,若连用都不会就去钻研源码可能会适得其反。
最后,我们阅读源码要注意一些技巧,现在根据自身经历总结一下相关思路和技巧,如下:
-
开始阅读源码时,先对框架的模块及其关系有一个整体的认识。我们要对框架项目的模块和目录要有一个全盘的了解,要知道每个模块是干嘛的,然后要了解模块与模块之间的关系。
举个栗子,比如Dubbo的模块分包核心的主要有以下八个,如下图,我们要知道最基础的的模块应该是dubbo-common公共逻辑模块,这个模块作为最基础的模块,主要是提供了通用模型和工具类;然后dubbo-remoting是远程通讯模块,依赖于dubbo-common模块,相当于Dubbo协议的实现;而dubbo-rpc则是远程调用模块,依赖于dubbo-remoting模块,抽象各种协议,以及动态代理;dubbo-cluster是集群模块,依赖于dubbo-rpc模块,将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等。
-
分析源码先从父类或父接口开始分析。因为父类或者父接口往往代表了一类功能,这些基类或基类接口往往抽象了各个具体子类共有的属性和行为,一些比较基础的方法都在父类中实现,然后留个模板方法给子类去实现即可(模板方法的应用)。
举个栗子,这里还是拿Dubbo的负载均衡来说吧,如下图,LoadBalance是各种负载均衡策略的超级接口,定义了 select 方法用来实现选择哪台机器;然后AbstractLoadBalance是一个抽象类,实现了LoadBalance接口,在覆盖了 select 方法后,其又增加了 calculateWarmupWeight 和 getWeight 权重相关的两个方法,因为这些方法都跟具体的负载均衡策略类有关,故在父类实现了。值得注意的是AbstractLoadBalance抽象类的 select 方法中里留了个给子类覆盖的 doSelect 方法,具体的负载均衡策略将在doSelect中实现。
-
阅读源码前首先要找到启动类。阅读分析源码时要先从启动类开始,因此找到框架启动的入口很重要。
-
阅读源码时要分清主干和枝节代码。找到启动入口后,然后就可以顺着启动入口一步一步调试来阅读源码了。不过在初次调试源码时值得注意的是一定要分清主次代码,即要先阅读主干代码,其他枝枝节节的代码没明白的可以放一边。切忌一开始就深入细节然后出不来了,这样就会造成只见冰山一角而看不到全貌的感觉。
-
阅读源码前要分清主次模块。即阅读分析源码不能漫无目的,全盘通读,我们要从我们平时有用到的模块开始分析。每个人的时间都很宝贵,我们要把时间花在刀刃上。比如SpringBoot增加的新特性中有自动配置,而自动配置特性又非常重要,因此可以挑选自动配置来进行源码分析。
-
要充分利用源码项目的测试类。之前也说过,一个框架之所以能流行,必定是经过大量测试的。因此如果我们像具体了解某个类和某个方法,我们可以充分利用这些测试类来辅助我们源码分析。
-
要学会一些调试技巧。这一点也很重要,比如在调试过程中如何查看调用关系等等,这里不多说,如何高效学习和阅读源码这篇文章中分享了大量调试的干活,小伙伴们可以瞅瞅。此外,还要学会有技巧的搜索源码,说到这里,下面举个栗子。
举个Spring事件监听的栗子。比如我们现在要知道哪个监听器监听了ContextRefreshedEvent事件,此时我们可以通过idea全局搜索"(ContextRefreshedEvent"关键字,得到以下截图:从下图可以看到spring-webmvc模块的FrameworkServlet,spring-context模块的ScheduledAnnotationBeanPostProcessor,和spring-jms模块的JmsListenerEndpointRegistry等类订阅了ContextRefreshedEvent事件,那么在容器刷新的时候这几个类将会监听到ContextRefreshedEvent事件,执行一些初始化逻辑。
-
肯定还有大量的阅读源码技巧,希望本文能起到抛砖引玉的作用,期待小伙伴们可以留言分享下,让笔者也收益一下。
4 学源码,谈实践,论坚持
最后,我们学习源码不是为了学习而学习,最理想的效果我们要学以致用。比如把从源码中学习到的设计模式,接口设计方法,面向对象原则和相关算法等等都可以应用到我们手头的项目中,这才是我们学习源码的最终目的,也是源码学习的最理想的效果。可能这里有些小伙伴会说,我平时参与的项目都是业务类的项目,而不是开发基础框架,开发中间件,CRUD比较多,可能学习基础框架的源码对我们用处很少。其实不是的,只要你有参与项目,学习源码我们学习的是思想,我们就可以把源码框架设计中的思想应用到我们的项目中。
最后的最后,我们来谈谈坚持,这是最难能可贵的。很多大道理我们都懂,比如要坚持运动,坚持学习,坚持...,可是就是没能坚持下来,包括我自己,嘿嘿。坚持这东西太南了,不过还是应该给自己立个flag吧,把自己有用到的框架比如SpringBoot,Spring,Mybatis,Dubbo,SpringCloud等框架源码都阅读分析一遍,加油,小伙伴们共勉!
如何搭建自己的SpringBoot源码调试环境?--SpringBoot源码(一)
1 前言
这是SpringBoot2.1源码分析专题的第一篇文章,主要讲如何来搭建我们的源码阅读调试环境。如果有经验的小伙伴们可以略过此篇文章。
2 环境安装要求
- IntelliJ IDEA
- JDK1.8
- Maven3.5以上
3 从github上将SpringBoot源码项目下载下来
首先提供SpringBoot2.1.0的github地址:
https://github.com/spring-projects/spring-boot/tree/v2.1.0.RELEASE
因为要进行阅读源码和分析源码项目,我们是不是要在里面写一些注释帮助我们阅读理解源码,因此需要将SpringBoot源码项目fork到自己的github仓库中,然后再利用git clone url命令将已经fork到自己github仓库的SpringBoot源码拉取下来即可。
但由于以上方式往往很慢,通常会超时,所以笔者直接将SpringBoot项目直接下载下来,然后再导入IDEA中。
4 将SpringBoot源码项目导入到IDEA中
将刚才下载的spring-boot2.1.0.RELEASE项目选择maven方式导入到IDEA中,然后一直next即可导入完成,注意选择JDK版本是1.8,maven版本是3.5+。
此时下载maven依赖是一个漫长的等待过程,建议maven没有配置阿里云仓库的小伙伴们配置一下,这样下载速度会快很多。参考配置maven使用阿里云仓库进行配置即可。
5 编译构建SpringBoot源码项目
此时导入项目后,我们进行编译构建SpringBoot源码项目了,在构建之前做两个配置:
- 我们要禁用maven的代码检查,在根pom.xml中增加一下配置即可,如下图:
- 可能有的小伙伴们的pom.xml文件的project标签上显示
java.lang.OutOfMemoryError
错误,这是因为IDEA里的Maven的importer设置的JVM最大堆内存过小而导致的,如下图,此时可参考Maven依赖包导入错误(IntelliJ IDEA)解决即可。
进行了上面的两点配置后,此时我们就可以直接执行以下maven命令来编译构建源码项目了。
mvn clean install -DskipTests -Pfast
此时又是漫长的等待,我这里等待5分钟左右就显示构建成功了,如下图:
6 运行SpringBoot自带的sample
因为SpringBoot源码中的spring-boot-samples模块自带了很多DEMO样例,我们可以利用其中的一个sample来测试运行刚刚构建的springboot源码项目即可。但此时发现spring-boot-samples模块是灰色的,如下图:
这是因为spring-boot-samples模块没有被添加到根pom.xml中,此时将其添加到根pom.xml中即可,增加如下配置,如下图:
此时我们挑选spring-boot-samples模块下的spring-boot-sample-tomcat样例项目来测试好了,此时启动SampleTomcatApplication
的main
函数,启动成功界面如下:
然后我们再在浏览器发送一个HTTP请求,此时可以看到服务端成功返回响应,说明此时SpringBoot源码环境就已经构建成功了,接下来我们就可以进行调试了,如下图:
7 动手实践环节
前面已经成功构建了SpringBoot的源码阅读环境,小伙伴们记得自己动手搭建一套属于自己的SpringBoot源码调试环境哦,阅读源码动手调试很重要,嘿嘿。
如何分析SpringBoot源码模块及结构?--SpringBoot源码(二)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
1 前言
本篇接
如何搭建自己的SpringBoot源码调试环境?--SpringBoot源码(一)。
前面搭建好了自己本地的SpringBoot源码调试环境后,此时我们不要急着下手进入到具体的源码调试细节中,刚开始阅读源码,此时我们一定要对项目结构等有一个整体的认识,然后再进行源码分析调试。推荐阅读下笔者之前写的的分析开源项目源码,我们该如何入手分析?一文,干货满满哦。
2 SpringBoot源码模块一览
我们先来对SpringBoot的源码模块来一个大致的了解,如下图:
从上图可以看到,主要有以下四个模块:
- spring-boot-project:整个SpringBoot框架全部功能在这个模块实现,SpringBoot项目95%的代码都在这里实现,源码总共有25万行左右。
- Spring-boot-samples:这个是SpringBoot给小伙伴们赠送的福利,里面包含了各种各样使用SpringBoot的简单demo,我们调试阅读源码的时候可以充分利用该模块。
- Spring-boot-sample-invoker:这个模块应该是跟sample模块有关,注意根pom.xml中有这么一句话:
Samples are built via the invoker plugin
,该模块无代码。 - Spring-boot-tests:这个模块SpringBoot的测试模块,跟部署测试和集成测试有关。
因为SpringBoot的全部功能在spring-boot-project模块实现,因此下面重点来介绍下 spring-boot-project 模块。
3 spring-boot-project源码模块详解
先来看下spring-boot-project整体模块结构,如下图,然后我们再逐个来介绍:
1) spring-boot-parent
这个模块没有代码,是spring-boot模块的父项目,被其他子模块继承。
2) spring-boot
这个模块是SpringBoot项目的核心,可以说一些基础核心的功能都在这里实现,为SpringBoot的其他模块组件功能提供了支持,主要包括以下核心功能:
SpringApplication
类,这个是SpringBoot的启动类,提供了一个静态的run
方法来启动程序,该类主要用来创建并且刷新Spring容器ApplicationContext
.- 支持选择不同的容器比如Tomcat,Jetty等来作为应用的嵌入容器,这个是SpringBoot的新特性之一。
- 外部配置支持,这个指的是我们执行
java -jar xxx.jar
命令时可以带一些参数,比如执行java -jar demo.jar --server.port=8888
来将应用端口修改为8888. - 该模块内置了一些SpringBoot启动时的生命周期事件和一些容器初始化器(
ApplicationContext
initializers),来执行一些SpringBoot启动时的初始化逻辑。
3) spring-boot-autoconfigure
这个模块跟SpringBoot的自动配置有关,也是SpringBoot的新特性之一。比如SpringBoot能基于类路径来自动配置某个项目模块,自动配置最为关键的注解是@EnableAutoConfiguration
,这个注解能触发Spring上下文的自动配置。另外一个重要的注解是@Conditional
。
举个栗子,若
HSQLDB
在项目的类路径中,且我们没有配置任何其他数据库的连接,此时自动配置就会自动根据类路径来创建相应的bean
。
除了根据类路径来进行自动配置外,还有根据容器中是否存在某个bean等方式来进行自动配置,这里不会进入到具体细节中。
4) spring-boot-starters
这个模块是跟SpringBoot的起步依赖有关,也是SpringBoot的新特性之一。SpringBoot通过提供众多起步依赖降低项目依赖的复杂度。起步依赖其实就是利用maven项目模型将其他相关的依赖给聚合起来,里面各种依赖的版本号都给定义好,避免用户在引入依赖时出现各种版本冲突,方便了我们的使用。
举个栗子,我们要用到activemq时,此时可以直接引入
spring-boot-starter-activemq
起步依赖即可,若SpringBoot官网或第三方组织没有提供相应的SpringBoot起步依赖时,此时我们可以进行定制自己的起步依赖。
注意,该模块没有代码,主要是通过maven的pom.xml来组织各种依赖。
5) spring-boot-cli
Spring Boot CLI是一个命令行工具,如果您想使用Spring快速开发,可以使用它。它允许您运行Groovy脚本,这意味着您有一个熟悉的类似Java的语法,而没有那么多样板代码。您还可以引导一个新项目或编写自己的命令。
6) spring-boot-actuator
这个跟SpringBoot的监控有关,也是SpringBoot的新特性之一。可以通过HTTP端点或JMX等来管理和监控应用。审计、运行状况和度量收集可以自动应用到应用程序。这个监控模块是开箱即用的,提供了一系列端点包括HealthEndpoint
, EnvironmentEndpoint
和BeansEndpoint
等端点。
7) spring-boot-actuator-autoconfigure
这个模块为监控模块提供自动配置的功能,通常也是根据类路径来进行配置。比如Micrometer
存在于类路径中,那么将会自动配置MetricsEndpoint
。
8) spring-boot-test
这个模式是spring-boot的跟测试有关的模块,包含了一些帮助我们测试的核心类和注解(比如@SpringBootTest
)。
9) spring-boot-dependencies
这个模块也没有代码,主要是定义了一些SpringBoot的maven相关的一些依赖及其版本。
10) spring-boot-devtools
这个模块跟SpringBoot的热部署有关,即修改代码后无需重启应用即生效。
11) spring-boot-docs
这个模块应该是跟文档相关的模块。
12) spring-boot-properties-migrator
看到 migrator 这个单词,估计就是跟项目迁移有关,没有去细
究。
13) spring-boot-test-autoconfigure
这个模块一看就是跟SpringBoot的测试的自动配置有关。
14) spring-boot-tools
这个模块一看就是SpringBoot的工具相关的模块,提供了加载,maven插件,metadata和后置处理相关的支持。
上面介绍了这么多spring-boot模块下的子模块,不用慌,我们要进行解读的模块不多,我们真正要看的模块有spring-boot
,spring-boot-autoconfigure
,spring-boot-starters
和spring-boot-actuator
模块。
5 用一个思维导图来总结下SpringBoot源码项目的脉络
6 SpringBoot模块之间的pom关系详解
前面弄清楚了SpringBoot的各个模块的具体功能,此时我们来看下SpringBoot模块的pom之间的关系是怎样的,因为项目是通过maven构建的,因此还是有必要去研究下这块关系滴。
先看SpringBoot源码项目的pom关系,如下图:
根据上图可得出以下结论:
spring-boot-build(pom.xml)
是项目的根pom,其子pom有spring-boot-project(pom.xml)
和spring-boot-dependencies(pom.xml)
;spring-boot-dependencies(pom.xml)
主要定义了SpringBoot项目的各种依赖及其版本,其子pom有spring-boot-parent(pom.xml)
和spring-boot-starter-parent(pom.xml)
;spring-boot-project(pom.xml)
起到聚合module的作用,其子模块并不继承于它,而是继承于spring-boot-parent(pom.xml)
;spring-boot-parent(pom.xml)
是spring-boot-project(pom.xml)
的子module,但继承的父pom为spring-boot-dependencies(pom.xml)
,其定义了一些properties等相关的东西。其子pom为spring-boot-project(pom.xml)
的子module(注意除去spring-boot-dependencies(pom.xml)
),比如有spring-boot(pom.xml)
,spring-boot-starters(pom.xml)
和spring-boot-actuator(pom.xml)
等;spring-boot-starters(pom.xml)
是所有具体起步依赖的父pom,其子pom有spring-boot-starter-data-jdbc(pom.xml)
和spring-boot-starter-data-redis(pom.xml)
等。spring-boot-starter-parent(pom.xml)
,是我们的所有具体SpringBoot项目的父pom,比如SpringBoot自带的样例的spring-boot-samples(pom.xml)
是继承于它的。
SpringBoot的各模块之间的pom关系有点复杂,确实有点绕,如果看完上面的图片和解释还是不太清楚的话,建议小伙伴们自己打开idea的项目,逐个去捋一下。总之记得SpringBoot的一些父pom无非是做了一些版本管理,聚合模块之间的事情。
5 小结
好了,前面已经把SpringBoot源码项目的各个模块的功能和模块pom之间的关系给捋清楚了,总之刚开始分析项目源码,有一个整体的大局观很重要。
助力SpringBoot自动配置的条件注解ConditionalOnXXX分析--SpringBoot源码(三)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
1 前言
本篇接
如何分析SpringBoot源码模块及结构?--SpringBoot源码(二)
上一篇分析了SpringBoot源码结构及各个模块pom之间的关系后,那么此篇开始就开始解开SpringBoot新特性之一--自动配置的神秘面纱了。因为SpringBoot自动配置原理是基于其大量的条件注解ConditionalOnXXX
,因此,本节我们先来撸下Spring的条件注解的相关源码。
2 SpringBoot的派生条件注解
我们都知道,SpringBoot自动配置是需要满足相应的条件才会自动配置,因此SpringBoot的自动配置大量应用了条件注解ConditionalOnXXX
。如下图:
那么上图的条件注解如何使用呢?
举个栗子,我们来看下如何使用
@ConditionalOnClass
和@ConditionalOnProperty
这两个注解,先看下图代码:HelloWorldEnableAutoConfiguration
这个自动配置类应用了@ConditionalOnClass
和ConditionalOnProperty
两个条件注解,那么只有在满足:classpath
中存在HelloWorldComponent.class
和配置了hello.world.name
和hello.world.age
属性这两个条件的情况下才会创建HelloWorldComponent
这个bean
。
其实SpringBoot的@ConditionalOnXXX
等条件注解都是派生注解,那么什么是派生注解呢?
就拿上面的栗子来说,以@ConditionalOnClass(HelloWorldComponent.class)
为例,我们打开ConditionalOnClass
注解源码,如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
可以看到@ConditionalOnClass
注解上面又标注了@Conditional(OnClassCondition.class)
注解,因此@ConditionalOnClass
是@Conditional
的派生注解,@Conditional(OnClassCondition.class)
和@ConditionalOnClass
注解是等价的,即这两个注解标注在某个配置类上的效果是等价的。
而SpringBoot的自动配置原理正是建立在这些大量的派生条件注解@ConditionalOnXXX
之上,而这些条件注解的原理跟Spring的Condition接口有关。因此我们先来研究下Condition接口的相关源码。
3 Condition接口
3.1 Condition接口源码分析
分析Condition接口源码前先看下如何自定义ConditionalOnXXX
注解,举个栗子,比如自定义一个@ConditionalOnLinux
注解,该注解只有在其属性environment
是"linux"才会创建相关的bean。定义了以下代码:
/**
* 实现spring 的Condition接口,并且重写matches()方法,如果@ConditionalOnLinux的注解属性environment是linux就返回true
*
*/
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获得注解@ConditionalOnLinux的所有属性
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
metadata.getAllAnnotationAttributes(
ConditionalOnLinux.class.getName()));
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
// 获得注解@ConditionalOnLinux的environment属性
String environment = annotationAttributes.getString("environment");
// 若environment等于linux,则返回true
if ("linux".equals(environment)) {
return true;
}
}
return false;
}
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(LinuxCondition.class)
public @interface ConditionalOnLinux {
// 标注是哪个环境
String environment() default "";
}
@Configuration
public class ConditionConfig {
// 只有`@ConditionalOnLinux`的注解属性`environment`是"linux"时才会创建bean
@Bean
@ConditionalOnLinux(environment = "linux")
public Environment linuxEnvironment() {
return new LinuxEnvironment();
}
}
上面的代码我们捋一下:
LinuxCondition
实现了Condition
接口并实现了matches
方法,而matches
方法则判断@ConditionalOnLinux
的注解属性environment
是否"linux",是则返回true,否则false。- 然后我们再定义了一个注解
@ConditionalOnLinux
,这个注解是@Conditional
的派生注解,与@Conditional(LinuxCondition.class)
等价,注意@ConditionalOnLinux
注解定义了一个属性environment
。而我们最终可以利用LinuxCondition
的matches
方法中的参数AnnotatedTypeMetadata
来获取@ConditionalOnLinux
的注解属性environment
的值,从而用来判断值是否为linux"。 - 最后我们又定义了一个配置类
ConditionConfig
,在linuxEnvironment
方法上标注了@ConditionalOnLinux(environment = "linux")
。因此,这里只有LinuxCondition
的matches
方法返回true才会创建bean
。
学会了如何自定义@ConditionalOnXXX
注解后,我们现在再来看下Condition
接口的源码:
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
Condition接口主要有一个matches
方法,该方法决定了是否要注册相应的bean
对象。其中matches
方法中有两个参数,参数类型分别是ConditionContext
和AnnotatedTypeMetadata
,这两个参数非常重要。它们分别用来获取一些环境信息和注解元数据从而用在matches
方法中判断是否符合条件。
ConditionContext
,顾名思义,主要是跟Condition
的上下文有关,主要用来获取Registry
,BeanFactory
,Environment
,ResourceLoader
和ClassLoader
等。那么获取这些用来干什么呢?举个栗子,比如OnResourceCondition
需要靠ConditionContext
来获取ResourceLoader
来加载指定资源,OnClassCondition
需要靠ConditionContext
来获取ClassLoader
来加载指定类等,下面看下其源码:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
@Nullable
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
@Nullable
ClassLoader getClassLoader();
}
AnnotatedTypeMetadata
,这个跟注解元数据有关,利用AnnotatedTypeMetadata
可以拿到某个注解的一些元数据,而这些元数据就包含了某个注解里面的属性,比如前面的栗子,利用AnnotatedTypeMetadata
可以拿到@ConditionalOnLinux
的注解属性environment
的值。下面看下其源码:
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName);
Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString);
}
回到刚才的栗子,我们知道@ConditionalOnLinux
注解真正起作用的是Condition
接口的具体实现类LinuxCondition
的matches
方法,那么这个matches
方法是在何时被调用的呢?
通过idea调试看调用的栈帧,如下图:
发现是在ConditionEvaluator
的shouldSkip
方法中调用了LinuxCondition
的matches
方法,自然我们再去看看ConditionEvaluator
的shouldSkip
的方法执行了什么逻辑。
// 这个方法主要是如果是解析阶段则跳过,如果是注册阶段则不跳过
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
// 若没有被@Conditional或其派生注解所标注,则不会跳过
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
// 没有指定phase,注意phase可以分为PARSE_CONFIGURATION或REGISTER_BEAN类型
if (phase == null) {
// 若标有@Component,@Import,@Bean或@Configuration等注解的话,则说明是PARSE_CONFIGURATION类型
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
// 否则是REGISTER_BEAN类型
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
List<Condition> conditions = new ArrayList<>();
// TODO 获得所有标有@Conditional注解或其派生注解里面的Condition接口实现类并实例化成对象。
// 比如@Conditional(OnBeanCondition.class)则获得OnBeanCondition.class,OnBeanCondition.class往往实现了Condition接口
for (String[] conditionClasses : getConditionClasses(metadata)) {
// 将类实例化成对象
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
// 排序,即按照Condition的优先级进行排序
AnnotationAwareOrderComparator.sort(conditions);
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
// 从condition中获得对bean是解析还是注册
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
// 若requiredPhase为null或获取的阶段类型正是当前阶段类型且不符合condition的matches条件,则跳过
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
shouldSkip
这个方法执行的逻辑主要是如果是解析阶段则跳过,如果是注册阶段则不跳过;如果是在注册阶段即REGISTER_BEAN
阶段的话,此时会得到所有的Condition
接口的具体实现类并实例化这些实现类,然后再执行下面关键的代码进行判断是否需要跳过。
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
上面代码最重要的逻辑是调用了Condition
接口的具体实现类的matches
方法,若matches
返回false
,则跳过,不进行注册bean
的操作;若matches
返回true
,则不跳过,进行注册bean
的操作;
好了,Condition
的源码分析就到此为止,再往上翻调用方法的话应该就是Spring加载bean
定义的相关源码了,不属于这里的分析范围。
3.2 Spring的内置Condition接口实现类
前面我们学会了如何自定义条件注解及Condition
的源码分析,那么我们不禁好奇,Spring究竟内置了哪些Condition
接口的实现类呢?
那么看下Spring的Condition
接口的具体实现类的类图:
发现Spring内置的Condition
接口的具体实现类虽然有多个,但只有ProfileCondition
不是测试相关的,因此可以说真正的内置的Condition
接口的具体实现类只有ProfileCondition
一个,非常非常少,这跟SpringBoot的大量派生条件注解形成了鲜明的对比。ProfileCondition
大家都知道,是跟环境有关,比如我们平时一般有dev
,test
和prod
环境,而ProfileCondition
就是判断我们项目配置了哪个环境的。下面是ProfileCondition
的源码,很简单,这里就不分析了。
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
4 SpringBootCondition源码解析
前面看到Spring对Condition
的内置注解可以说只有ProfileCondition
一个,但是我们都知道,SpringBoot则内置了大量的条件注解ConditionalOnXXX
。在分析前,我们先来看一下SpringBootCondition
的整体类图来个整体的理解,如下图:
可以看到SpringBootCondition
作为SpringBoot条件注解的基类,处于整个类图的中心,它实现了Condition
接口,然后又有很多具体的子类OnXXXCondition
,这些OnXXXCondition
其实就是@ConditionalOnXXX
的条件类。
我们先来看下SpringBootCondition
这个父类是主要做了哪些事情,抽象了哪些共有的逻辑?
SpringBootConditon
实现了Condition
接口,作为SpringBoot众多条件注解OnXXXCondtion
的父类,它的作用主要就是打印一些条件注解评估报告的日志,比如打印哪些配置类是符合条件注解的,哪些是不符合的。打印的日志形式如下图:
因为SpringBootConditon
实现了Condition
接口,也实现了matches
方法,因此该方法同样也是被ConditionEvaluator
的shouldSkip
方法中调用,因此我们就以SpringBootConditon
的matches
方法为入口去进行分析。直接上代码:
// SpringBootCondition.java
public final boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 得到metadata的类名或方法名
String classOrMethodName = getClassOrMethodName(metadata);
try {
// 判断每个配置类的每个条件注解@ConditionalOnXXX是否满足条件,然后记录到ConditionOutcome结果中
// 注意getMatchOutcome是一个抽象模板方法,交给OnXXXCondition子类去实现
ConditionOutcome outcome = getMatchOutcome(context, metadata);
// 打印condition评估的日志,哪些条件注解@ConditionalOnXXX是满足条件的,哪些是不满足条件的,这些日志都打印出来
logOutcome(classOrMethodName, outcome);
// 除了打印日志外,这些是否匹配的信息还要记录到ConditionEvaluationReport中
recordEvaluation(context, classOrMethodName, outcome);
// 最后返回@ConditionalOnXXX是否满足条件
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException(
"Could not evaluate condition on " + classOrMethodName + " due to "
+ ex.getMessage() + " not "
+ "found. Make sure your own configuration does not rely on "
+ "that class. This can also happen if you are "
+ "@ComponentScanning a springframework package (e.g. if you "
+ "put a @ComponentScan in the default package by mistake)",
ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException(
"Error processing condition on " + getName(metadata), ex);
}
}
上面代码的注释已经非常详细,我们知道了SpringBootCondition
抽象了所有其具体实现类OnXXXCondition
的共有逻辑--condition
评估信息打印,最重要的是封装了一个模板方法getMatchOutcome(context, metadata)
,留给各个OnXXXCondition
具体子类去覆盖实现属于自己的判断逻辑,然后再返回相应的匹配结果给SpringBootCondition
用于日志打印。
因此我们知道了SpringBootCondition
其实就是用来打印condition
评估信息的,对于其他枝节方法我们不必追究过深,免得丢了主线。我们现在的重点是放在交给OnXXXCondition
子类实现的模板方法上getMatchOutcome(context, metadata);
,因为这个方法将会由很多OnXXXCondition
覆盖重写判断逻辑,这里是我们接下来分析的重点。
因为SpringBootCondition
有众多具体实现类,下面只挑OnResourceCondition
,OnBeanCondition
和OnWebApplicationCondition
进行讲解,而AutoConfigurationImportFilter
跟自动配置有关,则留到自动配置源码解析的时候再进行分析。
4.1 OnResourceCondition源码分析
现在先来看下一个逻辑及其简单的注解条件类OnResourceCondition
,OnResourceCondition
继承了SpringBootCondition
父类,覆盖了其getMatchOutcome
方法,用于@ConditionalOnResource
注解指定的资源存在与否。OnResourceCondition
的判断逻辑非常简单,主要拿到@ConditionalOnResource
注解指定的资源路径后,然后用ResourceLoader
根据指定路径去加载看资源存不存在。下面直接看代码:
先来看下@ConditionalOnResource
的代码,
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnResourceCondition.class)
public @interface ConditionalOnResource {
/**
* The resources that must be present.
* @return the resource paths that must be present.
*/
String[] resources() default {};
}
再来看OnResourceCondition
的代码:
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnResourceCondition extends SpringBootCondition {
private final ResourceLoader defaultResourceLoader = new DefaultResourceLoader();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 获得@ConditionalOnResource注解的属性元数据
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
// 获得资源加载器,若ConditionContext中有ResourceLoader则用ConditionContext中的,没有则用默认的
ResourceLoader loader = (context.getResourceLoader() != null)
? context.getResourceLoader() : this.defaultResourceLoader;
List<String> locations = new ArrayList<>();
// 将@ConditionalOnResource中定义的resources属性值取出来装进locations集合
collectValues(locations, attributes.get("resources"));
Assert.isTrue(!locations.isEmpty(),
"@ConditionalOnResource annotations must specify at "
+ "least one resource location");
// missing集合是装不存在指定资源的资源路径的
List<String> missing = new ArrayList<>();
// 遍历所有的资源路径,若指定的路径的资源不存在则将其资源路径存进missing集合中
for (String location : locations) {
// 这里针对有些资源路径是Placeholders的情况,即处理${}
String resource = context.getEnvironment().resolvePlaceholders(location);
if (!loader.getResource(resource).exists()) {
missing.add(location);
}
}
// 如果存在某个资源不存在,那么则报错
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnResource.class)
.didNotFind("resource", "resources").items(Style.QUOTE, missing));
}
// 所有资源都存在,那么则返回能找到就提的资源
return ConditionOutcome
.match(ConditionMessage.forCondition(ConditionalOnResource.class)
.found("location", "locations").items(locations));
}
// 将@ConditionalOnResource中定义的resources属性值取出来装进locations集合
private void collectValues(List<String> names, List<Object> values) {
for (Object value : values) {
for (Object item : (Object[]) value) {
names.add((String) item);
}
}
}
}
可以看到OnResourceCondition
的getMatchOutcome
方法非常简单,这里不再详述。
4.2 OnBeanCondition源码分析
OnBeanCondition
同样继承了FilteringSpringBootCondition
父类,覆盖了父类FilteringSpringBootCondition
的getOutcomes
方法。而FilteringSpringBootCondition
又是SpringBootCondition
的子类,FilteringSpringBootCondition
跟自动配置类过滤有关,这里先不分析。值得注意的是OnBeanCondition
同样重写了SpringBootCondition
的getMatchOutcome
方法,用来判断Spring容器中是否存在指定条件的bean
。同时是OnBeanCondition
是@ConditionalOnBean
,@ConditionalOnSingleCandidate
和ConditionalOnMissingBean
的条件类。
同样,先来看OnBeanCondition
复写父类SpringBootCondition
的getMatchOutcome
方法的代码:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
// (1),配置类(metadata)标注@ConditionalOnBean注解的情况
if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
// 将@ConditionalOnBean注解属性封装进BeanSearchSpec对象中
// 注意BeanSearchSpec是一个静态内部类,用来存储@ConditionalOnBean和@ConditionalOnMissingBean注解的属性值
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnBean.class);
// 调用getMatchingBeans得到符合条件的bean
MatchResult matchResult = getMatchingBeans(context, spec);
// 如果不匹配
if (!matchResult.isAllMatched()) {
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnBean.class, spec).because(reason));
}
// 如果匹配
matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec)
.found("bean", "beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches());
}
// (2),配置类(metadata)标注@ConditionalOnSingleCandidate注解的情况
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,
ConditionalOnSingleCandidate.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnSingleCandidate.class, spec)
.didNotFind("any beans").atAll());
}
else if (!hasSingleAutowireCandidate(context.getBeanFactory(),
matchResult.getNamesOfAllMatches(),
spec.getStrategy() == SearchStrategy.ALL)) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnSingleCandidate.class, spec)
.didNotFind("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
}
matchMessage = matchMessage
.andCondition(ConditionalOnSingleCandidate.class, spec)
.found("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches());
}
// (3),配置类(metadata)标注@ConditionalOnMissingBean注解的情况
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
String reason = createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnMissingBean.class, spec)
.because(reason));
}
matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
.didNotFind("any beans").atAll();
}
// 最终返回matchMessage
return ConditionOutcome.match(matchMessage);
}
我们可以看到OnBeanCondition
类覆盖的getMatchOutcome
方法分别处理了标注@ConditionalOnBean
,@ConditionalOnSingleCandidate
和@ConditionalOnMissingBean
注解的情况,分别对应上面代码注释的(1)
,(2)
和(3)
处。
现在我们只看针对@ConditionalOnBean
注解的处理逻辑,从上面代码中可以看到若配置类(metadata)标注@ConditionalOnBean
注解的话,主要做了以下事情:
- 将该注解属性提取出来封装进
BeanSearchSpec
对象中; - 然后调用
getMatchingBeans(context, spec)
方法来获取是否有匹配的bean
; - 最后返回
bean
的匹配情况;
可以看到最重要的逻辑是第2步,那么我们再来看下getMatchingBeans
方法,直接上代码:
protected final MatchResult getMatchingBeans(ConditionContext context,
BeanSearchSpec beans) {
// 获得Spring容器的beanFactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 判断bean的搜索策略是否是SearchStrategy.ANCESTORS策略
if (beans.getStrategy() == SearchStrategy.ANCESTORS) {
BeanFactory parent = beanFactory.getParentBeanFactory();
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
"Unable to use SearchStrategy.PARENTS");
beanFactory = (ConfigurableListableBeanFactory) parent;
}
// MatchResult用来存储bean的匹配结果
MatchResult matchResult = new MatchResult();
// 如果bean的搜索策略不是SearchStrategy.CURRENT的话,则置considerHierarchy为true
boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
// 获取TypeExtractor,TypeExtractor是用来判断bean的类型的
TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader());
// 获取是否有被忽略bean类型,若有的话将该bean类型的名称装进beansIgnoredByType集合
// 这里主要是针对@ConditionalOnMissingBean的ignored属性
List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(
beans.getIgnoredTypes(), typeExtractor, beanFactory, context,
considerHierarchy);
// 遍历bean的所有类型
for (String type : beans.getTypes()) {
// 调用getBeanNamesForType方法根据bean类型得到所有符合条件的bean类型,并放到typeMatches集合
Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
typeExtractor, context.getClassLoader(), considerHierarchy);
// 移除掉Ignored的类型
typeMatches.removeAll(beansIgnoredByType);
// 若typeMatches为空,那么则说明正在遍历的这个type类型不符合匹配条件,此时用matchResult记录一下这个不符合条件的类型
if (typeMatches.isEmpty()) {
matchResult.recordUnmatchedType(type);
}
// 若typeMatches不为空,那么则说明正在遍历的这个type类型符合匹配条件,此时用matchResult记录一下这个符合条件的类型
else {
matchResult.recordMatchedType(type, typeMatches);
}
}
// 这里针对@ConditionalOnBean等注解的annotation属性的处理
for (String annotation : beans.getAnnotations()) {
List<String> annotationMatches = Arrays
.asList(getBeanNamesForAnnotation(beanFactory, annotation,
context.getClassLoader(), considerHierarchy));
annotationMatches.removeAll(beansIgnoredByType);
if (annotationMatches.isEmpty()) {
matchResult.recordUnmatchedAnnotation(annotation);
}
else {
matchResult.recordMatchedAnnotation(annotation, annotationMatches);
}
}
// 这里针对@ConditionalOnBean等注解的name属性的处理
for (String beanName : beans.getNames()) {
// beansIgnoredByType集合不包含beanName且beanFactory包含这个bean,则匹配
if (!beansIgnoredByType.contains(beanName)
&& containsBean(beanFactory, beanName, considerHierarchy)) {
matchResult.recordMatchedName(beanName);
}
// 否则,不匹配
else {
matchResult.recordUnmatchedName(beanName);
}
}
// 最后返回匹配结果
return matchResult;
}
上面的逻辑主要是从spring容器中搜索有无指定条件的bean
,搜索Spring容器搜索bean的话有三种搜索策略,分别是CURRENT
,ANCESTORS
和ALL
,分表表示只从当前的context
中搜索bean
,只从父context
中搜索bean
和从整个context
中搜索bean
;定义了搜索策略后,然后再根据BeanSearchSpec
对象封装的注解属性分别取指定的容器中查找有无符合条件的bean
,然后再进行一些过滤。比如@ConditionalOnMissingBean
注解有定义ignored
属性值,那么从容器中搜索到有符合条件的bean
时,此时还要移除掉ignored
指定的bean
。
好了,上面就已经分析了OnBeanCondition
这个条件类了,我们坚持主线优先的原则,具体的细节代码不会深究。
4.3 OnWebApplicationCondition
OnWebApplicationCondition
同样继承了FilteringSpringBootCondition
父类,覆盖了父类FilteringSpringBootCondition
的getOutcomes
方法。而FilteringSpringBootCondition
又是SpringBootCondition
的子类,FilteringSpringBootCondition
跟自动配置类过滤有关,这里先不分析。值得注意的是OnWebApplicationCondition
同样重写了SpringBootCondition
的getMatchOutcome
方法,用来判断当前应用是否web应用。同时是OnWebApplicationCondition
是@ConditionalOnWebApplication
的条件类。
同样,先来看OnWebApplicationCondition
重写SpringBootCondition
的getMatchOutcome
方法:
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 配置类是否标注有@ConditionalOnWebApplication注解
boolean required = metadata
.isAnnotated(ConditionalOnWebApplication.class.getName());
// 调用isWebApplication方法返回匹配结果
ConditionOutcome outcome = isWebApplication(context, metadata, required);
// 若有标注@ConditionalOnWebApplication但不符合条件,则返回不匹配
if (required && !outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
// 若没有标注@ConditionalOnWebApplication但符合条件,则返回不匹配
if (!required && outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
// 这里返回匹配的情况,TODO 不过有个疑问:如果没有标注@ConditionalOnWebApplication注解,又不符合条件的话,也会执行到这里,返回匹配?
return ConditionOutcome.match(outcome.getConditionMessage());
}
上面代码的逻辑很简单,主要是调用isWebApplication
方法来判断当前应用是否是web应用。因此,我们再来看下isWebApplication
方法:
private ConditionOutcome isWebApplication(ConditionContext context,
AnnotatedTypeMetadata metadata, boolean required) {
// 调用deduceType方法判断是哪种类型,其中有SERVLET,REACTIVE和ANY类型,其中ANY表示了SERVLET或REACTIVE类型
switch (deduceType(metadata)) {
// SERVLET类型
case SERVLET:
return isServletWebApplication(context);
// REACTIVE类型
case REACTIVE:
return isReactiveWebApplication(context);
default:
return isAnyWebApplication(context, required);
}
}
在isWebApplication
方法中,首先从@ConditionalOnWebApplication
注解中获取其定义了什么类型,然后根据不同的类型进入不同的判断逻辑。这里我们只看下SERVLET
的情况判断处理,看代码:
private ConditionOutcome isServletWebApplication(ConditionContext context) {
ConditionMessage.Builder message = ConditionMessage.forCondition("");
// 若classpath中不存在org.springframework.web.context.support.GenericWebApplicationContext.class,则返回不匹配
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS,
context.getClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("servlet web application classes").atAll());
}
// 若classpath中存在org.springframework.web.context.support.GenericWebApplicationContext.class,那么又分为以下几种匹配的情况
// session
if (context.getBeanFactory() != null) {
String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, "session")) {
return ConditionOutcome.match(message.foundExactly("'session' scope"));
}
}
// ConfigurableWebEnvironment
if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
return ConditionOutcome
.match(message.foundExactly("ConfigurableWebEnvironment"));
}
// WebApplicationContext
if (context.getResourceLoader() instanceof WebApplicationContext) {
return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
}
// 若以上三种都不匹配的话,则说明不是一个servlet web application
return ConditionOutcome.noMatch(message.because("not a servlet web application"));
}
对于是SERVLET
的情况,首先根据classpath
中是否存在org.springframework.web.context.support.GenericWebApplicationContext.class
,如果不存在该类,则直接返回不匹配;若存在的话那么又分为以下几种匹配的情况:
- session
- ConfigurableWebEnvironment
- WebApplicationContext
若上面三种情况都不匹配,则说明不是一个servlet web application。
4.4 其他
由于springboot的OnXXXCondition
类实现太多,不可能每个条件类都分析一遍,因此上面只分析了OnResourceCondition
,OnBeanCondition
和onWebApplicationCondition
的源码。我们分析源码不可能把所有代码都通读一遍的,阅读源码的话,只要理解了某个模块的类之间的关系及挑几个有代表性的类分析下就行,不可能一网打尽。
若有时间的话,推荐看下几个我们常用的条件类的源码:OnPropertyCondition
,OnClassCondition
和OnExpressionCondition
等。
5 如何扩展SpringBootCondition
前文我们知道了如何扩展Spring的Condition
接口,那么我们该如何扩展SpringBoot的SpringBootCondition
类呢?
推荐阅读springboot之使用SpringBootCondition获得答案
SpringBoot是如何实现自动配置的?--SpringBoot源码(四)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
1 前言
本篇接
助力SpringBoot自动配置的条件注解ConditionalOnXXX分析--SpringBoot源码(三)
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBoot的条件注解@ConditionalOnXxx的相关源码,现挑重点总结如下:
- SpringBoot的所有
@ConditionalOnXxx
的条件类OnXxxCondition
都是继承于SpringBootCondition
基类,而SpringBootCondition
又实现了Condition
接口。 SpringBootCondition
基类主要用来打印一些条件注解评估报告的日志,这些条件评估信息全部来源于其子类注解条件类OnXxxCondition
,因此其也抽象了一个模板方法getMatchOutcome
留给子类去实现来评估其条件注解是否符合条件。- 前一篇我们也还有一个重要的知识点还没分析,那就是跟过滤自动配置类逻辑有关的
AutoConfigurationImportFilter
接口,这篇文章我们来填一下这个坑。
前面我们分析了跟SpringBoot的自动配置息息相关内置条件注解@ConditionalOnXxx
后,现在我们就开始来撸SpringBoot自动配置的相关源码了。
2 @SpringBootApplication注解
在开始前,我们先想一下,SpringBoot为何一个标注有@SpringBootApplication
注解的启动类通过执行一个简单的run
方法就能实现SpringBoot大量Starter
的自动配置呢?
其实SpringBoot的自动配置就跟@SpringBootApplication
这个注解有关,我们先来看下其这个注解的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ...省略非关键代码
}
@SpringBootApplication
标注了很多注解,我们可以看到其中跟SpringBoot自动配置有关的注解就有一个即@EnableAutoConfiguration
,因此,可以肯定的是SpringBoot的自动配置肯定跟@EnableAutoConfiguration
息息相关(其中@ComponentScan
注解的excludeFilters
属性也有一个类AutoConfigurationExcludeFilter
,这个类跟自动配置也有点关系,但不是我们关注的重点)。
现在我们来打开@EnableAutoConfiguration
注解的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
看到@EnableAutoConfiguration
注解又标有@AutoConfigurationPackage
和@Import(AutoConfigurationImportSelector.class)
两个注解,顾名思义,@AutoConfigurationPackage
注解肯定跟自动配置的包有关,而AutoConfigurationImportSelector
则是跟SpringBoot的自动配置选择导入有关(Spring中的ImportSelector
是用来导入配置类的,通常是基于某些条件注解@ConditionalOnXxxx
来决定是否导入某个配置类)。
因此,可以看出AutoConfigurationImportSelector
类是我们本篇的重点,因为SpringBoot的自动配置肯定有一个配置类,而这个配置类的导入则需要靠AutoConfigurationImportSelector
这个哥们来实现。
接下来我们重点来看AutoConfigurationImportSelector
这个类,完了我们再简单分析下@AutoConfigurationPackage
这个注解的逻辑。
3 如何去找SpringBoot自动配置实现逻辑的入口方法?
可以肯定的是SpringBoot的自动配置的逻辑肯定与AutoConfigurationImportSelector这个类有关,那么我们该如何去找到SpringBoot自动配置实现逻辑的入口方法呢?
在找SpringBoot自动配置实现逻辑的入口方法前,我们先来看下AutoConfigurationImportSelector
的相关类图,好有个整体的理解。看下图:
可以看到AutoConfigurationImportSelector
重点是实现了DeferredImportSelector
接口和各种Aware
接口,然后DeferredImportSelector
接口又继承了ImportSelector
接口。
自然而然的,我们会去关注AutoConfigurationImportSelector
复写DeferredImportSelector
接口的实现方法selectImports
方法,因为selectImports
方法跟导入自动配置类有关,而这个方法往往是程序执行的入口方法。经过调试发现selectImports
方法很具有迷惑性,selectImports
方法跟自动配置相关的逻辑有点关系,但实质关系不大。
此时剧情的发展好像不太符合常理,此时我们又该如何来找到自动配置逻辑有关的入口方法呢?
最简单的方法就是在AutoConfigurationImportSelector
类的每个方法都打上断点,然后调试看先执行到哪个方法。但是我们可以不这么做,我们回想下,自定义一个Starter
的时候我们是不是要在spring.factories
配置文件中配置
EnableAutoConfiguration=XxxAutoConfiguration
因此可以推断,SpringBoot的自动配置原理肯定跟从spring.factories
配置文件中加载自动配置类有关,于是结合AutoConfigurationImportSelector
的方法注释,我们找到了getAutoConfigurationEntry
方法。于是我们在这个方法里面打上一个断点,此时通过调用栈帧来看下更上层的入口方法在哪里,然后我们再从跟自动配置相关的更上层的入口方法开始分析。
通过图1我们可以看到,跟自动配置逻辑相关的入口方法在DeferredImportSelectorGrouping
类的getImports
方法处,因此我们就从DeferredImportSelectorGrouping
类的getImports
方法来开始分析SpringBoot的自动配置源码好了。
4 分析SpringBoot自动配置原理
既然找到ConfigurationClassParser.getImports()方法
是自动配置相关的入口方法,那么下面我们就来真正分析SpringBoot自动配置的源码了。
先看一下getImports
方法代码:
// ConfigurationClassParser.java
public Iterable<Group.Entry> getImports() {
// 遍历DeferredImportSelectorHolder对象集合deferredImports,deferredImports集合装了各种ImportSelector,当然这里装的是AutoConfigurationImportSelector
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
// 【1】,利用AutoConfigurationGroup的process方法来处理自动配置的相关逻辑,决定导入哪些配置类(这个是我们分析的重点,自动配置的逻辑全在这了)
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
// 【2】,经过上面的处理后,然后再进行选择导入哪些配置类
return this.group.selectImports();
}
标【1】
处的的代码是我们分析的重中之重,自动配置的相关的绝大部分逻辑全在这里了,将在4.1 分析自动配置的主要逻辑深入分析。那么this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector())
;主要做的事情就是在this.group
即AutoConfigurationGroup
对象的process
方法中,传入的AutoConfigurationImportSelector
对象来选择一些符合条件的自动配置类,过滤掉一些不符合条件的自动配置类,就是这么个事情,无他。
注:
AutoConfigurationGroup
:是AutoConfigurationImportSelector
的内部类,主要用来处理自动配置相关的逻辑,拥有process
和selectImports
方法,然后拥有entries
和autoConfigurationEntries
集合属性,这两个集合分别存储被处理后的符合条件的自动配置类,我们知道这些就足够了;AutoConfigurationImportSelector
:承担自动配置的绝大部分逻辑,负责选择一些符合条件的自动配置类;metadata
:标注在SpringBoot启动类上的@SpringBootApplication
注解元数据
标【2】
的this.group.selectImports
的方法主要是针对前面的process
方法处理后的自动配置类再进一步有选择的选择导入,将在4.2 有选择的导入自动配置类这小节深入分析。
4.1 分析自动配置的主要逻辑
这里继续深究前面 4 分析SpringBoot自动配置原理这节标【1】
处的this.group.process
方法是如何处理自动配置相关逻辑的。
// AutoConfigurationImportSelector$AutoConfigurationGroup.java
// 这里用来处理自动配置类,比如过滤掉不符合匹配条件的自动配置类
public void process(AnnotationMetadata annotationMetadata,
DeferredImportSelector deferredImportSelector) {
Assert.state(
deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
// 【1】,调用getAutoConfigurationEntry方法得到自动配置类放入autoConfigurationEntry对象中
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(),
annotationMetadata);
// 【2】,又将封装了自动配置类的autoConfigurationEntry对象装进autoConfigurationEntries集合
this.autoConfigurationEntries.add(autoConfigurationEntry);
// 【3】,遍历刚获取的自动配置类
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
// 这里符合条件的自动配置类作为key,annotationMetadata作为值放进entries集合
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
上面代码中我们再来看标【1】
的方法getAutoConfigurationEntry
,这个方法主要是用来获取自动配置类有关,承担了自动配置的主要逻辑。直接上代码:
// AutoConfigurationImportSelector.java
// 获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
// 获取是否有配置spring.boot.enableautoconfiguration属性,默认返回true
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获得@Congiguration标注的Configuration类即被审视introspectedClass的注解数据,
// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
// 将会获取到exclude = FreeMarkerAutoConfiguration.class和excludeName=""的注解数据
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 【1】得到spring.factories文件配置的所有自动配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
// 利用LinkedHashSet移除重复的配置类
configurations = removeDuplicates(configurations);
// 得到要排除的自动配置类,比如注解属性exclude的配置类
// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
// 将会获取到exclude = FreeMarkerAutoConfiguration.class的注解数据
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查要被排除的配置类,因为有些不是自动配置类,故要抛出异常
checkExcludedClasses(configurations, exclusions);
// 【2】将要排除的配置类移除
configurations.removeAll(exclusions);
// 【3】因为从spring.factories文件获取的自动配置类太多,如果有些不必要的自动配置类都加载进内存,会造成内存浪费,因此这里需要进行过滤
// 注意这里会调用AutoConfigurationImportFilter的match方法来判断是否符合@ConditionalOnBean,@ConditionalOnClass或@ConditionalOnWebApplication,后面会重点分析一下
configurations = filter(configurations, autoConfigurationMetadata);
// 【4】获取了符合条件的自动配置类后,此时触发AutoConfigurationImportEvent事件,
// 目的是告诉ConditionEvaluationReport条件评估报告器对象来记录符合条件的自动配置类
// 该事件什么时候会被触发?--> 在刷新容器时调用invokeBeanFactoryPostProcessors后置处理器时触发
fireAutoConfigurationImportEvents(configurations, exclusions);
// 【5】将符合条件和要排除的自动配置类封装进AutoConfigurationEntry对象,并返回
return new AutoConfigurationEntry(configurations, exclusions);
}
AutoConfigurationEntry
方法主要做的事情就是获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费。我们下面总结下AutoConfigurationEntry
方法主要做的事情:
【1】从spring.factories
配置文件中加载EnableAutoConfiguration
自动配置类,获取的自动配置类如图3所示。这里我们知道该方法做了什么事情就行了,后面还会有一篇文章详述spring.factories
的原理;
【2】若@EnableAutoConfiguration
等注解标有要exclude
的自动配置类,那么再将这个自动配置类排除掉;
【3】排除掉要exclude
的自动配置类后,然后再调用filter
方法进行进一步的过滤,再次排除一些不符合条件的自动配置类;这个在稍后会详细分析。
【4】经过重重过滤后,此时再触发AutoConfigurationImportEvent
事件,告诉ConditionEvaluationReport
条件评估报告器对象来记录符合条件的自动配置类;(这个在6 AutoConfigurationImportListener这小节详细分析。)
【5】 最后再将符合条件的自动配置类返回。
总结了AutoConfigurationEntry
方法主要的逻辑后,我们再来细看一下AutoConfigurationImportSelector
的filter
方法:
// AutoConfigurationImportSelector.java
private List<String> filter(List<String> configurations,
AutoConfigurationMetadata autoConfigurationMetadata) {
long startTime = System.nanoTime();
// 将从spring.factories中获取的自动配置类转出字符串数组
String[] candidates = StringUtils.toStringArray(configurations);
// 定义skip数组,是否需要跳过。注意skip数组与candidates数组顺序一一对应
boolean[] skip = new boolean[candidates.length];
boolean skipped = false;
// getAutoConfigurationImportFilters方法:拿到OnBeanCondition,OnClassCondition和OnWebApplicationCondition
// 然后遍历这三个条件类去过滤从spring.factories加载的大量配置类
for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
// 调用各种aware方法,将beanClassLoader,beanFactory等注入到filter对象中,
// 这里的filter对象即OnBeanCondition,OnClassCondition或OnWebApplicationCondition
invokeAwareMethods(filter);
// 判断各种filter来判断每个candidate(这里实质要通过candidate(自动配置类)拿到其标注的
// @ConditionalOnClass,@ConditionalOnBean和@ConditionalOnWebApplication里面的注解值)是否匹配,
// 注意candidates数组与match数组一一对应
/**********************【主线,重点关注】********************************/
boolean[] match = filter.match(candidates, autoConfigurationMetadata);
// 遍历match数组,注意match顺序跟candidates的自动配置类一一对应
for (int i = 0; i < match.length; i++) {
// 若有不匹配的话
if (!match[i]) {
// 不匹配的将记录在skip数组,标志skip[i]为true,也与candidates数组一一对应
skip[i] = true;
// 因为不匹配,将相应的自动配置类置空
candidates[i] = null;
// 标注skipped为true
skipped = true;
}
}
}
// 这里表示若所有自动配置类经过OnBeanCondition,OnClassCondition和OnWebApplicationCondition过滤后,全部都匹配的话,则全部原样返回
if (!skipped) {
return configurations;
}
// 建立result集合来装匹配的自动配置类
List<String> result = new ArrayList<>(candidates.length);
for (int i = 0; i < candidates.length; i++) {
// 若skip[i]为false,则说明是符合条件的自动配置类,此时添加到result集合中
if (!skip[i]) {
result.add(candidates[i]);
}
}
// 打印日志
if (logger.isTraceEnabled()) {
int numberFiltered = configurations.size() - result.size();
logger.trace("Filtered " + numberFiltered + " auto configuration class in "
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
+ " ms");
}
// 最后返回符合条件的自动配置类
return new ArrayList<>(result);
}
AutoConfigurationImportSelector
的filter
方法主要做的事情就是调用AutoConfigurationImportFilter
接口的match
方法来判断每一个自动配置类上的条件注解(若有的话)@ConditionalOnClass
,@ConditionalOnBean
或@ConditionalOnWebApplication
是否满足条件,若满足,则返回true,说明匹配,若不满足,则返回false说明不匹配。
我们现在知道AutoConfigurationImportSelector
的filter
方法主要做了什么事情就行了,现在先不用研究的过深,至于AutoConfigurationImportFilter
接口的match
方法将在5 AutoConfigurationImportFilter这小节再详细分析,填补一下我们前一篇条件注解源码分析中留下的坑。
注意:我们坚持主线优先的原则,其他枝节代码这里不深究,以免丢了主线哈。
4.2 有选择的导入自动配置类
这里继续深究前面 4 分析SpringBoot自动配置原理这节标【2】
处的this.group.selectImports
方法是如何进一步有选择的导入自动配置类的。直接看代码:
// AutoConfigurationImportSelector$AutoConfigurationGroup.java
public Iterable<Entry> selectImports() {
if (this.autoConfigurationEntries.isEmpty()) {
return Collections.emptyList();
}
// 这里得到所有要排除的自动配置类的set集合
Set<String> allExclusions = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getExclusions)
.flatMap(Collection::stream).collect(Collectors.toSet());
// 这里得到经过滤后所有符合条件的自动配置类的set集合
Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getConfigurations)
.flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
// 移除掉要排除的自动配置类
processedConfigurations.removeAll(allExclusions);
// 对标注有@Order注解的自动配置类进行排序,
return sortAutoConfigurations(processedConfigurations,
getAutoConfigurationMetadata())
.stream()
.map((importClassName) -> new Entry(
this.entries.get(importClassName), importClassName))
.collect(Collectors.toList());
}
可以看到,selectImports
方法主要是针对经过排除掉exclude
的和被AutoConfigurationImportFilter
接口过滤后的满足条件的自动配置类再进一步排除exclude
的自动配置类,然后再排序。逻辑很简单,不再详述。
不过有个疑问,前面已经exclude
过一次了,为何这里还要再exclude
一次?
5 AutoConfigurationImportFilter
这里继续深究前面 4.1节的AutoConfigurationImportSelector.filter
方法的过滤自动配置类的boolean[] match = filter.match(candidates, autoConfigurationMetadata);
这句代码。
因此我们继续分析AutoConfigurationImportFilter
接口,分析其match
方法,同时也是对前一篇@ConditionalOnXxx
的源码分析文章中留下的坑进行填补。
AutoConfigurationImportFilter
接口只有一个match
方法用来过滤不符合条件的自动配置类。
@FunctionalInterface
public interface AutoConfigurationImportFilter {
boolean[] match(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata);
}
同样,在分析AutoConfigurationImportFilter
接口的match
方法前,我们先来看下其类关系图:
可以看到,AutoConfigurationImportFilter
接口有一个具体的实现类FilteringSpringBootCondition
,FilteringSpringBootCondition
又有三个具体的子类:OnClassCondition
,OnBeanCondtition
和OnWebApplicationCondition
。
那么这几个类之间的关系是怎样的呢?
FilteringSpringBootCondition
实现了AutoConfigurationImportFilter
接口的match
方法,然后在FilteringSpringBootCondition
的match
方法调用getOutcomes
这个抽象模板方法返回自动配置类的匹配与否的信息。同时,最重要的是FilteringSpringBootCondition
的三个子类OnClassCondition
,OnBeanCondtition
和OnWebApplicationCondition
将会复写这个模板方法实现自己的匹配判断逻辑。
好了,AutoConfigurationImportFilter
接口的整体关系已经清楚了,现在我们再进入其具体实现类FilteringSpringBootCondition
的match
方法看看是其如何根据条件过滤自动配置类的。
// FilteringSpringBootCondition.java
@Override
public boolean[] match(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
// 创建评估报告
ConditionEvaluationReport report = ConditionEvaluationReport
.find(this.beanFactory);
// 注意getOutcomes是模板方法,将spring.factories文件种加载的所有自动配置类传入
// 子类(这里指的是OnClassCondition,OnBeanCondition和OnWebApplicationCondition类)去过滤
// 注意outcomes数组存储的是不匹配的结果,跟autoConfigurationClasses数组一一对应
/*****************************【主线,重点关注】*********************************************/
ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses,
autoConfigurationMetadata);
boolean[] match = new boolean[outcomes.length];
// 遍历outcomes,这里outcomes为null则表示匹配,不为null则表示不匹配
for (int i = 0; i < outcomes.length; i++) {
ConditionOutcome outcome = outcomes[i];
match[i] = (outcome == null || outcome.isMatch());
if (!match[i] && outcomes[i] != null) {
// 这里若有某个类不匹配的话,此时调用父类SpringBootCondition的logOutcome方法打印日志
logOutcome(autoConfigurationClasses[i], outcomes[i]);
// 并将不匹配情况记录到report
if (report != null) {
report.recordConditionEvaluation(autoConfigurationClasses[i], this,
outcomes[i]);
}
}
}
return match;
}
FilteringSpringBootCondition
的match
方法主要做的事情还是调用抽象模板方法getOutcomes
来根据条件来过滤自动配置类,而复写getOutcomes
模板方法的有三个子类,这里不再一一分析,只挑选OnClassCondition
复写的getOutcomes
方法进行分析。
5.1 OnClassCondition
先直接上OnClassCondition
复写的getOutcomes
方法的代码:
// OnClassCondition.java
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
// Split the work and perform half in a background thread. Using a single
// additional thread seems to offer the best performance. More threads make
// things worse
// 这里经过测试用两个线程去跑的话性能是最好的,大于两个线程性能反而变差
int split = autoConfigurationClasses.length / 2;
// 【1】开启一个新线程去扫描判断已经加载的一半自动配置类
OutcomesResolver firstHalfResolver = createOutcomesResolver(
autoConfigurationClasses, 0, split, autoConfigurationMetadata);
// 【2】这里用主线程去扫描判断已经加载的一半自动配置类
OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(
autoConfigurationClasses, split, autoConfigurationClasses.length,
autoConfigurationMetadata, getBeanClassLoader());
// 【3】先让主线程去执行解析一半自动配置类是否匹配条件
ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();
// 【4】这里用新开启的线程取解析另一半自动配置类是否匹配
// 注意为了防止主线程执行过快结束,resolveOutcomes方法里面调用了thread.join()来
// 让主线程等待新线程执行结束,因为后面要合并两个线程的解析结果
ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes();
// 新建一个ConditionOutcome数组来存储自动配置类的筛选结果
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
// 将前面两个线程的筛选结果分别拷贝进outcomes数组
System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length);
System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length);
// 返回自动配置类的筛选结果
return outcomes;
}
可以看到,OnClassCondition
的getOutcomes
方法主要解析自动配置类是否符合匹配条件,当然这个匹配条件指自动配置类上的注解@ConditionalOnClass
指定的类存不存在于classpath
中,存在则返回匹配,不存在则返回不匹配。
由于解析自动配置类是否匹配比较耗时,因此从上面代码中我们可以看到分别创建了firstHalfResolver
和secondHalfResolver
两个解析对象,这两个解析对象个分别对应一个线程去解析加载的自动配置类是否符合条件,最终将两个线程的解析自动配置类的匹配结果合并后返回。
那么自动配置类是否符合条件的解析判断过程又是怎样的呢?现在我们分别来看一下上面代码注释标注的【1】
,【2】
,【3】
和【4】
处。
5.1.1 createOutcomesResolver
这里对应前面5.1节的代码注释标注【1】
处的OutcomesResolver firstHalfResolver = createOutcomesResolver(...);
的方法:
// OnClassCondition.java
private OutcomesResolver createOutcomesResolver(String[] autoConfigurationClasses,
int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) {
// 新建一个StandardOutcomesResolver对象
OutcomesResolver outcomesResolver = new StandardOutcomesResolver(
autoConfigurationClasses, start, end, autoConfigurationMetadata,
getBeanClassLoader());
try {
// new一个ThreadedOutcomesResolver对象,并将StandardOutcomesResolver类型的outcomesResolver对象作为构造器参数传入
return new ThreadedOutcomesResolver(outcomesResolver);
}
// 若上面开启的线程抛出AccessControlException异常,则返回StandardOutcomesResolver对象
catch (AccessControlException ex) {
return outcomesResolver;
}
}
可以看到createOutcomesResolver
方法创建了一个封装了StandardOutcomesResolver
类的ThreadedOutcomesResolver
解析对象。
我们再来看下ThreadedOutcomesResolver
这个线程解析类封装StandardOutcomesResolver
这个对象的目的是什么?我们继续跟进代码:
// OnClassCondtion.java
private ThreadedOutcomesResolver(OutcomesResolver outcomesResolver) {
// 这里开启一个新的线程,这个线程其实还是利用StandardOutcomesResolver的resolveOutcomes方法
// 对自动配置类进行解析判断是否匹配
this.thread = new Thread(
() -> this.outcomes = outcomesResolver.resolveOutcomes());
// 开启线程
this.thread.start();
}
可以看到在构造ThreadedOutcomesResolver
对象时候,原来是开启了一个线程,然后这个线程其实还是调用了刚传进来的StandardOutcomesResolver
对象的resolveOutcomes
方法去解析自动配置类。具体如何解析呢?稍后我们在分析【3】
处代码secondHalfResolver.resolveOutcomes();
的时候再深究。
5.1.2 new StandardOutcomesResolver
这里对应前面5.1节的【2】
处的代码OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(...);
,逻辑很简单,就是创建了一个StandardOutcomesResolver
对象,用于后面解析自动配置类是否匹配,同时,新建的一个线程也是利用它来完成自动配置类的解析的。
5.1.3 StandardOutcomesResolver.resolveOutcomes方法
这里对应前面5.1节标注的【3】
的代码ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();
。
这里StandardOutcomesResolver.resolveOutcomes
方法承担了解析自动配置类匹配与否的全部逻辑,是我们要重点分析的方法,resolveOutcomes
方法最终把解析的自动配置类的结果赋给secondHalf
数组。那么它是如何解析自动配置类是否匹配条件的呢?
// OnClassCondition$StandardOutcomesResolver.java
public ConditionOutcome[] resolveOutcomes() {
// 再调用getOutcomes方法来解析
return getOutcomes(this.autoConfigurationClasses, this.start, this.end,
this.autoConfigurationMetadata);
}
private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) { // 只要autoConfigurationMetadata没有存储相关自动配置类,那么outcome默认为null,则说明匹配
ConditionOutcome[] outcomes = new ConditionOutcome[end - start];
// 遍历每一个自动配置类
for (int i = start; i < end; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];
// TODO 对于autoConfigurationMetadata有个疑问:为何有些自动配置类的条件注解能被加载到autoConfigurationMetadata,而有些又不能,比如自己定义的一个自动配置类HelloWorldEnableAutoConfiguration就没有被存到autoConfigurationMetadata中
if (autoConfigurationClass != null) {
// 这里取出注解在AutoConfiguration自动配置类类的@ConditionalOnClass注解的指定类的全限定名,
// 举个栗子,看下面的KafkaStreamsAnnotationDrivenConfiguration这个自动配置类
/**
* @ConditionalOnClass(StreamsBuilder.class)
* class KafkaStreamsAnnotationDrivenConfiguration {
* // 省略无关代码
* }
*/
// 那么取出的就是StreamsBuilder类的全限定名即candidates = org.apache.kafka.streams.StreamsBuilder
String candidates = autoConfigurationMetadata
.get(autoConfigurationClass, "ConditionalOnClass"); // 因为这里是处理某个类是否存在于classpath中,所以传入的key是ConditionalOnClass
// 若自动配置类标有ConditionalOnClass注解且有值,此时调用getOutcome判断是否存在于类路径中
if (candidates != null) {
// 拿到自动配置类注解@ConditionalOnClass的值后,再调用getOutcome方法去判断匹配结果,若该类存在于类路径,则getOutcome返回null,否则非null
/*******************【主线,重点关注】******************/
outcomes[i - start] = getOutcome(candidates);
}
}
}
return outcomes;
}
可以看到StandardOutcomesResolver.resolveOutcomes
的方法中再次调用getOutcomes
方法,主要是从autoConfigurationMetadata
对象中获取到自动配置类上的注解@ConditionalOnClass
指定的类的全限定名,然后作为参数传入getOutcome
方法用于去类路径加载该类,若能加载到则说明注解@ConditionalOnClass
满足条件,此时说明自动配置类匹配成功。
但是别忘了,这里只是过了@ConditionalOnClass
注解这一关,若自动配置类还有其他注解比如@ConditionalOnBean
,若该@ConditionalOnBean
注解不满足条件的话,同样最终结果是不匹配的。这里扯的有点远,我们回到OnClassCondtion
的判断逻辑,继续进入getOutcome
方法看它是如何去判断@ConditionalOnClass
注解满不满足条件的。
// OnClassCondition$StandardOutcomesResolver.java
// 返回的outcome记录的是不匹配的情况,不为null,则说明不匹配;为null,则说明匹配
private ConditionOutcome getOutcome(String candidates) {
// candidates的形式为“org.springframework.boot.autoconfigure.aop.AopAutoConfiguration.ConditionalOnClass=org.aspectj.lang.annotation.Aspect,org.aspectj.lang.reflect.Advice,org.aspectj.weaver.AnnotatedElement”
try {// 自动配置类上@ConditionalOnClass的值只有一个的话,直接调用getOutcome方法判断是否匹配
if (!candidates.contains(",")) {
// 看到因为传入的参数是 ClassNameFilter.MISSING,因此可以猜测这里应该是得到不匹配的结果
/******************【主线,重点关注】********************/
return getOutcome(candidates, ClassNameFilter.MISSING,
this.beanClassLoader);
}
// 自动配置类上@ConditionalOnClass的值有多个的话,则遍历每个值(其值以逗号,分隔)
for (String candidate : StringUtils
.commaDelimitedListToStringArray(candidates)) {
ConditionOutcome outcome = getOutcome(candidate,
ClassNameFilter.MISSING, this.beanClassLoader);
// 可以看到,这里只要有一个不匹配的话,则返回不匹配结果
if (outcome != null) {
return outcome;
}
}
}
catch (Exception ex) {
// We'll get another chance later
}
return null;
}
可以看到,getOutcome
方法再次调用重载方法getOutcome
进一步去判断注解@ConditionalOnClass
指定的类存不存在类路径中,跟着主线继续跟进去:
// OnClassCondition$StandardOutcomesResolver.java
private ConditionOutcome getOutcome(String className,
ClassNameFilter classNameFilter, ClassLoader classLoader) {
// 调用classNameFilter的matches方法来判断`@ConditionalOnClass`指定的类存不存在类路径中
/******************【主线,重点关注】********************/
if (classNameFilter.matches(className, classLoader)) { // 这里调用classNameFilter去判断className是否存在于类路径中,其中ClassNameFilter又分为PRESENT和MISSING两种;目前只看到ClassNameFilter为MISSING的调用情况,所以默认为true的话记录不匹配信息;若传入ClassNameFilter为PRESENT的话,估计还要再写一个else分支
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnClass.class)
.didNotFind("required class").items(Style.QUOTE, className));
}
return null;
}
我们一层一层的剥,最终剥到了最底层了,这个真的需要足够耐心,没办法,源码只能一点一点的啃,嘿嘿。可以看到最终是调用ClassNameFilter
的matches
方法来判断@ConditionalOnClass
指定的类存不存在类路径中,若不存在的话,则返回不匹配。
我们继续跟进ClassNameFilter
的源码:
// FilteringSpringBootCondition.java
protected enum ClassNameFilter {
// 这里表示指定的类存在于类路径中,则返回true
PRESENT {
@Override
public boolean matches(String className, ClassLoader classLoader) {
return isPresent(className, classLoader);
}
},
// 这里表示指定的类不存在于类路径中,则返回true
MISSING {
@Override
public boolean matches(String className, ClassLoader classLoader) {
return !isPresent(className, classLoader); // 若classpath不存在className这个类,则返回true
}
};
// 这又是一个抽象方法,分别被PRESENT和MISSING枚举类实现
public abstract boolean matches(String className, ClassLoader classLoader);
// 检查指定的类是否存在于类路径中
public static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
// 利用类加载器去加载相应类,若没有抛出异常则说明类路径中存在该类,此时返回true
try {
forName(className, classLoader);
return true;
}// 若不存在于类路径中,此时抛出的异常将catch住,返回false。
catch (Throwable ex) {
return false;
}
}
// 利用类加载器去加载指定的类
private static Class<?> forName(String className, ClassLoader classLoader)
throws ClassNotFoundException {
if (classLoader != null) {
return classLoader.loadClass(className);
}
return Class.forName(className);
}
}
可以看到ClassNameFilter
原来是FilteringSpringBootCondition
的一个内部枚举类,其实现了判断指定类是否存在于classpath
中的逻辑,这个类很简单,这里不再详述。
5.1.4 ThreadedOutcomesResolver.resolveOutcomes方法
这里对应前面5.1节的标注的【4】
的代码ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes()
。
前面分析5.1.3 StandardOutcomesResolver.resolveOutcomes方法已经刨根追底,陷入细节比较深,现在我们需要跳出来继续看前面标注的【4】
的代码ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes()
的方法哈。
这里是用新开启的线程去调用StandardOutcomesResolver.resolveOutcomes
方法解析另一半自动配置类是否匹配,因为是新线程,这里很可能会出现这么一种情况:主线程解析完属于自己解析的一半自动配置类后,那么久继续往下跑了,此时不会等待新开启的子线程的。
因此,为了让主线程解析完后,我们需要让主线程继续等待正在解析的子线程,直到子线程结束。那么我们继续跟进代码区看下ThreadedOutcomesResolver.resolveOutcomes
方法是怎样实现让主线程等待子线程的:
// OnClassCondition$ThreadedOutcomesResolver.java
public ConditionOutcome[] resolveOutcomes() {
try {
// 调用子线程的Join方法,让主线程等待
this.thread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
// 若子线程结束后,此时返回子线程的解析结果
return this.outcomes;
}
可以看到用了Thread.join()
方法来让主线程等待正在解析自动配置类的子线程,这里应该也可以用CountDownLatch
来让主线程等待子线程结束。最终将子线程解析后的结果赋给firstHalf
数组。
5.2 OnBeanCondition和OnWebApplicationCondition
前面5.1 OnClassCondition节深入分析了OnClassCondition
是如何过滤自动配置类的,那么自动配置类除了要经过OnClassCondition
的过滤,还要经过OnBeanCondition
和OnWebApplicationCondition
这两个条件类的过滤,这里不再详述,有兴趣的小伙伴可自行分析。
6 AutoConfigurationImportListener
这里继续深究前面 4.1节的AutoConfigurationImportSelector.getAutoConfigurationEntry
方法的触发自动配置类过滤完毕的事件fireAutoConfigurationImportEvents(configurations, exclusions);
这句代码。
我们直接点进fireAutoConfigurationImportEvents
方法看看其是如何触发事件的:
// AutoConfigurationImportSelector.java
private void fireAutoConfigurationImportEvents(List<String> configurations,
Set<String> exclusions) {
// 从spring.factories总获取到AutoConfigurationImportListener即ConditionEvaluationReportAutoConfigurationImportListener
List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
if (!listeners.isEmpty()) {
// 新建一个AutoConfigurationImportEvent事件
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
configurations, exclusions);
// 遍历刚获取到的AutoConfigurationImportListener
for (AutoConfigurationImportListener listener : listeners) {
// 这里调用各种Aware方法用于触发事件前赋值,比如设置factory,environment等
invokeAwareMethods(listener);
// 真正触发AutoConfigurationImportEvent事件即回调listener的onXXXEveent方法。这里用于记录自动配置类的评估信息
listener.onAutoConfigurationImportEvent(event);
}
}
}
如上,fireAutoConfigurationImportEvents
方法做了以下两件事情:
- 调用
getAutoConfigurationImportListeners
方法从spring.factoris
配置文件获取实现AutoConfigurationImportListener
接口的事件监听器;如下图,可以看到获取的是ConditionEvaluationReportAutoConfigurationImportListener
:
- 遍历获取的各个事件监听器,然后调用监听器各种
Aware
方法给监听器赋值,最后再依次回调事件监听器的onAutoConfigurationImportEvent
方法,执行监听事件的逻辑。
此时我们再来看下ConditionEvaluationReportAutoConfigurationImportListener
监听器监听到事件后,它的onAutoConfigurationImportEvent
方法究竟做了哪些事情:
// ConditionEvaluationReportAutoConfigurationImportListener.java
public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
if (this.beanFactory != null) {
// 获取到条件评估报告器对象
ConditionEvaluationReport report = ConditionEvaluationReport
.get(this.beanFactory);
// 将符合条件的自动配置类记录到unconditionalClasses集合中
report.recordEvaluationCandidates(event.getCandidateConfigurations());
// 将要exclude的自动配置类记录到exclusions集合中
report.recordExclusions(event.getExclusions());
}
}
可以看到,ConditionEvaluationReportAutoConfigurationImportListener
监听器监听到事件后,做的事情很简单,只是分别记录下符合条件和被exclude
的自动配置类。
7 AutoConfigurationPackages
前面已经详述了SpringBoot的自动配置原理了,最后的最后,跟SpringBoot自动配置有关的注解@AutoConfigurationPackage
还没分析,我们来看下这个注解的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
可以看到@AutoConfigurationPackage
注解是跟SpringBoot自动配置所在的包相关的,即将 添加该注解的类所在的package 作为 自动配置package 进行管理。
接下来我们再看看AutoConfigurationPackages.Registrar
类是干嘛的,直接看源码:
//AutoConfigurationPackages.Registrar.java
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
可以看到Registrar
类是AutoConfigurationPackages
的静态内部类,实现了ImportBeanDefinitionRegistrar
和DeterminableImports
两个接口。现在我们主要来关注下Registrar
实现的registerBeanDefinitions
方法,顾名思义,这个方法是注册bean
定义的方法。看到它又调用了AutoConfigurationPackages
的register
方法,继续跟进源码:
// AutoConfigurationPackages.java
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition
.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0,
addBasePackages(constructorArguments, packageNames));
}
else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,
packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
如上,可以看到register
方法注册了一个packageNames
即自动配置类注解@EnableAutoConfiguration
所在的所在的包名相关的bean
。那么注册这个bean
的目的是为了什么呢?
结合官网注释知道,注册这个自动配置包名相关的bean
是为了被其他地方引用,比如JPA entity scanner
,具体拿来干什么久不知道了,这里不再深究了。
8 小结
好了,SpringBoot的自动配置的源码分析就到这里了,比较长,有些地方也很深入细节,读完需要一定的耐心。
最后,我们再总结下SpringBoot自动配置的原理,主要做了以下事情:
- 从spring.factories配置文件中加载自动配置类;
- 加载的自动配置类中排除掉
@EnableAutoConfiguration
注解的exclude
属性指定的自动配置类; - 然后再用
AutoConfigurationImportFilter
接口去过滤自动配置类是否符合其标注注解(若有标注的话)@ConditionalOnClass
,@ConditionalOnBean
和@ConditionalOnWebApplication
的条件,若都符合的话则返回匹配结果; - 然后触发
AutoConfigurationImportEvent
事件,告诉ConditionEvaluationReport
条件评估报告器对象来分别记录符合条件和exclude
的自动配置类。 - 最后spring再将最后筛选后的自动配置类导入IOC容器中
最后留个自己的疑问,还望知道答案的大佬解答,这里表示感谢:
为了避免加载不必要的自动配置类造成内存浪费,
FilteringSpringBootCondition
用于过滤spring.factories
文件的自动配置类,而FilteringSpringBootCondition
为啥只有OnOnBeanCondition
,OnClassCondition
和onWebApplicationCondition
这三个条件类用于过滤,为啥没有onPropertyCondtion
,onResourceCondition
等条件类来过滤自动配置类呢?
外部配置属性值是如何被绑定到XxxProperties类属性上的?--SpringBoot源码(五)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
1 前言
本篇接 SpringBoot是如何实现自动配置的?--SpringBoot源码(四)
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBoot的自动配置的相关源码,自动配置相关源码主要有以下几个重要的步骤:
-
从spring.factories配置文件中加载自动配置类;
-
加载的自动配置类中排除掉
@EnableAutoConfiguration
注解的exclude
属性指定的自动配置类; -
然后再用
AutoConfigurationImportFilter
接口去过滤自动配置类是否符合其标注注解(若有标注的话)@ConditionalOnClass
,@ConditionalOnBean
和@ConditionalOnWebApplication
的条件,若都符合的话则返回匹配结果; -
然后触发
AutoConfigurationImportEvent
事件,告诉ConditionEvaluationReport
条件评估报告器对象来分别记录符合条件和exclude
的自动配置类。 -
最后spring再将最后筛选后的自动配置类导入IOC容器中
本篇继续来分析SpringBoot的自动配置的相关源码,我们来分析下@EnableConfigurationProperties
和@EnableConfigurationProperties
这两个注解,来探究下外部配置属性值是如何被绑定到@ConfigurationProperties注解的类属性中的?
举个栗子:以配置web项目的服务器端口为例,若我们要将服务器端口配置为
8081
,那么我们会在application.properties
配置文件中配置server.port=8081
,此时该配置值8081
就将会绑定到被@ConfigurationProperties
注解的类ServerProperties
的属性port
上,从而使得配置生效。
2 @EnableConfigurationProperties
我们接着前面的设置服务器端口的栗子来分析,我们先直接来看看ServerProperties
的源码,应该能找到源码的入口:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
/**
* Server HTTP port.
*/
private Integer port;
// ...省略非关键代码
}
可以看到,ServerProperties
类上标注了@ConfigurationProperties
这个注解,服务器属性配置前缀为server
,是否忽略未知的配置值(ignoreUnknownFields
)设置为true
。
那么我们再来看下@ConfigurationProperties
这个注解的源码:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {
// 前缀别名
@AliasFor("prefix")
String value() default "";
// 前缀
@AliasFor("value")
String prefix() default "";
// 忽略无效的配置属性
boolean ignoreInvalidFields() default false;
// 忽略未知的配置属性
boolean ignoreUnknownFields() default true;
}
@ConfigurationProperties
这个注解的作用就是将外部配置的配置值绑定到其注解的类的属性上,可以作用于配置类或配置类的方法上。可以看到@ConfigurationProperties
注解除了有设置前缀,是否忽略一些不存在或无效的配置等属性等外,这个注解没有其他任何的处理逻辑,可以看到@ConfigurationProperties
是一个标志性的注解,源码入口不在这里。
这里讲的是服务器的自动配置,自然而然的,我们来看下自动配置类ServletWebServerFactoryAutoConfiguration
的源码:
@Configuration
@EnableConfigurationProperties(ServerProperties.class)
// ...省略非关键注解
public class ServletWebServerFactoryAutoConfiguration {
// ...省略非关键代码
}
为了突出重点,我已经把ServletWebServerFactoryAutoConfiguration
的非关键代码和非关键注解省略掉了。可以看到,ServletWebServerFactoryAutoConfiguration
自动配置类中有一个@EnableConfigurationProperties
注解,且注解值是前面讲的ServerProperties.class
,因此@EnableConfigurationProperties
注解肯定就是我们关注的重点了。
同样,再来看下@EnableConfigurationProperties
注解的源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
// 这个值指定的类就是@ConfigurationProperties注解标注的类,其将会被注册到spring容器中
Class<?>[] value() default {};
}
@EnableConfigurationProperties
注解的主要作用就是为@ConfigurationProperties
注解标注的类提供支持,即对将外部配置属性值(比如application.properties配置值)绑定到@ConfigurationProperties
标注的类的属性中。
注意:SpringBoot源码中还存在了
ConfigurationPropertiesAutoConfiguration
这个自动配置类,同时spring.factories
配置文件中的EnableAutoConfiguration
接口也配置了ConfigurationPropertiesAutoConfiguration
,这个自动配置类上也有@EnableConfigurationProperties
这个注解,堆属性绑定进行了默认开启。
那么,@EnableConfigurationProperties
这个注解对属性绑定提供怎样的支持呢?
可以看到@EnableConfigurationProperties
这个注解上还标注了@Import(EnableConfigurationPropertiesImportSelector.class)
,其导入了EnableConfigurationPropertiesImportSelector
,因此可以肯定的是@EnableConfigurationProperties
这个注解对属性绑定提供的支持必定跟EnableConfigurationPropertiesImportSelector
有关。
到了这里,EnableConfigurationPropertiesImportSelector
这个哥们是我们接下来要分析的对象,那么我们下面继续来分析EnableConfigurationPropertiesImportSelector
是如何承担将外部配置属性值绑定到@ConfigurationProperties
标注的类的属性中的。
3 EnableConfigurationPropertiesImportSelector
EnableConfigurationPropertiesImportSelector
类的作用主要用来处理外部属性绑定的相关逻辑,其实现了ImportSelector
接口,我们都知道,实现ImportSelector
接口的selectImports
方法可以向容器中注册bean。
那么,我们来看下EnableConfigurationPropertiesImportSelector
覆写的selectImports
方法:
// EnableConfigurationPropertiesImportSelector.java
class EnableConfigurationPropertiesImportSelector implements ImportSelector {
// IMPORTS数组即是要向spring容器中注册的bean
private static final String[] IMPORTS = {
ConfigurationPropertiesBeanRegistrar.class.getName(),
ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
@Override
public String[] selectImports(AnnotationMetadata metadata) {
// 返回ConfigurationPropertiesBeanRegistrar和ConfigurationPropertiesBindingPostProcessorRegistrar的全限定名
// 即上面两个类将会被注册到Spring容器中
return IMPORTS;
}
}
可以看到EnableConfigurationPropertiesImportSelector
类中的selectImports
方法中返回的是IMPORTS
数组,而这个IMPORTS
是一个常量数组,值是ConfigurationPropertiesBeanRegistrar
和ConfigurationPropertiesBindingPostProcessorRegistrar
。即EnableConfigurationPropertiesImportSelector
的作用是向Spring容器中注册了ConfigurationPropertiesBeanRegistrar
和ConfigurationPropertiesBindingPostProcessorRegistrar
这两个bean
。
我们在EnableConfigurationPropertiesImportSelector
类中没看到处理外部属性绑定的相关逻辑,其只是注册了ConfigurationPropertiesBeanRegistrar
和ConfigurationPropertiesBindingPostProcessorRegistrar
这两个bean
,接下来我们再看下注册的这两个bean
类。
4 ConfigurationPropertiesBeanRegistrar
我们先来看下ConfigurationPropertiesBeanRegistrar
这个类。
ConfigurationPropertiesBeanRegistrar
是EnableConfigurationPropertiesImportSelector
的内部类,其实现了ImportBeanDefinitionRegistrar
接口,覆写了registerBeanDefinitions
方法。可见,ConfigurationPropertiesBeanRegistrar
又是用来注册一些bean
definition
的,即也是向Spring
容器中注册一些bean。
先看下ConfigurationPropertiesBeanRegistrar
的源码:
// ConfigurationPropertiesBeanRegistrar$ConfigurationPropertiesBeanRegistrar.java
public static class ConfigurationPropertiesBeanRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, // metadata是AnnotationMetadataReadingVisitor对象,存储了某个配置类的元数据
BeanDefinitionRegistry registry) {
// (1)得到@EnableConfigurationProperties注解的所有属性值,
// 比如@EnableConfigurationProperties(ServerProperties.class),那么得到的值是ServerProperties.class
// (2)然后再将得到的@EnableConfigurationProperties注解的所有属性值注册到容器中
getTypes(metadata).forEach((type) -> register(registry,
(ConfigurableListableBeanFactory) registry, type));
}
}
在ConfigurationPropertiesBeanRegistrar
实现的registerBeanDefinitions
中,可以看到主要做了两件事:
- 调用
getTypes
方法获取@EnableConfigurationProperties
注解的属性值XxxProperties
; - 调用
register
方法将获取的属性值XxxProperties
注册到Spring
容器中,用于以后和外部属性绑定时使用。
我们来看下getTypes
方法的源码:
// ConfigurationPropertiesBeanRegistrar$ConfigurationPropertiesBeanRegistrar.java
private List<Class<?>> getTypes(AnnotationMetadata metadata) {
// 得到@EnableConfigurationProperties注解的所有属性值,
// 比如@EnableConfigurationProperties(ServerProperties.class),那么得到的值是ServerProperties.class
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
// 将属性值取出装进List集合并返回
return collectClasses((attributes != null) ? attributes.get("value")
: Collections.emptyList());
}
getTypes
方法里面的逻辑很简单即将@EnableConfigurationProperties
注解里面的属性值XxxProperties
(比如ServerProperties.class
)取出并装进List
集合并返回。
由getTypes
方法拿到@EnableConfigurationProperties
注解里面的属性值XxxProperties
(比如ServerProperties.class
)后,此时再遍历将XxxProperties
逐个注册进Spring
容器中,我们来看下register
方法:
// ConfigurationPropertiesBeanRegistrar$ConfigurationPropertiesBeanRegistrar.java
private void register(BeanDefinitionRegistry registry,
ConfigurableListableBeanFactory beanFactory, Class<?> type) {
// 得到type的名字,一般用类的全限定名作为bean name
String name = getName(type);
// 根据bean name判断beanFactory容器中是否包含该bean
if (!containsBeanDefinition(beanFactory, name)) {
// 若不包含,那么注册bean definition
registerBeanDefinition(registry, name, type);
}
}
我们再来看下由EnableConfigurationPropertiesImportSelector
导入的另一个类ConfigurationPropertiesBindingPostProcessorRegistrar
又是干嘛的呢?
5 ConfigurationPropertiesBindingPostProcessorRegistrar
可以看到ConfigurationPropertiesBindingPostProcessorRegistrar
类名字又是以Registrar
单词为结尾,说明其肯定又是导入一些bean
definition
的。直接看源码:
// ConfigurationPropertiesBindingPostProcessorRegistrar.java
public class ConfigurationPropertiesBindingPostProcessorRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 若容器中没有注册ConfigurationPropertiesBindingPostProcessor这个处理属性绑定的后置处理器,
// 那么将注册ConfigurationPropertiesBindingPostProcessor和ConfigurationBeanFactoryMetadata这两个bean
// 注意onApplicationEnvironmentPreparedEvent事件加载配置属性在先,然后再注册一些后置处理器用来处理这些配置属性
if (!registry.containsBeanDefinition(
ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
// (1)注册ConfigurationPropertiesBindingPostProcessor后置处理器,用来对配置属性进行后置处理
registerConfigurationPropertiesBindingPostProcessor(registry);
// (2)注册一个ConfigurationBeanFactoryMetadata类型的bean,
// 注意ConfigurationBeanFactoryMetadata实现了BeanFactoryPostProcessor,然后其会在postProcessBeanFactory中注册一些元数据
registerConfigurationBeanFactoryMetadata(registry);
}
}
// 注册ConfigurationPropertiesBindingPostProcessor后置处理器
private void registerConfigurationPropertiesBindingPostProcessor(
BeanDefinitionRegistry registry) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(
ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);
}
// 注册ConfigurationBeanFactoryMetadata后置处理器
private void registerConfigurationBeanFactoryMetadata(
BeanDefinitionRegistry registry) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,
definition);
}
}
ConfigurationPropertiesBindingPostProcessorRegistrar
类的逻辑非常简单,主要用来注册外部配置属性绑定相关的后置处理器即ConfigurationBeanFactoryMetadata
和ConfigurationPropertiesBindingPostProcessor
。
那么接下来我们再来探究下注册的这两个后置处理器又是执行怎样的后置处理逻辑呢?
6 ConfigurationBeanFactoryMetadata
先来看ConfigurationBeanFactoryMetadata
这个后置处理器,其实现了BeanFactoryPostProcessor
接口的postProcessBeanFactory
方法,在初始化bean
factory
时将@Bean
注解的元数据存储起来,以便在后续的外部配置属性绑定的相关逻辑中使用。
先来看下ConfigurationBeanFactoryMetadata
类实现BeanFactoryPostProcessor
接口的postProcessBeanFactory
方法源码:
// ConfigurationBeanFactoryMetadata
public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {
/**
* The bean name that this class is registered with.
*/
public static final String BEAN_NAME = ConfigurationBeanFactoryMetadata.class
.getName();
private ConfigurableListableBeanFactory beanFactory;
/**
* beansFactoryMetadata集合存储beansFactory的元数据
* key:某个bean的名字 value:FactoryMetadata对象(封装了工厂bean名和工厂方法名)
* 比如下面这个配置类:
*
* @Configuration
* public class ConfigA {
* @Bean
* public BeanXXX methodB(configA, ) {
* return new BeanXXX();
* }
* }
*
* 那么:key值为"methodB",value为FactoryMetadata(configA, methodB)对象,其bean属性值为"configA",method属性值为"methodB"
*/
private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
this.beanFactory = beanFactory;
// 遍历beanFactory的beanDefinitionName,即每个bean的名字(比如工厂方法对应的bean名字)
for (String name : beanFactory.getBeanDefinitionNames()) {
// 根据name得到beanDefinition
BeanDefinition definition = beanFactory.getBeanDefinition(name);
// 工厂方法名:一般是注解@Bean的方法名
String method = definition.getFactoryMethodName();
// 工厂bean名:一般是注解@Configuration的类名
String bean = definition.getFactoryBeanName();
if (method != null && bean != null) {
// 将beanDefinitionName作为Key,封装了工厂bean名和工厂方法名的FactoryMetadata对象作为value装入beansFactoryMetadata中
this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
}
}
}
}
从上面代码可以看到ConfigurationBeanFactoryMetadata
类覆写的postProcessBeanFactory
方法做的事情就是将工厂Bean
(可以理解为@Configuration
注解的类)及其@Bean
注解的工厂方法的一些元数据缓存到beansFactoryMetadata
集合中,以便后续使用,这个后面会详述。
由上代码中我们看到了ConfigurationBeanFactoryMetadata
类的beansFactoryMetadata
集合类型是Map<String, FactoryMetadata>
,那么我们再来看下封装相关工厂元数据的FactoryMetadata
类:
// ConfigurationBeanFactoryMetadata$FactoryMetadata.java
private static class FactoryMetadata {
// @Configuration注解的配置类的类名
private final String bean;
// @Bean注解的方法名
private final String method;
FactoryMetadata(String bean, String method) {
this.bean = bean;
this.method = method;
}
public String getBean() {
return this.bean;
}
public String getMethod() {
return this.method;
}
}
FactoryMetadata
仅有两个属性bean
和method
,分别表示@Configuration
注解的工厂bean
和@Bean
注解的工厂方法。
上面说了那么多,直接举个栗子会更直观:
/**
* beansFactoryMetadata集合存储beansFactory的元数据
* key:某个bean的名字 value:FactoryMetadata对象(封装了工厂bean名和工厂方法名)
* 比如下面这个配置类:
*
* @Configuration
* public class ConfigA {
* @Bean
* public BeanXXX methodB(configA, ) {
* return new BeanXXX();
* }
* }
*
* 那么:key值为"methodB",value为FactoryMetadata(configA, methodB)对象,其bean属性值为"configA",method属性值为"methodB"
*/
private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();
为了更好理解上面beansFactoryMetadata
集合存储的数据是啥,建议最好自己动手调试看看其里面装的是什么哦。总之这里记住一点就好了:ConfigurationBeanFactoryMetadata
类的beansFactoryMetadata
集合存储的是工厂bean
的相关元数据,以便在ConfigurationPropertiesBindingPostProcessor
后置处理器中使用。
7 ConfigurationPropertiesBindingPostProcessor
我们再来看下ConfigurationPropertiesBindingPostProcessorRegistrar
类注册的另外一个后置处理器ConfigurationPropertiesBindingPostProcessor
,这个后置处理器就尤其重要了,主要承担了将外部配置属性绑定到@ConfigurationProperties
注解标注的XxxProperties类的属性中(比如application.properties
配置文件中设置了server.port=8081
,那么8081
将会绑定到ServerProperties
类的port
属性中)的实现逻辑。
同样,先来看下ConfigurationPropertiesBindingPostProcessor
的源码:
// ConfigurationPropertiesBindingPostProcessor.java
public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
PriorityOrdered, ApplicationContextAware, InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
// ...这里省略实现代码先
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// ...这里省略实现代码先
}
// ...省略非关键代码
}
可以看到ConfigurationPropertiesBindingPostProcessor
后置处理器实现了两个重要的接口InitializingBean
和BeanPostProcessor
。
我们都知道:
InitializingBean
接口的afterPropertiesSet
方法会在bean
属性赋值后调用,用来执行一些自定义的初始化逻辑比如检查某些强制的属性是否有被赋值,校验某些配置或给一些未被赋值的属性赋值。BeanPostProcessor
接口是bean
的后置处理器,其有postProcessBeforeInitialization
和postProcessAfterInitialization
两个勾子方法,分别会在bean
初始化前后被调用来执行一些后置处理逻辑,比如检查标记接口或是否用代理包装了bean
。
同时由上代码可以看到ConfigurationPropertiesBindingPostProcessor
后置处理器覆写了InitializingBean
的afterPropertiesSet
方法和BeanPostProcessor
的postProcessBeforeInitialization
方法。
接下来我们再来探究ConfigurationPropertiesBindingPostProcessor
后置处理器覆写的两个方法的源码。
7.1 在执行外部属性绑定逻辑前先准备好相关元数据和配置属性绑定器
我们先来分析下ConfigurationPropertiesBindingPostProcessor
覆写InitializingBean
接口的afterPropertiesSet
方法:
// ConfigurationPropertiesBindingPostProcessor.java
/**
* 配置属性校验器名字
*/
public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
/**
* 工厂bean相关元数据
*/
private ConfigurationBeanFactoryMetadata beanFactoryMetadata;
/**
* 上下文
*/
private ApplicationContext applicationContext;
/**
* 配置属性绑定器
*/
private ConfigurationPropertiesBinder configurationPropertiesBinder;
// 这里主要是给beanFactoryMetadata和configurationPropertiesBinder的属性赋值,用于后面的后置处理器方法处理属性绑定的时候用
@Override
public void afterPropertiesSet() throws Exception {
// We can't use constructor injection of the application context because
// it causes eager factory bean initialization
// 【1】利用afterPropertiesSet这个勾子方法从容器中获取之前注册的ConfigurationBeanFactoryMetadata对象赋给beanFactoryMetadata属性
// (问1)beanFactoryMetadata这个bean是什么时候注册到容器中的?
// (答1)在ConfigurationPropertiesBindingPostProcessorRegistrar类的registerBeanDefinitions方法中将beanFactoryMetadata这个bean注册到容器中
// (问2)从容器中获取beanFactoryMetadata对象后,什么时候会被用到?
// (答2)beanFactoryMetadata对象的beansFactoryMetadata集合保存的工厂bean相关的元数据,在ConfigurationPropertiesBindingPostProcessor类
// 要判断某个bean是否有FactoryAnnotation或FactoryMethod时会根据这个beanFactoryMetadata对象的beansFactoryMetadata集合的元数据来查找
this.beanFactoryMetadata = this.applicationContext.getBean(
ConfigurationBeanFactoryMetadata.BEAN_NAME,
ConfigurationBeanFactoryMetadata.class);
// 【2】new一个ConfigurationPropertiesBinder,用于后面的外部属性绑定时使用
this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(
this.applicationContext, VALIDATOR_BEAN_NAME); // VALIDATOR_BEAN_NAME="configurationPropertiesValidator"
}
可以看到以上代码主要逻辑就是在执行外部属性绑定逻辑前先准备好相关元数据和配置属性绑定器,即从Spring
容器中获取到之前注册的ConfigurationBeanFactoryMetadata
对象赋给ConfigurationPropertiesBindingPostProcessor
后置处理器的beanFactoryMetadata
属性,还有就是新建一个ConfigurationPropertiesBinder
配置属性绑定器对象并赋值给configurationPropertiesBinder
属性。
我们再来看下ConfigurationPropertiesBinder
这个配置属性绑定器对象是如何构造的。
// ConfigurationPropertiesBinder.java
ConfigurationPropertiesBinder(ApplicationContext applicationContext,
String validatorBeanName) {
this.applicationContext = applicationContext;
// 将applicationContext封装到PropertySourcesDeducer对象中并返回
this.propertySources = new PropertySourcesDeducer(applicationContext)
.getPropertySources(); // 获取属性源,主要用于在ConfigurableListableBeanFactory的后置处理方法postProcessBeanFactory中处理
// 如果没有配置validator的话,这里一般返回的是null
this.configurationPropertiesValidator = getConfigurationPropertiesValidator(
applicationContext, validatorBeanName);
// 检查实现JSR-303规范的bean校验器相关类在classpath中是否存在
this.jsr303Present = ConfigurationPropertiesJsr303Validator
.isJsr303Present(applicationContext);
}
可以看到在构造ConfigurationPropertiesBinder
对象时主要给其相关属性赋值(一般构造器逻辑都是这样):
- 给
applicationContext
属性赋值注入上下文对象; - 给
propertySources
属性赋值,属性源即外部配置值比如application.properties
配置的属性值,注意这里的属性源是由ConfigFileApplicationListener
这个监听器负责读取的,ConfigFileApplicationListener
将会在后面源码分析章节中详述。 - 给
configurationPropertiesValidator
属性赋值,值来自Spring
容器中名为configurationPropertiesValidator
的bean
。 - 给
jsr303Present
属性赋值,当javax.validation.Validator
,javax.validation.ValidatorFactory
和javax.validation.bootstrap.GenericBootstrap"
这三个类同时存在于classpath
中jsr303Present
属性值才为true
。
关于JSR303:
JSR-303
是JAVA EE 6中的一项子规范,叫做Bean Validation
,Hibernate Validator
是Bean Validation
的参考实现 。Hibernate Validator
提供了JSR 303
规范中所有内置constraint
的实现,除此之外还有一些附加的constraint
。
7.2 执行真正的外部属性绑定逻辑【主线】
前面分析了那么多,发现都还没到外部属性绑定的真正处理逻辑,前面步骤都是在做一些准备性工作,为外部属性绑定做铺垫。
在执行外部属性绑定逻辑前,准备好了相关元数据和配置属性绑定器后,此时我们再来看看ConfigurationPropertiesBindingPostProcessor
实现BeanPostProcessor
接口的postProcessBeforeInitialization
后置处理方法了,外部属性绑定逻辑都是在这个后置处理方法里实现,是我们关注的重中之重。
直接看代码:
// ConfigurationPropertiesBindingPostProcessor.java
// 因为是外部配置属性后置处理器,因此这里对@ConfigurationProperties注解标注的XxxProperties类进行后置处理完成属性绑定
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 注意,BeanPostProcessor后置处理器默认会对所有的bean进行处理,因此需要根据bean的一些条件进行过滤得到最终要处理的目的bean,
// 这里的过滤条件就是判断某个bean是否有@ConfigurationProperties注解
// 【1】从bean上获取@ConfigurationProperties注解,若bean有标注,那么返回该注解;若没有,则返回Null。比如ServerProperty上标注了@ConfigurationProperties注解
ConfigurationProperties annotation = getAnnotation(bean, beanName,
ConfigurationProperties.class);
// 【2】若标注有@ConfigurationProperties注解的bean,那么则进行进一步处理:将配置文件的配置注入到bean的属性值中
if (annotation != null) {
/********主线,重点关注】********/
bind(bean, beanName, annotation);
}
// 【3】返回外部配置属性值绑定后的bean(一般是XxxProperties对象)
return bean;
}
ConfigurationPropertiesBindingPostProcessor
类覆写的postProcessBeforeInitialization
方法的做的事情就是将外部属性配置绑定到@ConfigurationProperties
注解标注的XxxProperties
类上,现关键步骤总结如下:
- 从
bean
上获取@ConfigurationProperties
注解; - 若标注有
@ConfigurationProperties
注解的bean
,那么则进行进一步的处理:将外部配置属性值绑定到bean的属性值中后再返回bean
;若没有标注有@ConfigurationProperties
注解的bean
,那么将直接原样返回bean
。
注意:后置处理器默认会对每个容器中的
bean
进行后置处理,因为这里只针对标注有@ConfigurationProperties
注解的bean
进行外部属性绑定,因此没有标注@ConfigurationProperties
注解的bean
将不会被处理。
接下来我们紧跟主线,再来看下外部配置属性是如何绑定到@ConfigurationProperties
注解的XxxProperties
类属性上的呢?
直接看代码:
// ConfigurationPropertiesBindingPostProcessor.java
private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
// 【1】得到bean的类型,比如ServerPropertie这个bean得到的类型是:org.springframework.boot.autoconfigure.web.ServerProperties
ResolvableType type = getBeanType(bean, beanName);
// 【2】获取bean上标注的@Validated注解
Validated validated = getAnnotation(bean, beanName, Validated.class);
// 若标注有@Validated注解的话则跟@ConfigurationProperties注解一起组成一个Annotation数组
Annotation[] annotations = (validated != null)
? new Annotation[] { annotation, validated }
: new Annotation[] { annotation };
// 【3】返回一个绑定了XxxProperties类的Bindable对象target,这个target对象即被外部属性值注入的目标对象
// (比如封装了标注有@ConfigurationProperties注解的ServerProperties对象的Bindable对象)
Bindable<?> target = Bindable.of(type).withExistingValue(bean)
.withAnnotations(annotations); // 设置annotations属性数组
try {
// 【4】执行外部配置属性绑定逻辑
/********【主线,重点关注】********/
this.configurationPropertiesBinder.bind(target);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
ex);
}
}
关键步骤上面代码已经标注【x】
,这里在继续讲解外部配置属性绑定的主线逻辑(在8 ConfigurationPropertiesBinder这一小节分析 )前先穿插一个知识点,还记得ConfigurationBeanFactoryMetadata
覆写的postProcessBeanFactory
方法里已经将相关工厂bean
的元数据封装到ConfigurationBeanFactoryMetadata
类的beansFactoryMetadata
集合这一回事吗?
我们再来看下上面代码中的【1】getBeanType
和【2】getAnnotation
方法源码:
// ConfigurationPropertiesBindingPostProcessor.java
private ResolvableType getBeanType(Object bean, String beanName) {
// 首先获取有没有工厂方法
Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
// 若有工厂方法
if (factoryMethod != null) {
return ResolvableType.forMethodReturnType(factoryMethod);
}
// 没有工厂方法,则说明是普通的配置类
return ResolvableType.forClass(bean.getClass());
}
private <A extends Annotation> A getAnnotation(Object bean, String beanName,
Class<A> type) {
A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
}
return annotation;
}
注意到上面代码中的beanFactoryMetadata
对象没,ConfigurationPropertiesBindingPostProcessor
后置处理器的getBeanType
和getAnnotation
方法分别会调用ConfigurationBeanFactoryMetadata
的findFactoryMethod
和findFactoryAnnotation
方法,而ConfigurationBeanFactoryMetadata
的findFactoryMethod
和findFactoryAnnotation
方法又会依赖存储工厂bean
元数据的beansFactoryMetadata
集合来寻找是否有FactoryMethod
和FactoryAnnotation
。因此,到这里我们就知道之ConfigurationBeanFactoryMetadata
的beansFactoryMetadata
集合存储工厂bean
元数据的作用了。
8 ConfigurationPropertiesBinder
我们再继续紧跟外部配置属性绑定的主线,继续前面看7.2 执行真正的外部属性绑定逻辑中的this.configurationPropertiesBinder.bind(target);
这句代码:
// ConfigurationPropertiesBinder.java
public void bind(Bindable<?> target) {
//【1】得到@ConfigurationProperties注解
ConfigurationProperties annotation = target
.getAnnotation(ConfigurationProperties.class);
Assert.state(annotation != null,
() -> "Missing @ConfigurationProperties on " + target);
// 【2】得到Validator对象集合,用于属性校验
List<Validator> validators = getValidators(target);
// 【3】得到BindHandler对象(默认是IgnoreTopLevelConverterNotFoundBindHandler对象),
// 用于对ConfigurationProperties注解的ignoreUnknownFields等属性的处理
BindHandler bindHandler = getBindHandler(annotation, validators);
// 【4】得到一个Binder对象,并利用其bind方法执行外部属性绑定逻辑
/********************【主线,重点关注】********************/
getBinder().bind(annotation.prefix(), target, bindHandler);
}
上面代码的主要逻辑是:
- 先获取
target
对象(对应XxxProperties
类)上的@ConfigurationProperties
注解和校验器(若有); - 然后再根据获取的的
@ConfigurationProperties
注解和校验器来获得BindHandler
对象,BindHandler
的作用是用于在属性绑定时来处理一些附件逻辑;在8.1节分析. - 最后再获取一个
Binder
对象,调用其bind
方法来执行外部属性绑定的逻辑,在8.2节分析.
8.1 获取BindHandler对象以便在属性绑定时来处理一些附件逻辑
我们在看getBindHandler
方法的逻辑前先来认识下BindHandler
是干啥的。
BindHandler
是一个父类接口,用于在属性绑定时来处理一些附件逻辑。我们先看下BindHandler
的类图,好有一个整体的认识:
可以看到AbstractBindHandler
作为抽象基类实现了BindHandler
接口,其又有四个具体的子类分别是IgnoreTopLevelConverterNotFoundBindHandler
,NoUnboundElementsBindHandler
,IgnoreErrorsBindHandler
和ValidationBindHandler
。
IgnoreTopLevelConverterNotFoundBindHandler
:在处理外部属性绑定时的默认BindHandler
,当属性绑定失败时会忽略最顶层的ConverterNotFoundException
;NoUnboundElementsBindHandler
:用来处理配置文件配置的未知的属性;IgnoreErrorsBindHandler
:用来忽略无效的配置属性例如类型错误;ValidationBindHandler
:利用校验器对绑定的结果值进行校验。
分析完类关系后,我们再来看下BindHandler
接口提供了哪些方法在外部属性绑定时提供一些额外的附件逻辑,直接看代码:
// BindHandler.java
public interface BindHandler {
/**
* Default no-op bind handler.
*/
BindHandler DEFAULT = new BindHandler() {
};
// onStart方法在外部属性绑定前被调用
default <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
BindContext context) {
return target;
}
// onSuccess方法在外部属性成功绑定时被调用,该方法能够改变最终返回的属性值或对属性值进行校验
default Object onSuccess(ConfigurationPropertyName name, Bindable<?> target,
BindContext context, Object result) {
return result;
}
// onFailure方法在外部属性绑定失败(包括onSuccess方法里的逻辑执行失败)时被调用,
// 该方法可以用来catch住相关异常或者返回一个替代的结果(跟微服务的降级结果有点类似,嘿嘿)
default Object onFailure(ConfigurationPropertyName name, Bindable<?> target,
BindContext context, Exception error) throws Exception {
throw error;
}
// 当外部属性绑定结束时(不管绑定成功还是失败)被调用
default void onFinish(ConfigurationPropertyName name, Bindable<?> target,
BindContext context, Object result) throws Exception {
}
}
可以看到BindHandler
接口定义了onStart
,onSuccess
,onFailure
和onFinish
方法,这四个方法分别会在执行外部属性绑定时的不同时机会被调用,在属性绑定时用来添加一些额外的处理逻辑,比如在onSuccess
方法改变最终绑定的属性值或对属性值进行校验,在onFailure
方法catch
住相关异常或者返回一个替代的绑定的属性值。
知道了BindHandler
是在属性绑定时添加一些额外的附件处理逻辑后,我们再来看下getBindHandler
方法的逻辑,直接上代码:
// ConfigurationPropertiesBinder.java
// 注意BindHandler的设计技巧,应该是责任链模式,非常巧妙,值得借鉴
private BindHandler getBindHandler(ConfigurationProperties annotation,
List<Validator> validators) {
// 新建一个IgnoreTopLevelConverterNotFoundBindHandler对象,这是个默认的BindHandler对象
BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
// 若注解@ConfigurationProperties的ignoreInvalidFields属性设置为true,
// 则说明可以忽略无效的配置属性例如类型错误,此时新建一个IgnoreErrorsBindHandler对象
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
}
// 若注解@ConfigurationProperties的ignoreUnknownFields属性设置为true,
// 则说明配置文件配置了一些未知的属性配置,此时新建一个ignoreUnknownFields对象
if (!annotation.ignoreUnknownFields()) {
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
handler = new NoUnboundElementsBindHandler(handler, filter);
}
// 如果@Valid注解不为空,则创建一个ValidationBindHandler对象
if (!validators.isEmpty()) {
handler = new ValidationBindHandler(handler,
validators.toArray(new Validator[0]));
}
// 遍历获取的ConfigurationPropertiesBindHandlerAdvisor集合,
// ConfigurationPropertiesBindHandlerAdvisor目前只在测试类中有用到
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
// 对handler进一步处理
handler = advisor.apply(handler);
}
// 返回handler
return handler;
}
getBindHandler
方法的逻辑很简单,主要是根据传入的@ConfigurationProperties
注解和validators
校验器来创建不同的BindHandler
具体实现类:
- 首先
new
一个IgnoreTopLevelConverterNotFoundBindHandler
作为默认的BindHandler
; - 若
@ConfigurationProperties
注解的属性ignoreInvalidFields
值为true
,那么再new
一个IgnoreErrorsBindHandler
对象,把刚才新建的IgnoreTopLevelConverterNotFoundBindHandler
对象作为构造参数传入赋值给AbstractBindHandler
父类的parent
属性; - 若
@ConfigurationProperties
注解的属性ignoreUnknownFields
值为false
,那么再new
一个UnboundElementsSourceFilter
对象,把之前构造的BindHandler
对象作为构造参数传入赋值给AbstractBindHandler
父类的parent
属性; - ......以此类推,前一个
handler
对象作为后一个hangdler
对象的构造参数,就这样利用AbstractBindHandler
父类的parent
属性将每一个handler
链起来,最后再得到最终构造的handler
。
GET技巧:上面的这个设计模式是不是很熟悉,这个就是责任链模式。我们学习源码,同时也是学习别人怎么熟练运用设计模式。责任链模式的应用案例有很多,比如
Dubbo
的各种Filter
们(比如AccessLogFilter
是用来记录服务的访问日志的,ExceptionFilter
是用来处理异常的...),我们一开始学习java web时的Servlet
的Filter
,MyBatis
的Plugin
们以及Netty
的Pipeline
都采用了责任链模式。
我们了解了BindHandler
的作用后,再来紧跟主线,看属性绑定是如何绑定的?
8.2 获取Binder对象用于进行属性绑定【主线】
这里接8 ConfigurationPropertiesBinder节代码中标注【4】
的主线代码getBinder().bind(annotation.prefix(), target, bindHandler);
.
可以看到这句代码主要做了两件事:
- 调用
getBinder
方法获取用于属性绑定的Binder
对象; - 调用
Binder
对象的bind
方法进行外部属性绑定到@ConfigurationProperties
注解的XxxProperties
类的属性上。
那么我们先看下getBinder
方法源码:
// ConfigurationPropertiesBinder.java
private Binder getBinder() {
// Binder是一个能绑定ConfigurationPropertySource的容器对象
if (this.binder == null) {
// 新建一个Binder对象,这个binder对象封装了ConfigurationPropertySources,
// PropertySourcesPlaceholdersResolver,ConversionService和PropertyEditorInitializer对象
this.binder = new Binder(getConfigurationPropertySources(), // 将PropertySources对象封装成SpringConfigurationPropertySources对象并返回
getPropertySourcesPlaceholdersResolver(), getConversionService(), // 将PropertySources对象封装成PropertySourcesPlaceholdersResolver对象并返回,从容器中获取到ConversionService对象
getPropertyEditorInitializer()); // 得到Consumer<PropertyEditorRegistry>对象,这些初始化器用来配置property editors,property editors通常可以用来转换值
}
// 返回binder
return this.binder;
}
可以看到Binder
对象封装了ConfigurationPropertySources
,PropertySourcesPlaceholdersResolver
,ConversionService
和PropertyEditorInitializer
这四个对象,Binder
对象封装了这四个哥们肯定是在后面属性绑定逻辑中会用到,先看下这四个对象是干嘛的:
ConfigurationPropertySources
:外部配置文件的属性源,由ConfigFileApplicationListener
监听器负责触发读取;PropertySourcesPlaceholdersResolver
:解析属性源中的占位符${}
;ConversionService
:对属性类型进行转换PropertyEditorInitializer
:用来配置property editors
那么,我们获取了Binder
属性绑定器后,再来看下它的bind
方法是如何执行属性绑定的。
// Binder.java
public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler) {
// ConfigurationPropertyName.of(name):将name(这里指属性前缀名)封装到ConfigurationPropertyName对象中
// 将外部配置属性绑定到目标对象target中
return bind(ConfigurationPropertyName.of(name), target, handler);
}
public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target,
BindHandler handler) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(target, "Target must not be null");
handler = (handler != null) ? handler : BindHandler.DEFAULT;
// Context是Binder的内部类,实现了BindContext,Context可以理解为Binder的上下文,可以用来获取binder的属性比如Binder的sources属性
Context context = new Context();
// 进行属性绑定,并返回绑定属性后的对象bound,注意bound的对象类型是T,T就是@ConfigurationProperties注解的类比如ServerProperties
/********【主线,重点关注】************/
T bound = bind(name, target, handler, context, false);
// 将刚才返回的bound对象封装到BindResult对象中并返回
return BindResult.of(bound);
}
上面代码中首先创建了一个Context
对象,Context
是Binder
的内部类,为Binder
的上下文,利用Context
上下文可以获取Binder
的属性比如获取Binder
的sources
属性值并绑定到XxxProperties
属性中。然后我们再紧跟主线看下bind(name, target, handler, context, false)
方法源码:
// Binder.java
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
BindHandler handler, Context context, boolean allowRecursiveBinding) {
// 清空Binder的configurationProperty属性值
context.clearConfigurationProperty();
try {
// 【1】调用BindHandler的onStart方法,执行一系列的责任链对象的该方法
target = handler.onStart(name, target, context);
if (target == null) {
return null;
}// 【2】调用bindObject方法对Bindable对象target的属性进行绑定外部配置的值,并返回赋值给bound对象。
// 举个栗子:比如设置了server.port=8888,那么该方法最终会调用Binder.bindProperty方法,最终返回的bound的value值为8888
/************【主线:重点关注】***********/
Object bound = bindObject(name, target, handler, context,
allowRecursiveBinding);
// 【3】封装handleBindResult对象并返回,注意在handleBindResult的构造函数中会调用BindHandler的onSucess,onFinish方法
return handleBindResult(name, target, handler, context, bound);
}
catch (Exception ex) {
return handleBindError(name, target, handler, context, ex);
}
}
上面代码的注释已经非常详细,这里不再详述。我们接着紧跟主线来看看bindObject
方法源码:
// Binder.java
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,
BindHandler handler, Context context, boolean allowRecursiveBinding) {
// 从propertySource中的配置属性,获取ConfigurationProperty对象property即application.properties配置文件中若有相关的配置的话,
// 那么property将不会为null。举个栗子:假如你在配置文件中配置了spring.profiles.active=dev,那么相应property值为dev;否则为null
ConfigurationProperty property = findProperty(name, context);
// 若property为null,则不会执行后续的属性绑定相关逻辑
if (property == null && containsNoDescendantOf(context.getSources(), name)) {
// 如果property == null,则返回null
return null;
}
// 根据target类型获取不同的Binder,可以是null(普通的类型一般是Null),MapBinder,CollectionBinder或ArrayBinder
AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
// 若aggregateBinder不为null比如配置了spring.profiles属性(当然包括其子属性比如spring.profiles.active等)
if (aggregateBinder != null) {
// 若aggregateBinder不为null,则调用bindAggregate并返回绑定后的对象
return bindAggregate(name, target, handler, context, aggregateBinder);
}
// 若property不为null
if (property != null) {
try {
// 绑定属性到对象中,比如配置文件中设置了server.port=8888,那么将会最终调用bindProperty方法进行属性设置
return bindProperty(target, context, property);
}
catch (ConverterNotFoundException ex) {
// We might still be able to bind it as a bean
Object bean = bindBean(name, target, handler, context,
allowRecursiveBinding);
if (bean != null) {
return bean;
}
throw ex;
}
}
// 只有@ConfigurationProperties注解的类进行外部属性绑定才会走这里
/***********************【主线,重点关注】****************************/
return bindBean(name, target, handler, context, allowRecursiveBinding);
}
由上代码中可以看到bindObject
中执行属性绑定的逻辑会根据不同的属性类型进入不同的绑定逻辑中,举个栗子:
application.properties
配置文件中配置了spring.profiles.active=dev
的话,那么将会进入return bindAggregate(name, target, handler, context, aggregateBinder);
这个属性绑定的代码逻辑;application.properties
配置文件中配置了server.port=8081
的话,那么将会进入return bindBean(name, target, handler, context, allowRecursiveBinding);
的属性绑定的逻辑。
因此我们再次紧跟主线,进入@ConfigurationProperties
注解的XxxProperties
类的属性绑定逻辑中的bindBean
方法中:
// Binder.java
private Object bindBean(ConfigurationPropertyName name, Bindable<?> target, // name指的是ConfigurationProperties的前缀名
BindHandler handler, Context context, boolean allowRecursiveBinding) {
// 这里做一些ConfigurationPropertyState的相关检查
if (containsNoDescendantOf(context.getSources(), name)
|| isUnbindableBean(name, target, context)) {
return null;
}// 这里新建一个BeanPropertyBinder的实现类对象,注意这个对象实现了bindProperty方法
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
name.append(propertyName), propertyTarget, handler, context, false);
/**
* (propertyName, propertyTarget) -> bind(
* name.append(propertyName), propertyTarget, handler, context, false);
* 等价于
* new BeanPropertyBinder() {
* Object bindProperty(String propertyName, Bindable<?> target){
* bind(name.append(propertyName), propertyTarget, handler, context, false);
* }
* }
*/
// type类型即@ConfigurationProperties注解标注的XxxProperties类
Class<?> type = target.getType().resolve(Object.class);
if (!allowRecursiveBinding && context.hasBoundBean(type)) {
return null;
}
// 这里应用了java8的lambda语法,作为没怎么学习java8的lambda语法的我,不怎么好理解下面的逻辑,哈哈
// 真正实现将外部配置属性绑定到@ConfigurationProperties注解的XxxProperties类的属性中的逻辑应该就是在这句lambda代码了
/*******************【主线】***************************/
return context.withBean(type, () -> {
Stream<?> boundBeans = BEAN_BINDERS.stream()
.map((b) -> b.bind(name, target, context, propertyBinder));
return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
});
// 根据上面的lambda语句翻译如下:
/** 这里的T指的是各种属性绑定对象,比如ServerProperties
* return context.withBean(type, new Supplier<T>() {
* T get() {
* Stream<?> boundBeans = BEAN_BINDERS.stream()
* .map((b) -> b.bind(name, target, context, propertyBinder));
* return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
* }
* });
*/
}
从上面代码中,我们追根究底来到了外部配置属性绑定到XxxProperties
类属性中的比较底层的代码了,可以看到属性绑定的逻辑应该就在上面代码标注【主线】
的lambda
代码处了。这里就不再详述了,因为这个属于SpringBoot的属性绑定Binder
的范畴,Binder
相关类是SpringBoot2.0才出现的,即对之前的属性绑定相关代码进行推翻重写了。属性绑定相关的源码也比较多,后续有需要再另开一篇来分析探究吧。
9 小结
好了,外部配置属性值是如何被绑定到XxxProperties
类属性上的源码分析就到此结束了,又是蛮长的一篇文章,不知自己表述清楚没,重要步骤现总结下:
- 首先是
@EnableConfigurationProperties
注解import
了EnableConfigurationPropertiesImportSelector
后置处理器; EnableConfigurationPropertiesImportSelector
后置处理器又向Spring
容器中注册了ConfigurationPropertiesBeanRegistrar
和ConfigurationPropertiesBindingPostProcessorRegistrar
这两个bean
;- 其中
ConfigurationPropertiesBeanRegistrar
向Spring
容器中注册了XxxProperties
类型的bean
;ConfigurationPropertiesBindingPostProcessorRegistrar
向Spring
容器中注册了ConfigurationBeanFactoryMetadata
和ConfigurationPropertiesBindingPostProcessor
两个后置处理器; ConfigurationBeanFactoryMetadata
后置处理器在初始化bean
factory
时将@Bean
注解的元数据存储起来,以便在后续的外部配置属性绑定的相关逻辑中使用;ConfigurationPropertiesBindingPostProcessor
后置处理器将外部配置属性值绑定到XxxProperties
类属性的逻辑委托给ConfigurationPropertiesBinder
对象,然后ConfigurationPropertiesBinder
对象又最终将属性绑定的逻辑委托给Binder
对象来完成。
SpringBoot内置的各种Starter是怎样构建的?--SpringBoot源码(六)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
1 温故而知新
本篇接 外部配置属性值是如何被绑定到XxxProperties类属性上的?--SpringBoot源码(五)
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBoot外部配置属性值是如何被绑定到XxxProperties类属性上的相关源码,现将外部属性绑定的重要步骤总结如下:
- 首先是
@EnableConfigurationProperties
注解import
了EnableConfigurationPropertiesImportSelector
后置处理器; EnableConfigurationPropertiesImportSelector
后置处理器又向Spring
容器中注册了ConfigurationPropertiesBeanRegistrar
和ConfigurationPropertiesBindingPostProcessorRegistrar
这两个bean
;- 其中
ConfigurationPropertiesBeanRegistrar
向Spring
容器中注册了XxxProperties
类型的bean
;ConfigurationPropertiesBindingPostProcessorRegistrar
向Spring
容器中注册了ConfigurationBeanFactoryMetadata
和ConfigurationPropertiesBindingPostProcessor
两个后置处理器; ConfigurationBeanFactoryMetadata
后置处理器在初始化bean
factory
时将@Bean
注解的元数据存储起来,以便在后续的外部配置属性绑定的相关逻辑中使用;ConfigurationPropertiesBindingPostProcessor
后置处理器将外部配置属性值绑定到XxxProperties
类属性的逻辑委托给ConfigurationPropertiesBinder
对象,然后ConfigurationPropertiesBinder
对象又最终将属性绑定的逻辑委托给Binder
对象来完成。
可见,重要的是上面的第5步。
2 引言
我们都知道,SpringBoot内置了各种Starter
起步依赖,我们使用非常方便,大大减轻了我们的开发工作。有了Starter
起步依赖,我们不用去考虑这个项目需要什么库,这个库的groupId
和artifactId
是什么?更不用担心引入这个版本的库后会不会跟其他依赖有没有冲突。
举个栗子:现在我们想开发一个web项目,那么只要引入
spring-boot-starter-web
这个起步依赖就可以了,不用考虑要引入哪些版本的哪些依赖了。像以前我们还要考虑引入哪些依赖库,比如要引入spring-web
和spring-webmvc
依赖等;此外,还要考虑引入这些库的哪些版本才不会跟其他库冲突等问题。
那么我们今天暂时不分析SpringBoot自动配置的源码,由于起步依赖跟自动配置的关系是如影随形的关系,因此本篇先站在maven项目构建的角度来宏观分析下我们平时使用的SpringBoot内置的各种Starter
是怎样构建的?
3 Maven传递依赖的optional标签
在分析SpringBoot内置的各种Starter
构建原理前,我们先来认识下Maven的optional
标签,因为这个标签起到至关重要的作用。
Maven的optional
标签表示可选依赖即不可传递的意思,下面直接举个栗子来说明。
比如有A
,B
和C
三个库,C
依赖B
,B
依赖A
。下面看下这三个库的pom.xml
文件:
// A的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.ymbj</groupId>
<artifactId>A</artifactId>
<version>1.0-SNAPSHOT</version>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.ymbj</groupId>
<artifactId>B</artifactId>
<version>1.0-SNAPSHOT</version>
<!--注意是可选依赖-->
<dependencies>
<dependency>
<groupId>com.ymbj</groupId>
<artifactId>A</artifactId>
<version>1.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>com.ymbj</groupId>
<artifactId>C</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.ymbj</groupId>
<artifactId>B</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
上面三个A
,B
和C
库的pom.xml
可知,B
库依赖A
库,然后C
库又依赖了B
库,那么请想一下,Maven打包构建C
库后,A
库有没有被引进来?
答案肯定是没有,因为B
库引入A
库依赖时使用了<optional>true</optional>
,即将Maven的optional
标签值设为了true
,此时C
库再引入B
库依赖时,A
库是不会被引入到C
库的。
同时跟Maven传递依赖有关的还有一个exclusions
标签,这个表示将某个库的某个子依赖排除掉,这里不再详述。
4 SpringBoot内置的各种Starter是怎样构建的?
我们现在来探究SpringBoot内置的各种Starter
到底是怎样构建的呢?
还记得如何分析SpringBoot源码模块及结构?这篇文章分析的SpringBoot内部的模块之间的关系吗?先来回顾一下SpringBoot源码内部模块图:
我们都知道,SpringBoot的Starter
的构建的原理实质就是自动配置,因此由图1可以看到SpringBoot源码项目内部跟Starter
及其自动配置有关的模块有四个:spring-boot-starters
,spring-boot-actuator-autoconfigure
,spring-boot-autoconfigure
和spring-boot-test-autoconfigure
。 每个模块的作用请看如何分析SpringBoot源码模块及结构?这篇文章,这里不再赘述。
那么,spring-boot-starters
模块跟后面三个自动配置有关的模块xxx-autoconfigure
模块的关系是怎样的呢?
此时我们先来看看spring-boot-starters
模块里面的结构是怎样的?
由图2可以看到spring-boot-starters
模块包含了SpringBoot内置的各种starter
:spring-boot-starter-xxx
。由于SpringBoot内置的各种starter
太多,以我们常用的spring-boot-starter-web
起步依赖来探究好了。
我们首先看下spring-boot-starter-web
模块内部结构:
可以看到spring-boot-starter-web
模块里面只有.flattened-pom.xml
和pom.xml
文件,而没有任何代码!有点出乎我们意料。我们都知道若要用到SpringBoot的web功能时引入spring-boot-starter-web
起步依赖即可,而现在spring-boot-starter-web
模块里面没有一行代码,那么spring-boot-starter-web
究竟是如何构建的呢?会不会跟图1所示的spring-boot-autoconfigure
自动配置模块有关?
此时我们就需要看下spring-boot-starter-web
模块的pom.xml
文件内容:
由图4可以看到,spring-boot-starter-web
模块依赖了spring-boot-starter
,spring-boot-starter-tomcat
,spring-web
和spring-webmvc
等模块,居然没有依赖spring-boot-autoconfigure
自动配置模块!
由于spring-boot-starter-web
模块肯定跟spring-boot-autoconfigure
自动配置模块有关,所以spring-boot-starter-web
模块肯定是间接依赖了spring-boot-autoconfigure
自动配置模块。
图4标有标注"重点关注"的spring-boot-starter
模块是绝大部分spring-boot-starter-xxx
模块依赖的基础模块,是核心的Starter
,包括了自动配置,日志和YAML
支持。我们此时来关注下spring-boot-starter
的pom.xml
文件,也许其依赖了了spring-boot-autoconfigure
自动配置模块。
由图5可以看到,我们前面的猜想没有错,正是spring-boot-starter
模块依赖了spring-boot-autoconfigure
自动配置模块!因此,到了这里我们就可以得出结论了:spring-boot-starter-web
模块没有一行代码,但是其通过spring-boot-starter
模块间接依赖了spring-boot-autoconfigure
自动配置模块,从而实现了其起步依赖的功能。
此时我们再来看下spring-boot-autoconfigure
自动配置模块的内部包结构:
由图6红框处,我们可以知道spring-boot-starter-web
起步依赖的自动配置功能原来是由spring-boot-autoconfigure
模块的web
包下的类实现的。
到了这里spring-boot-starter-web
起步依赖的构建基本原理我们就搞清楚了,但是还有一个特别重要的关键点我们还没Get到。这个关键点跟Maven的optional
标签有的作用有关。
为了Get到这个点,我们先来思考一个问题:平时我们开发web
项目为什么引入了spring-boot-starter-web
这个起步依赖后,spring-boot-autoconfigure
模块的web
相关的自动配置类就会起自动起作用呢?
我们应该知道,某个自动配置类起作用往往是由于classpath
中存在某个类,这里以DispatcherServletAutoConfiguration
这个自动配置类为切入点去Get这个点好了。
先看下DispatcherServletAutoConfiguration
能够自动配置的条件是啥?
由图7所示,DispatcherServletAutoConfiguration
能够自动配置的条件之一是@ConditionalOnClass(DispatcherServlet.class)
,即只有classpath
中存在DispatcherServlet.class
这个类,那么DispatcherServletAutoConfiguration
自动配置相关逻辑才能起作用。
而DispatcherServlet
这个类是在spring-webmvc
这个依赖库中的,如下图所示:
此时我们再看下spring-boot-autoconfigure
模块的pom.xml
文件引入spring-webmvc
这个依赖的情况:
由图9所示,spring-boot-autoconfigure
模块引入的spring-webmvc
这个依赖时optional
被设置为true
,原来是可选依赖。即spring-webmvc
这个依赖库只会被导入到spring-boot-autoconfigure
模块中,而不会被导入到间接依赖spring-boot-autoconfigure
模块的spring-boot-starter-web
这个起步依赖中。
此时,我们再来看看spring-boot-starter-web
的pom.xml
文件的依赖情况:
由图10所示,spring-boot-starter-web
起步依赖显式引入了spring-webmvc
这个依赖库,即引入spring-webmvc
时没有optional
这个标签,又因为DispatcherServlet
这个类是在spring-webmvc
这个依赖库中的,从而classpath
中存在DispatcherServlet
这个类,因此DispatcherServletAutoConfiguration
这个自动配置类就生效了。当然,web
相关的其他自动配置类生效也是这个原理。
至此,我们也明白了spring-boot-autoconfigure
模块为什么要把引入的spring-webmvc
这个依赖作为可选依赖了,其目的就是为了在spring-boot-starter-web
起步依赖中能显式引入spring-webmvc
这个依赖(这个起决定性作用),从而我们开发web项目只要引入了spring-boot-starter-web
起步依赖,那么web相关的自动配置类就生效,从而可以开箱即用这个就是spring-boot-starter-web
这个起步依赖的构建原理了。
前面提到的spring-boot-starter-actuator
,spring-boot-starter-test
及其他内置的spring-boot-starter-xxx
的起步依赖的构建原理也是如此,只不过spring-boot-starter-actuator
依赖的是spring-boot-actuator-autoconfigure
,spring-boot-starter-test
依赖的是spring-boot-test-autoconfigure
模块罢了,这里不再详述。
思考:
spring-boot-actuator-autoconfigure
的pom.xml
文件引入了20多个可选依赖,而为什么spring-boot-starter-actuator
起步依赖只引入了micrometer-core
这个依赖呢?
5 模仿SpringBoot包结构自定义一个Starter
前面分析了SpringBoot内置的各种Starter
的构建原理,理论联系实践,那么如果能够动手实践一下自定义Starter
那就更好了。
下面提供一个自定义Starter
的一个简单Demo
,这个Demo
完全模仿SpringBoot
内置Starter
的内部包结构来编写,对于进一步了解SpringBoot内置的各种Starter
的构建原理很有帮助。
下面是这个Demo
的github地址,推荐给有兴趣的小伙伴们。
模仿springboot内部结构自定义Starter。此外,如何自定义一个Starter
,可以参考下Mybatis的spring-boot-starter是如何编写的。
6 小结
好了,SpringBoot内置的各种Starter
的构建原理分析就到此结束了,现将关键点总结下:
spring-boot-starter-xxx
起步依赖没有一行代码,而是直接或间接依赖了xxx-autoconfigure
模块,而xxx-autoconfigure
模块承担了spring-boot-starter-xxx
起步依赖自动配置的实现;xxx-autoconfigure
自动配置模块引入了一些可选依赖,这些可选依赖不会被传递到spring-boot-starter-xxx
起步依赖中,这是起步依赖构建的关键点;spring-boot-starter-xxx
起步依赖显式引入了一些对自动配置起作用的可选依赖;- 经过前面3步的准备,我们项目只要引入了某个起步依赖后,就可以开箱即用了,而不用手动去创建一些
bean
等。
SpringBoot的启动流程是怎样的?SpringBoot源码(七)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
1 温故而知新
本篇接 SpringBoot内置的各种Starter是怎样构建的? SpringBoot源码(六)
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBootSpringBoot内置的各种Starter是怎样构建的?,现将关键点重新回顾总结下:
spring-boot-starter-xxx
起步依赖没有一行代码,而是直接或间接依赖了xxx-autoconfigure
模块,而xxx-autoconfigure
模块承担了spring-boot-starter-xxx
起步依赖自动配置的实现;xxx-autoconfigure
自动配置模块引入了一些可选依赖,这些可选依赖不会被传递到spring-boot-starter-xxx
起步依赖中,这是起步依赖构建的关键点;spring-boot-starter-xxx
起步依赖显式引入了一些对自动配置起作用的可选依赖,因此会触发xxx-autoconfigure
自动配置的逻辑(比如创建某些符合条件的配置bean
);- 经过前面3步的准备,我们项目只要引入了某个起步依赖后,就可以开箱即用了,而不用手动去创建一些
bean
等。
2 引言
本来这篇文章会继续SpringBoot自动配置的源码分析的,想分析下spring-boot-starter-web
的自动配置的源码是怎样的的。但是考虑到spring-boot-starter-web
的自动配置逻辑跟内置Tomcat
等有关,因此想以后等分析了SpringBoot的内置Tomcat
的相关源码后再来继续分析spring-boot-starter-web
的自动配置的源码。
因此,本篇我们来探究下SpringBoot的启动流程是怎样的?
3 如何编写一个SpringBoot启动类
我们都知道,我们运行一个SpringBoot项目,引入相关Starters
和相关依赖后,再编写一个启动类,然后在这个启动类标上@SpringBootApplication
注解,然后就可以启动运行项目了,如下代码:
//MainApplication.java
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
如上代码,我们在MainApplication
启动类上标注了@SpringBootApplication
注解,然后在main
函数中调用SpringApplication.run(MainApplication.class, args);
这句代码就完成了SpringBoot的启动流程,非常简单。
4 @SpringBootApplication
现在我们来分析下标注在启动类上的@SpringBootApplication
注解,直接上源码:
// SpringBootApplication.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { // TODO 这两个排除过滤器TypeExcludeFilter和AutoConfigurationExcludeFilter暂不知道啥作用
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// 等同于EnableAutoConfiguration注解的exclude属性
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
// 等同于EnableAutoConfiguration注解的excludeName属性
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
// 等同于ComponentScan注解的basePackages属性
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
// 等同于ComponentScan注解的basePackageClasses属性
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
可以看到,@SpringBootApplication
注解是一个组合注解,主要由@SpringBootConfiguration
,@EnableAutoConfiguration
和@ComponentScan
这三个注解组合而成。
因此@SpringBootApplication
注解主要作为一个配置类,能够触发包扫描和自动配置的逻辑,从而使得SpringBoot的相关bean
被注册进Spring容器。
5 SpringBoot的启动流程是怎样的?
接下来是本篇的重点,我们来分析下SpringBoot的启动流程是怎样的?
我们接着来看前面main
函数里的SpringApplication.run(MainApplication.class, args);
这句代码,那么SpringApplication
这个类是干嘛的呢?
SpringApplication
类是用来启动SpringBoot项目的,可以在java的main
方法中启动,目前我们知道这些就足够了。下面看下SpringApplication.run(MainApplication.class, args);
这句代码的源码:
// SpringApplication.java
// run方法是一个静态方法,用于启动SpringBoot
public static ConfigurableApplicationContext run(Class<?> primarySource,
String... args) {
// 继续调用静态的run方法
return run(new Class<?>[] { primarySource }, args);
}
在上面的静态run
方法里又继续调用另一个静态run
方法:
// SpringApplication.java
// run方法是一个静态方法,用于启动SpringBoot
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
// 构建一个SpringApplication对象,并调用其run方法来启动
return new SpringApplication(primarySources).run(args);
}
如上代码,可以看到构建了一个SpringApplication
对象,然后再调用其run
方法来启动SpringBoot项目。关于SpringApplication
对象是如何构建的,我们后面再分析,现在直接来看下启动流程的源码:
// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
// new 一个StopWatch用于统计run启动过程花了多少时间
StopWatch stopWatch = new StopWatch();
// 开始计时
stopWatch.start();
ConfigurableApplicationContext context = null;
// exceptionReporters集合用来存储异常报告器,用来报告SpringBoot启动过程的异常
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 配置headless属性,即“java.awt.headless”属性,默认为ture
// 其实是想设置该应用程序,即使没有检测到显示器,也允许其启动.对于服务器来说,是不需要显示器的,所以要这样设置.
configureHeadlessProperty();
// 【1】从spring.factories配置文件中加载到EventPublishingRunListener对象并赋值给SpringApplicationRunListeners
// EventPublishingRunListener对象主要用来发射SpringBoot启动过程中内置的一些生命周期事件,标志每个不同启动阶段
SpringApplicationRunListeners listeners = getRunListeners(args);
// 启动SpringApplicationRunListener的监听,表示SpringApplication开始启动。
// 》》》》》发射【ApplicationStartingEvent】事件
listeners.starting();
try {
// 创建ApplicationArguments对象,封装了args参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 【2】准备环境变量,包括系统变量,环境变量,命令行参数,默认变量,servlet相关配置变量,随机值,
// JNDI属性值,以及配置文件(比如application.properties)等,注意这些环境变量是有优先级的
// 》》》》》发射【ApplicationEnvironmentPreparedEvent】事件
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
// 配置spring.beaninfo.ignore属性,默认为true,即跳过搜索BeanInfo classes.
configureIgnoreBeanInfo(environment);
// 【3】控制台打印SpringBoot的bannner标志
Banner printedBanner = printBanner(environment);
// 【4】根据不同类型创建不同类型的spring applicationcontext容器
// 因为这里是servlet环境,所以创建的是AnnotationConfigServletWebServerApplicationContext容器对象
context = createApplicationContext();
// 【5】从spring.factories配置文件中加载异常报告期实例,这里加载的是FailureAnalyzers
// 注意FailureAnalyzers的构造器要传入ConfigurableApplicationContext,因为要从context中获取beanFactory和environment
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context); // ConfigurableApplicationContext是AnnotationConfigServletWebServerApplicationContext的父接口
// 【6】为刚创建的AnnotationConfigServletWebServerApplicationContext容器对象做一些初始化工作,准备一些容器属性值等
// 1)为AnnotationConfigServletWebServerApplicationContext的属性AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner设置environgment属性
// 2)根据情况对ApplicationContext应用一些相关的后置处理,比如设置resourceLoader属性等
// 3)在容器刷新前调用各个ApplicationContextInitializer的初始化方法,ApplicationContextInitializer是在构建SpringApplication对象时从spring.factories中加载的
// 4)》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
// 5)从context容器中获取beanFactory,并向beanFactory中注册一些单例bean,比如applicationArguments,printedBanner
// 6)TODO 加载bean到application context,注意这里只是加载了部分bean比如mainApplication这个bean,大部分bean应该是在AbstractApplicationContext.refresh方法中被加载?这里留个疑问先
// 7)》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
// 【7】刷新容器,这一步至关重要,以后会在分析Spring源码时详细分析,主要做了以下工作:
// 1)在context刷新前做一些准备工作,比如初始化一些属性设置,属性合法性校验和保存容器中的一些早期事件等;
// 2)让子类刷新其内部bean factory,注意SpringBoot和Spring启动的情况执行逻辑不一样
// 3)对bean factory进行配置,比如配置bean factory的类加载器,后置处理器等
// 4)完成bean factory的准备工作后,此时执行一些后置处理逻辑,子类通过重写这个方法来在BeanFactory创建并预准备完成以后做进一步的设置
// 在这一步,所有的bean definitions将会被加载,但此时bean还不会被实例化
// 5)执行BeanFactoryPostProcessor的方法即调用bean factory的后置处理器:
// BeanDefinitionRegistryPostProcessor(触发时机:bean定义注册之前)和BeanFactoryPostProcessor(触发时机:bean定义注册之后bean实例化之前)
// 6)注册bean的后置处理器BeanPostProcessor,注意不同接口类型的BeanPostProcessor;在Bean创建前后的执行时机是不一样的
// 7)初始化国际化MessageSource相关的组件,比如消息绑定,消息解析等
// 8)初始化事件广播器,如果bean factory没有包含事件广播器,那么new一个SimpleApplicationEventMulticaster广播器对象并注册到bean factory中
// 9)AbstractApplicationContext定义了一个模板方法onRefresh,留给子类覆写,比如ServletWebServerApplicationContext覆写了该方法来创建内嵌的tomcat容器
// 10)注册实现了ApplicationListener接口的监听器,之前已经有了事件广播器,此时就可以派发一些early application events
// 11)完成容器bean factory的初始化,并初始化所有剩余的单例bean。这一步非常重要,一些bean postprocessor会在这里调用。
// 12)完成容器的刷新工作,并且调用生命周期处理器的onRefresh()方法,并且发布ContextRefreshedEvent事件
refreshContext(context);
// 【8】执行刷新容器后的后置处理逻辑,注意这里为空方法
afterRefresh(context, applicationArguments);
// 停止stopWatch计时
stopWatch.stop();
// 打印日志
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
listeners.started(context);
// 【9】调用ApplicationRunner和CommandLineRunner的run方法,实现spring容器启动后需要做的一些东西比如加载一些业务数据等
callRunners(context, applicationArguments);
}
// 【10】若启动过程中抛出异常,此时用FailureAnalyzers来报告异常
// 并》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
listeners.running(context);
}
// 若出现异常,此时仅仅报告异常,而不会发射任何事件
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
// 【11】最终返回容器
return context;
}
如上代码就是SpringBoot的启动流程了,其中注释也非常详细,主要步骤也已经标注【x】
,现将主要步骤总结如下:
- 从
spring.factories
配置文件中加载EventPublishingRunListener
对象,该对象拥有SimpleApplicationEventMulticaster
属性,即在SpringBoot启动过程的不同阶段用来发射内置的生命周期事件; - 准备环境变量,包括系统变量,环境变量,命令行参数,默认变量,
servlet
相关配置变量,随机值以及配置文件(比如application.properties
)等; - 控制台打印SpringBoot的
bannner
标志; - 根据不同类型环境创建不同类型的
applicationcontext
容器,因为这里是servlet
环境,所以创建的是AnnotationConfigServletWebServerApplicationContext
容器对象; - 从
spring.factories
配置文件中加载FailureAnalyzers
对象,用来报告SpringBoot启动过程中的异常; - 为刚创建的容器对象做一些初始化工作,准备一些容器属性值等,对
ApplicationContext
应用一些相关的后置处理和调用各个ApplicationContextInitializer
的初始化方法来执行一些初始化逻辑等; - 刷新容器,这一步至关重要。比如调用
bean factory
的后置处理器,注册BeanPostProcessor
后置处理器,初始化事件广播器且广播事件,初始化剩下的单例bean
和SpringBoot创建内嵌的Tomcat
服务器等等重要且复杂的逻辑都在这里实现,主要步骤可见代码的注释,关于这里的逻辑会在以后的spring源码分析专题详细分析; - 执行刷新容器后的后置处理逻辑,注意这里为空方法;
- 调用
ApplicationRunner
和CommandLineRunner
的run方法,我们实现这两个接口可以在spring容器启动后需要的一些东西比如加载一些业务数据等; - 报告启动异常,即若启动过程中抛出异常,此时用
FailureAnalyzers
来报告异常; - 最终返回容器对象,这里调用方法没有声明对象来接收。
当然在SpringBoot启动过程中,每个不同的启动阶段会分别发射不同的内置生命周期事件,比如在准备environment
前会发射ApplicationStartingEvent
事件,在environment
准备好后会发射ApplicationEnvironmentPreparedEvent
事件,在刷新容器前会发射ApplicationPreparedEvent
事件等,总之SpringBoot总共内置了7个生命周期事件,除了标志SpringBoot的不同启动阶段外,同时一些监听器也会监听相应的生命周期事件从而执行一些启动初始化逻辑。
6 小结
好了,SpringBoot的启动流程就已经分析完了,这篇内容主要让我们对SpringBoot的启动流程有一个整体的认识,现在还没必要去深究每一个细节,以免丢了主线,现在我们对SpringBoot的启动流程有一个整体的认识即可,关于启动流程的一些重要步骤我们会在以后的源码分析中来深究。
Java是如何实现自己的SPI机制的? JDK源码(一)
注:该源码分析对应JDK版本为1.8
1 引言
这是【源码笔记】的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码。
2 什么是SPI机制
那么,什么是SPI机制呢?
SPI是Service Provider Interface 的简称,即服务提供者接口的意思。根据字面意思我们可能还有点困惑,SPI说白了就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化,其实SPI就是这么一个东西。说到SPI机制,我们最常见的就是Java的SPI机制,此外,还有Dubbo和SpringBoot自定义的SPI机制。
有了SPI机制,那么就为一些框架的灵活扩展提供了可能,而不必将框架的一些实现类写死在代码里面。
那么,某些框架是如何利用SPI机制来做到灵活扩展的呢?下面举几个栗子来阐述下:
- JDBC驱动加载案例:利用Java的SPI机制,我们可以根据不同的数据库厂商来引入不同的JDBC驱动包;
- SpringBoot的SPI机制:我们可以在
spring.factories
中加上我们自定义的自动配置类,事件监听器或初始化器等; - Dubbo的SPI机制:Dubbo更是把SPI机制应用的淋漓尽致,Dubbo基本上自身的每个功能点都提供了扩展点,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现替换掉Dubbo的实现即可。
上面的三个栗子先让我们直观感受下某些框架利用SPI机制是如何做到灵活扩展的。
3 如何使用Java的SPI?
我们先来看看如何使用Java自带的SPI。
先定义一个Developer
接口
// Developer.java
package com.ymbj.spi;
public interface Developer {
void sayHi();
}
再定义两个Developer
接口的两个实现类:
// JavaDeveloper.java
package com.ymbj.spi;
public class JavaDeveloper implements Developer {
@Override
public void sayHi() {
System.out.println("Hi, I am a Java Developer.");
}
}
// PythonDeveloper.java
package com.ymbj.spi;
public class PythonDeveloper implements Developer {
@Override
public void sayHi() {
System.out.println("Hi, I am a Python Developer.");
}
}
然后再在项目resources
目录下新建一个META-INF/services
文件夹,然后再新建一个以Developer
接口的全限定名命名的文件,文件内容为:
// com.ymbj.spi.Developer文件
com.ymbj.spi.JavaDeveloper
com.ymbj.spi.PythonDeveloper
最后我们再新建一个测试类JdkSPITest
:
// JdkSPITest.java
public class JdkSPITest {
@Test
public void testSayHi() throws Exception {
ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);
serviceLoader.forEach(Developer::sayHi);
}
}
运行上面那个测试类,运行成功结果如下截图所示:
由上面简单的Demo我们知道了如何使用Java的SPI机制来实现扩展点加载,下面推荐一篇文章JAVA拾遗--关于SPI机制,通过这篇文章,相信大家对Java的SPI会有一个比较深刻的理解,特别是JDBC加载驱动这方面。
4 Java的SPI机制的源码解读
通过前面扩展Developer
接口的简单Demo,我们看到Java的SPI机制实现跟ServiceLoader
这个类有关,那么我们先来看下ServiceLoader
的类结构代码:
// ServiceLoader实现了【Iterable】接口
public final class ServiceLoader<S>
implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
// 构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// ...暂时省略相关代码
// ServiceLoader的内部类LazyIterator,实现了【Iterator】接口
// Private inner class implementing fully-lazy provider lookup
private class LazyIterator
implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
// 覆写Iterator接口的hasNext方法
public boolean hasNext() {
// ...暂时省略相关代码
}
// 覆写Iterator接口的next方法
public S next() {
// ...暂时省略相关代码
}
// 覆写Iterator接口的remove方法
public void remove() {
// ...暂时省略相关代码
}
}
// 覆写Iterable接口的iterator方法,返回一个迭代器
public Iterator<S> iterator() {
// ...暂时省略相关代码
}
// ...暂时省略相关代码
}
可以看到,ServiceLoader
实现了Iterable
接口,覆写其iterator
方法能产生一个迭代器;同时ServiceLoader
有一个内部类LazyIterator
,而LazyIterator
又实现了Iterator
接口,说明LazyIterator
是一个迭代器。
4.1 ServiceLoader.load方法,为加载服务提供者实现类做前期准备
那么我们现在开始探究Java的SPI机制的源码,
先来看JdkSPITest
的第一句代码ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);
中的ServiceLoader.load(Developer.class);
的源码:
// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 将service接口类和线程上下文类加载器作为参数传入,继续调用load方法
return ServiceLoader.load(service, cl);
}
我们再来看下ServiceLoader.load(service, cl);
方法:
// ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 将service接口类和线程上下文类加载器作为构造参数,新建了一个ServiceLoader对象
return new ServiceLoader<>(service, loader);
}
继续看new ServiceLoader<>(service, loader);
是如何构建的?
// ServiceLoader.java
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
可以看到在构建ServiceLoader
对象时除了给其成员属性赋值外,还调用了reload
方法:
// ServiceLoader.java
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
可以看到在reload
方法中又新建了一个LazyIterator
对象,然后赋值给lookupIterator
。
// ServiceLoader$LazyIterator.java
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
可以看到在构建LazyIterator
对象时,也只是给其成员变量service
和loader
属性赋值呀,我们一路源码跟下来,也没有看到去META-INF/services
文件夹加载Developer
接口的实现类!这就奇怪了,我们都被ServiceLoader
的load
方法名骗了。
还记得分析前面的代码时新建了一个LazyIterator
对象吗?Lazy
顾名思义是懒的意思,Iterator
就是迭代的意思。我们此时猜测那么LazyIterator
对象的作用应该就是在迭代的时候再去加载Developer
接口的实现类了。
4.2 ServiceLoader.iterator方法,实现服务提供者实现类的懒加载
我们现在再来看JdkSPITest
的第二句代码serviceLoader.forEach(Developer::sayHi);
,执行这句代码后最终会调用serviceLoader
的iterator
方法:
// serviceLoader.java
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
// 调用lookupIterator即LazyIterator的hasNext方法
// 可以看到是委托给LazyIterator的hasNext方法来实现
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 调用lookupIterator即LazyIterator的next方法
// 可以看到是委托给LazyIterator的next方法来实现
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
可以看到调用serviceLoader
的iterator
方法会返回一个匿名的迭代器对象,而这个匿名迭代器对象其实相当于一个门面类,其覆写的hasNext
和next
方法又分别委托LazyIterator
的hasNext
和next
方法来实现了。
我们继续调试,发现接下来会进入LazyIterator
的hasNext
方法:
// serviceLoader$LazyIterator.java
public boolean hasNext() {
if (acc == null) {
// 调用hasNextService方法
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
继续跟进hasNextService
方法:
// serviceLoader$LazyIterator.java
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = "META-INF/services/"
// service.getName()即接口的全限定名
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性service赋值吗
String fullName = PREFIX + service.getName();
// 加载META-INF/services/目录下的接口文件中的服务提供者类
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性loader赋值吗
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 返回META-INF/services/目录下的接口文件中的服务提供者类并赋值给pending属性
pending = parse(service, configs.nextElement());
}
// 然后取出一个全限定名赋值给LazyIterator的成员变量nextName
nextName = pending.next();
return true;
}
可以看到在执行LazyIterator
的hasNextService
方法时最终将去META-INF/services/
目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给LazyIterator
的成员变量nextName
。到了这里,我们就明白了LazyIterator
的作用真的是懒加载,在用到的时候才会去加载。
思考:为何这里要用懒加载呢?懒加载的思想是怎样的呢?懒加载有啥好处呢?你还能举出其他懒加载的案例吗?
同样,执行完LazyIterator
的hasNext
方法后,会继续执行LazyIterator
的next
方法:
// serviceLoader$LazyIterator.java
public S next() {
if (acc == null) {
// 调用nextService方法
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
我们继续跟进nextService
方法:
// serviceLoader$LazyIterator.java
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 还记得在hasNextService方法中为nextName赋值过服务提供者实现类的全限定名吗
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 【1】去classpath中根据传入的类加载器和服务提供者实现类的全限定名去加载服务提供者实现类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 【2】实例化刚才加载的服务提供者实现类,并进行转换
S p = service.cast(c.newInstance());
// 【3】最终将实例化后的服务提供者实现类放进providers集合
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
可以看到LazyIterator
的nextService
方法最终将实例化之前加载的服务提供者实现类,并放进providers
集合中,随后再调用服务提供者实现类的方法(比如这里指JavaDeveloper
的sayHi
方法)。注意,这里是加载一个服务提供者实现类后,若main
函数中有调用该服务提供者实现类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。
设计模式:可以看到,Java的SPI机制实现代码中应用了迭代器模式,迭代器模式屏蔽了各种存储对象的内部结构差异,提供一个统一的视图来遍历各个存储对象(存储对象可以为集合,数组等)。
java.util.Iterator
也是迭代器模式的实现:同时Java的各个集合类一般实现了Iterable
接口,实现了其iterator
方法从而获得Iterator
接口的实现类对象(一般为集合内部类),然后再利用Iterator
对象的实现类的hasNext
和next
方法来遍历集合元素。
5 JDBC驱动加载源码解读
前面分析了Java的SPI机制的源码实现,现在我们再来看下Java的SPI机制的实际案例的应用。
我们都知道,JDBC驱动加载是Java的SPI机制的典型应用案例。JDBC主要提供了一套接口规范,而这套规范的api在java的核心库(rt.jar
)中实现,而不同的数据库厂商只要编写符合这套JDBC接口规范的驱动代码,那么就可以用Java语言来连接数据库了。
java的核心库(rt.jar
)中跟JDBC驱动加载的最核心的接口和类分别是java.sql.Driver
接口和java.sql.DriverManager
类,其中java.sql.Driver
是各个数据库厂商的驱动类要实现的接口,而DriverManager
是用来管理数据库的驱动类的,值得注意的是DriverManager
这个类有一个registeredDrivers
集合属性,用来存储数据库的驱动类。
// DriverManager.java
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
这里以加载Mysql驱动为例来分析JDBC驱动加载的源码。
我们的项目引入mysql-connector-java
依赖(这里的版本是5.1.47
)后,那么Mysql的驱动实现类文件如下图所示:
可以看到Mysql的驱动包中有两个Driver
驱动类,分别是com.mysql.jdbc.Driver
和com.mysql.fabric.jdbc.FabricMySQLDriver
,默认情况下一般我们只用到前者。
5.1 利用Java的SPI加载Mysql的驱动类
那么接下来我们就来探究下JDBC驱动加载的代码是如何实现的。
先来看一下一个简单的JDBC的测试代码:
// JdbcTest.java
public class JdbcTest {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
// 注意:在JDBC 4.0规范中,这里可以不用再像以前那样编写显式加载数据库的代码了
// Class.forName("com.mysql.jdbc.Driver");
// 获取数据库连接,注意【这里将会加载mysql的驱动包】
/***************【主线,切入点】****************/
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");
// 创建Statement语句
statement = connection.createStatement();
// 执行查询语句
rs = statement.executeQuery("select * from user");
// 遍历查询结果集
while(rs.next()){
String name = rs.getString("name");
System.out.println(name);
}
} catch(Exception e) {
e.printStackTrace();
} finally {
// ...省略释放资源的代码
}
}
}
在JdbcTest
的main
函数调用DriverManager
的getConnection
方法时,此时必然会先执行DriverManager
类的静态代码块的代码,然后再执行getConnection
方法,那么先来看下DriverManager
的静态代码块:
// DriverManager.java
static {
// 加载驱动实现类
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
继续跟进loadInitialDrivers
的代码:
// DriverManager.java
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 来到这里,是不是感觉似曾相识,对,没错,我们在前面的JdkSPITest代码中执行过下面的两句代码
// 这句代码前面已经分析过,这里不会真正加载服务提供者实现类
// 而是实例化一个ServiceLoader对象且实例化一个LazyIterator对象用于懒加载
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 调用ServiceLoader的iterator方法,在迭代的同时,也会去加载并实例化META-INF/services/java.sql.Driver文件
// 的com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver两个驱动类
/****************【主线,重点关注】**********************/
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
在上面的代码中,我们可以看到Mysql的驱动类加载主要是利用Java的SPI机制实现的,即利用ServiceLoader
来实现加载并实例化Mysql的驱动类。
5.2 注册Mysql的驱动类
那么,上面的代码只是Mysql驱动类的加载和实例化,那么,驱动类又是如何被注册进DriverManager
的registeredDrivers
集合的呢?
这时,我们注意到com.mysql.jdbc.Driver
类里面也有个静态代码块,即实例化该类时肯定会触发该静态代码块代码的执行,那么我们直接看下这个静态代码块做了什么事情:
// com.mysql.jdbc.Driver.java
// Register ourselves with the DriverManager
static {
try {
// 将自己注册进DriverManager类的registeredDrivers集合
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
可以看到,原来就是Mysql驱动类com.mysql.jdbc.Driver
在实例化的时候,利用执行其静态代码块的时机时将自己注册进DriverManager
的registeredDrivers
集合中。
好,继续跟进DriverManager
的registerDriver
方法:
// DriverManager.java
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
// 继续调用registerDriver方法
registerDriver(driver, null);
}
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
// 将driver驱动类实例注册进registeredDrivers集合
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
分析到了这里,我们就明白了Java的SPI机制是如何加载Mysql的驱动类的并如何将Mysql的驱动类注册进DriverManager
的registeredDrivers
集合中的。
5.3 使用之前注册的Mysql驱动类连接数据库
既然Mysql的驱动类已经被注册进来了,那么何时会被用到呢?
我们要连接Mysql数据库,自然需要用到Mysql的驱动类,对吧。此时我们回到JDBC的测试代码JdbcTest
类的connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");
这句代码中,看一下getConnection
的源码:
// DriverManager.java
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
// 继续调用getConnection方法来连接数据库
return (getConnection(url, info, Reflection.getCallerClass()));
}
继续跟进getConnection
方法:
// DriverManager.java
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
// 遍历registeredDrivers集合,注意之前加载的Mysql驱动类实例被注册进这个集合
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
// 判断有无权限
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
// 利用Mysql驱动类来连接数据库
/*************【主线,重点关注】*****************/
Connection con = aDriver.driver.connect(url, info);
// 只要连接上,那么加载的其余驱动类比如FabricMySQLDriver将会忽略,因为下面直接返回了
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
可以看到,DriverManager
的getConnection
方法会从registeredDrivers
集合中拿出刚才加载的Mysql驱动类来连接数据库。
好了,到了这里,JDBC驱动加载的源码就基本分析完了。
6 线程上下文类加载器
前面基本分析完了JDBC驱动加载的源码,但是还有一个很重要的知识点还没讲解,那就是破坏类加载机制的双亲委派模型的线程上下文类加载器。
我们都知道,JDBC规范的相关类(比如前面的java.sql.Driver
和java.sql.DriverManager
)都是在Jdk的rt.jar
包下,意味着这些类将由启动类加载器(BootstrapClassLoader)加载;而Mysql的驱动类由外部数据库厂商实现,当驱动类被引进项目时也是位于项目的classpath
中,此时启动类加载器肯定是不可能加载这些驱动类的呀,此时该怎么办?
由于类加载机制的双亲委派模型在这方面的缺陷,因此只能打破双亲委派模型了。因为项目classpath
中的类是由应用程序类加载器(AppClassLoader)来加载,所以我们可否"逆向"让启动类加载器委托应用程序类加载器去加载这些外部数据库厂商的驱动类呢?如果可以,我们怎样才能做到让启动类加载器委托应用程序类加载器去加载classpath
中的类呢?
答案肯定是可以的,我们可以将应用程序类加载器设置进线程里面,即线程里面新定义一个类加载器的属性contextClassLoader
,然后在某个时机将应用程序类加载器设置进线程的contextClassLoader
这个属性里面,如果没有设置的话,那么默认就是应用程序类加载器。然后启动类加载器去加载java.sql.Driver
和java.sql.DriverManager
等类时,同时也会从当前线程中取出contextClassLoader
即应用程序类加载器去classpath
中加载外部厂商提供的JDBC驱动类。因此,通过破坏类加载机制的双亲委派模型,利用线程上下文类加载器完美的解决了该问题。
此时我们再回过头来看下在加载Mysql驱动时是什么时候获取的线程上下文类加载器呢?
答案就是在DriverManager
的loadInitialDrivers
方法调用了ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这句代码,而取出线程上下文类加载器就是在ServiceLoader
的load
方法中取出:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 取出线程上下文类加载器取出的是contextClassLoader,而contextClassLoader装的应用程序类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 把刚才取出的线程上下文类加载器作为参数传入,用于后去加载classpath中的外部厂商提供的驱动类
return ServiceLoader.load(service, cl);
}
因此,到了这里,我们就明白了线程上下文类加载器在加载JDBC驱动包中充当的作用了。此外,我们应该知道,Java的绝大部分涉及SPI的加载都是利用线程上下文类加载器来完成的,比如JNDI,JCE,JBI等。
扩展:打破类加载机制的双亲委派模型的还有代码的热部署等,另外,Tomcat的类加载机制也值得一读。
注:若有些小伙伴对类加载机制的双亲委派模型不清楚的话,推荐完全理解双亲委派模型与自定义 ClassLoader这篇文了解下。
7 扩展:Dubbo的SPI机制
前面也讲到Dubbo框架身上处处是SPI机制的应用,可以说处处都是扩展点,真的是把SPI机制应用的淋漓尽致。但是Dubbo没有采用默认的Java的SPI机制,而是自己实现了一套SPI机制。
那么,Dubbo为什么没有采用Java的SPI机制呢?
原因主要有两个:
- Java的SPI机制会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源;
- Java的SPI机制没有Ioc和AOP的支持,因此Dubbo用了自己的SPI机制:增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。
由于以上原因,Dubbo自定义了一套SPI机制,用于加载自己的扩展点。关于Dubbo的SPI机制这里不再详述,感兴趣的小伙伴们可以去Dubbo官网看看是如何扩展Dubbo的SPI的?还有其官网也有Duboo的SPI的源码分析文章。
8 小结
好了,Java的SPI机制就解读到这里了,先将前面的知识点再总结下:
- Java的SPI机制的使用;
- Java的SPI机制的原理;
- JDBC驱动的加载原理;
- 线程上下文类加载器在JDBC驱动加载中的作用;
- 简述了Duboo的SPI机制。
SpringApplication对象是如何构建的? SpringBoot源码(八)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE
本篇接 SpringBoot的启动流程是怎样的?SpringBoot源码(七)
1 温故而知新
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBoot的启动流程,现将关键步骤再浓缩总结下:
- 构建
SpringApplication
对象,用于启动SpringBoot; - 从
spring.factories
配置文件中加载EventPublishingRunListener
对象用于在不同的启动阶段发射不同的生命周期事件; - 准备环境变量,包括系统变量,环境变量,命令行参数及配置文件(比如
application.properties
)等; - 创建容器
ApplicationContext
; - 为第4步创建的容器对象做一些初始化工作,准备一些容器属性值等,同时调用各个
ApplicationContextInitializer
的初始化方法来执行一些初始化逻辑等; - 刷新容器,这一步至关重要,是重点中的重点,太多复杂逻辑在这里实现;
- 调用
ApplicationRunner
和CommandLineRunner
的run方法,可以实现这两个接口在容器启动后来加载一些业务数据等;
在SpringBoot启动过程中,每个不同的启动阶段会分别发射不同的内置生命周期事件,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作比如ConfigFileApplicationListener
会监听onApplicationEnvironmentPreparedEvent
事件来加载环境变量等。
2 引言
上篇文章在讲解SpringBoot的启动流程中,我们有看到新建了一个SpringApplication
对象用来启动SpringBoot项目。那么,我们今天就来看看SpringApplication
对象的构建过程,同时讲解一下SpringBoot自己实现的SPI机制。
3 SpringApplication对象的构建过程
本小节开始讲解SpringApplication
对象的构造过程,因为一个对象的构造无非就是在其构造函数里给它的一些成员属性赋值,很少包含其他额外的业务逻辑(当然有时候我们可能也会在构造函数里开启一些线程啥的)。那么,我们先来看下构造SpringApplication
对象时需要用到的一些成员属性哈:
// SpringApplication.java
/**
* SpringBoot的启动类即包含main函数的主类
*/
private Set<Class<?>> primarySources;
/**
* 包含main函数的主类
*/
private Class<?> mainApplicationClass;
/**
* 资源加载器
*/
private ResourceLoader resourceLoader;
/**
* 应用类型
*/
private WebApplicationType webApplicationType;
/**
* 初始化器
*/
private List<ApplicationContextInitializer<?>> initializers;
/**
* 监听器
*/
private List<ApplicationListener<?>> listeners;
可以看到构建SpringApplication
对象时主要是给上面代码中的六个成员属性赋值,现在我接着来看SpringApplication
对象的构造过程。
我们先回到上一篇文章讲解的构建SpringApplication
对象的代码处:
// SpringApplication.java
// run方法是一个静态方法,用于启动SpringBoot
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
// 构建一个SpringApplication对象,并调用其run方法来启动
return new SpringApplication(primarySources).run(args);
}
跟进SpringApplication
的构造函数中:
// SpringApplication.java
public SpringApplication(Class<?>... primarySources) {
// 继续调用SpringApplication另一个构造函数
this(null, primarySources);
}
继续跟进SpringApplication
另一个构造函数:
// SpringApplication.java
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 【1】给resourceLoader属性赋值,注意传入的resourceLoader参数为null
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 【2】给primarySources属性赋值,传入的primarySources其实就是SpringApplication.run(MainApplication.class, args);中的MainApplication.class
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 【3】给webApplicationType属性赋值,根据classpath中存在哪种类型的类来确定是哪种应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 【4】给initializers属性赋值,利用SpringBoot自定义的SPI从spring.factories中加载ApplicationContextInitializer接口的实现类并赋值给initializers属性
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
// 【5】给listeners属性赋值,利用SpringBoot自定义的SPI从spring.factories中加载ApplicationListener接口的实现类并赋值给listeners属性
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 【6】给mainApplicationClass属性赋值,即这里要推断哪个类调用了main函数,然后再赋值给mainApplicationClass属性,用于后面启动流程中打印一些日志。
this.mainApplicationClass = deduceMainApplicationClass();
}
可以看到构建SpringApplication
对象时其实就是给前面讲的6个SpringApplication
类的成员属性赋值而已,做一些初始化工作:
- 给
resourceLoader
属性赋值,resourceLoader
属性,资源加载器,此时传入的resourceLoader
参数为null
; - 给
primarySources
属性赋值,primarySources
属性即SpringApplication.run(MainApplication.class,args);
中传入的MainApplication.class
,该类为SpringBoot项目的启动类,主要通过该类来扫描Configuration
类加载bean
; - 给
webApplicationType
属性赋值,webApplicationType
属性,代表应用类型,根据classpath
存在的相应Application
类来判断。因为后面要根据webApplicationType
来确定创建哪种Environment
对象和创建哪种ApplicationContext
,详细分析请见后面的第3.1小节
; - 给
initializers
属性赋值,initializers
属性为List<ApplicationContextInitializer<?>>
集合,利用SpringBoot的SPI机制从spring.factories
配置文件中加载,后面在初始化容器的时候会应用这些初始化器来执行一些初始化工作。因为SpringBoot自己实现的SPI机制比较重要,因此独立成一小节来分析,详细分析请见后面的第4小节
; - 给
listeners
属性赋值,listeners
属性为List<ApplicationListener<?>>
集合,同样利用利用SpringBoot的SPI机制从spring.factories
配置文件中加载。因为SpringBoot启动过程中会在不同的阶段发射一些事件,所以这些加载的监听器们就是来监听SpringBoot启动过程中的一些生命周期事件的; - 给
mainApplicationClass
属性赋值,mainApplicationClass
属性表示包含main
函数的类,即这里要推断哪个类调用了main
函数,然后把这个类的全限定名赋值给mainApplicationClass
属性,用于后面启动流程中打印一些日志,详细分析见后面的第3.2小节
。
3.1 推断项目应用类型
我们接着分析构造SpringApplication
对象的第【3】
步WebApplicationType.deduceFromClasspath();
这句代码:
// WebApplicationType.java
public enum WebApplicationType {
// 普通的应用
NONE,
// Servlet类型的web应用
SERVLET,
// Reactive类型的web应用
REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework."
+ "web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org."
+ "springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
static WebApplicationType deduceFromClasspath() {
// 若classpath中不存在"org.springframework." + "web.servlet.DispatcherServlet"和"org.glassfish.jersey.servlet.ServletContainer"
// 则返回WebApplicationType.REACTIVE,表明是reactive应用
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
// 若{ "javax.servlet.Servlet",
// "org.springframework.web.context.ConfigurableWebApplicationContext" }
// 都不存在在classpath,则说明是不是web应用
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
// 最终返回普通的web应用
return WebApplicationType.SERVLET;
}
}
如上代码,根据classpath
判断应用类型,即通过反射加载classpath
判断指定的标志类存在与否来分别判断是Reactive
应用,Servlet
类型的web应用还是普通的应用。
3.2 推断哪个类调用了main函数
我们先跳过构造SpringApplication
对象的第【4】
步和第【5】
步,先来分析构造SpringApplication
对象的第【6】
步this.mainApplicationClass = deduceMainApplicationClass();
这句代码:
// SpringApplication.java
private Class<?> deduceMainApplicationClass() {
try {
// 获取StackTraceElement对象数组stackTrace,StackTraceElement对象存储了调用栈相关信息(比如类名,方法名等)
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
// 遍历stackTrace数组
for (StackTraceElement stackTraceElement : stackTrace) {
// 若stackTraceElement记录的调用方法名等于main
if ("main".equals(stackTraceElement.getMethodName())) {
// 那么就返回stackTraceElement记录的类名即包含main函数的类名
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
可以看到deduceMainApplicationClass
方法的主要作用就是从StackTraceElement
调用栈数组中获取哪个类调用了main
方法,然后再返回赋值给mainApplicationClass
属性,然后用于后面启动流程中打印一些日志。
4 SpringBoot的SPI机制原理解读
由于SpringBoot的SPI机制是一个很重要的知识点,因此这里单独一小节来分析。我们都知道,SpringBoot没有使用Java的SPI机制(Java的SPI机制可以看看笔者的Java是如何实现自己的SPI机制的?,真的是干货满满),而是自定义实现了一套自己的SPI机制。SpringBoot利用自定义实现的SPI机制可以加载初始化器实现类,监听器实现类和自动配置类等等。如果我们要添加自动配置类或自定义监听器,那么我们很重要的一步就是在spring.factories
中进行配置,然后才会被SpringBoot加载。
好了,那么接下来我们就来重点分析下SpringBoot是如何是实现自己的SPI机制的。
这里接第3小节的构造SpringApplication
对象的第【4】
步和第【5】
步代码,因为第【4】
步和第【5】
步都是利用SpringBoot的SPI机制来加载扩展实现类,因此这里只分析第【4】
步的setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
这句代码,看看getSpringFactoriesInstances
方法中SpringBoot是如何实现自己的一套SPI来加载ApplicationContextInitializer
初始化器接口的扩展实现类的?
// SpringApplication.java
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
// 继续调用重载的getSpringFactoriesInstances方法进行加载
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
继续跟进重载的getSpringFactoriesInstances
方法:
// SpringApplication.java
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
// 【1】获得类加载器
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 【2】将接口类型和类加载器作为参数传入loadFactoryNames方法,从spring.factories配置文件中进行加载接口实现类
Set<String> names = new LinkedHashSet<>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 【3】实例化从spring.factories中加载的接口实现类
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
// 【4】进行排序
AnnotationAwareOrderComparator.sort(instances);
// 【5】返回加载并实例化好的接口实现类
return instances;
}
可以看到,SpringBoot自定义实现的SPI机制代码中最重要的是上面代码的【1】
,【2】
,【3】
步,这3步下面分别进行重点分析。
4.1 获得类加载器
还记得Java是如何实现自己的SPI机制的?这篇文章中Java的SPI机制默认是利用线程上下文类加载器去加载扩展类的,那么,SpringBoot自己实现的SPI机制又是利用哪种类加载器去加载spring.factories
配置文件中的扩展实现类呢?
我们直接看第【1】
步的ClassLoader classLoader = getClassLoader();
这句代码,先睹为快:
// SpringApplication.java
public ClassLoader getClassLoader() {
// 前面在构造SpringApplicaiton对象时,传入的resourceLoader参数是null,因此不会执行if语句里面的逻辑
if (this.resourceLoader != null) {
return this.resourceLoader.getClassLoader();
}
// 获取默认的类加载器
return ClassUtils.getDefaultClassLoader();
}
继续跟进getDefaultClassLoader
方法:
// ClassUtils.java
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
// 【重点】获取线程上下文类加载器
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
// 这里的逻辑不会执行
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
// 返回刚才获取的线程上下文类加载器
return cl;
}
可以看到,原来SpringBoot的SPI机制中也是用线程上下文类加载器去加载spring.factories
文件中的扩展实现类的!
4.2 加载spring.factories配置文件中的SPI扩展类
我们再来看下第【2】
步中的SpringFactoriesLoader.loadFactoryNames(type, classLoader)
这句代码是如何加载spring.factories
配置文件中的SPI扩展类的?
// SpringFactoriesLoader.java
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
// factoryClass即SPI接口,比如ApplicationContextInitializer,EnableAutoConfiguration等接口
String factoryClassName = factoryClass.getName();
// 【主线,重点关注】继续调用loadSpringFactories方法加载SPI扩展类
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
继续跟进loadSpringFactories
方法:
// SpringFactoriesLoader.java
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 以classLoader作为键先从缓存中取,若能取到则直接返回
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
// 若缓存中无记录,则去spring.factories配置文件中获取
try {
// 这里加载所有jar包中包含"MATF-INF/spring.factories"文件的url路径
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 遍历urls路径,将所有spring.factories文件的键值对(key:SPI接口类名 value:SPI扩展类名)
// 加载放到 result集合中
while (urls.hasMoreElements()) {
// 取出一条url
URL url = urls.nextElement();
// 将url封装到UrlResource对象中
UrlResource resource = new UrlResource(url);
// 利用PropertiesLoaderUtils的loadProperties方法将spring.factories文件键值对内容加载进Properties对象中
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历刚加载的键值对properties对象
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// 取出SPI接口名
String factoryClassName = ((String) entry.getKey()).trim();
// 遍历SPI接口名对应的实现类即SPI扩展类
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
// SPI接口名作为key,SPI扩展类作为value放入result中
result.add(factoryClassName, factoryName.trim());
}
}
}
// 以classLoader作为key,result作为value放入cache缓存
cache.put(classLoader, result);
// 最终返回result对象
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
如上代码,loadSpringFactories
方法主要做的事情就是利用之前获取的线程上下文类加载器将classpath
中的所有spring.factories
配置文件中所有SPI接口的所有扩展实现类给加载出来,然后放入缓存中。注意,这里是一次性加载所有的SPI扩展实现类哈,所以之后根据SPI接口就直接从缓存中获取SPI扩展类了,就不用再次去spring.factories
配置文件中获取SPI接口对应的扩展实现类了。比如之后的获取ApplicationListener
,FailureAnalyzer
和EnableAutoConfiguration
接口的扩展实现类都直接从缓存中获取即可。
思考1: 这里为啥要一次性从
spring.factories
配置文件中获取所有的扩展类放入缓存中呢?而不是每次都是根据SPI接口去spring.factories
配置文件中获取呢?
思考2: 还记得之前讲的SpringBoot的自动配置源码时提到的
AutoConfigurationImportFilter
这个接口的作用吗?现在我们应该能更清楚的理解这个接口的作用了吧。
将所有的SPI扩展实现类加载出来后,此时再调用getOrDefault(factoryClassName, Collections.emptyList())
方法根据SPI接口名去筛选当前对应的扩展实现类,比如这里传入的factoryClassName
参数名为ApplicationContextInitializer
接口,那么这个接口将会作为key
从刚才缓存数据中取出ApplicationContextInitializer
接口对应的SPI扩展实现类。其中从spring.factories
中获取的ApplicationContextInitializer
接口对应的所有SPI扩展实现类如下图所示:
4.3 实例化从spring.factories中加载的SPI扩展类
前面从spring.factories
中获取到ApplicationContextInitializer
接口对应的所有SPI扩展实现类后,此时会将这些SPI扩展类进行实例化。
此时我们再来看下前面的第【3】
步的实例化代码:List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
。
// SpringApplication.java
private <T> List<T> createSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
Set<String> names) {
// 新建instances集合,用于存储稍后实例化后的SPI扩展类对象
List<T> instances = new ArrayList<>(names.size());
// 遍历name集合,names集合存储了所有SPI扩展类的全限定名
for (String name : names) {
try {
// 根据全限定名利用反射加载类
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
// 断言刚才加载的SPI扩展类是否属于SPI接口类型
Assert.isAssignable(type, instanceClass);
// 获得SPI扩展类的构造器
Constructor<?> constructor = instanceClass
.getDeclaredConstructor(parameterTypes);
// 实例化SPI扩展类
T instance = (T) BeanUtils.instantiateClass(constructor, args);
// 添加进instances集合
instances.add(instance);
}
catch (Throwable ex) {
throw new IllegalArgumentException(
"Cannot instantiate " + type + " : " + name, ex);
}
}
// 返回
return instances;
}
上面代码很简单,主要做的事情就是实例化SPI扩展类。
好了,SpringBoot自定义的SPI机制就已经分析完了。
思考3: SpringBoot为何弃用Java的SPI而自定义了一套SPI?
5 小结
好了,本片就到此结束了,先将前面的知识点再总结下:
- 分析了
SpringApplication
对象的构造过程; - 分析了SpringBoot自己实现的一套SPI机制。
SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)
SpringBoot中文注释项目Github地址:
https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE
本篇接 SpringApplication对象是如何构建的? SpringBoot源码(八)
1 温故而知新
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringApplication对象的构建过程及SpringBoot自己实现的一套SPI机制,现将关键步骤再浓缩总结下:
SpringApplication
对象的构造过程其实就是给SpringApplication
类的6个成员变量赋值;- SpringBoot通过以下步骤实现自己的SPI机制:
- 1)首先获取线程上下文类加载器;
- 2)然后利用上下文类加载器从
spring.factories
配置文件中加载所有的SPI扩展实现类并放入缓存中; - 3)根据SPI接口从缓存中取出相应的SPI扩展实现类;
- 4)实例化从缓存中取出的SPI扩展实现类并返回。
2 引言
在SpringBoot启动过程中,每个不同的启动阶段会分别广播不同的内置生命周期事件,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作比如ConfigFileApplicationListener
会监听onApplicationEnvironmentPreparedEvent
事件来加载配置文件application.properties
的环境变量等。
因此本篇内容将来分析下SpringBoot的事件监听机制的源码。
3 SpringBoot广播内置生命周期事件流程分析
为了探究SpringBoot广播内置生命周期事件流程,我们再来回顾一下SpringBoot的启动流程代码:
// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 【0】新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件
SpringApplicationRunListeners listeners = getRunListeners(args);
// 【1】》》》》》发射【ApplicationStartingEvent】事件,标志SpringApplication开始启动
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 【2】》》》》》发射【ApplicationEnvironmentPreparedEvent】事件,此时会去加载application.properties等配置文件的环境变量,同时也有标志环境变量已经准备好的意思
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 【3】》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
// 【4】》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 【5】》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
listeners.started(context);
callRunners(context, applicationArguments);
}
// 【6】》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 【7】》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
可以看到SpringBoot在启动过程中首先会先新建一个SpringApplicationRunListeners
对象用于发射SpringBoot启动过程中的各种生命周期事件,比如发射ApplicationStartingEvent
,ApplicationEnvironmentPreparedEvent
和ApplicationContextInitializedEvent
等事件,然后相应的监听器会执行一些SpringBoot启动过程中的初始化逻辑。那么,监听这些SpringBoot的生命周期事件的监听器们是何时被加载实例化的呢?还记得上篇文章在分析SpringApplication
的构建过程吗?没错,这些执行初始化逻辑的监听器们正是在SpringApplication
的构建过程中根据ApplicationListener
接口去spring.factories
配置文件中加载并实例化的。
3.1 为广播SpringBoot内置生命周期事件做前期准备
3.1.1 加载ApplicationListener监听器实现类
我们再来回顾下SpringApplication对象是如何构建的? SpringBoot源码(八)一文中讲到在构建SpringApplication
对象时的setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
这句代码。
这句代码做的事情就是从spring.factories
中加载出ApplicationListener
事件监听接口的SPI扩展实现类然后添加到SpringApplication
对象的listeners
集合中,用于后续监听SpringBoot启动过程中的事件,来执行一些初始化逻辑工作。
SpringBoot启动时的具体监听器们都实现了ApplicationListener
接口,其在spring.factories
部分配置如下:
不过在调试时,会从所有的spring.factories配置文件中加载监听器,最终加载了10个监听器。如下图:
3.1.2 加载SPI扩展类EventPublishingRunListener
前面讲到,在SpringBoot的启动过程中首先会先新建一个SpringApplicationRunListeners
对象用于发射SpringBoot启动过程中的生命周期事件,即我们现在来看下SpringApplicationRunListeners listeners = getRunListeners(args);
这句代码:
// SpringApplication.java
private SpringApplicationRunListeners getRunListeners(String[] args) {
// 构造一个由SpringApplication.class和String[].class组成的types
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
// 1) 根据SpringApplicationRunListener接口去spring.factories配置文件中加载其SPI扩展实现类
// 2) 构建一个SpringApplicationRunListeners对象并返回
return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
SpringApplicationRunListener.class, types, this, args));
}
我们将重点放到getSpringFactoriesInstances( SpringApplicationRunListener.class, types, this, args)
这句代码,getSpringFactoriesInstances
这个方法我们已经很熟悉,在上一篇分析SpringBoot的SPI机制时已经详细分析过这个方法。可以看到SpringBoot此时又是根据SpringApplicationRunListener
这个SPI接口去spring.factories
中加载相应的SPI扩展实现类,我们直接去spring.factories
中看看SpringApplicationRunListener
有哪些SPI实现类:
由上图可以看到,SpringApplicationRunListener
只有EventPublishingRunListener
这个SPI实现类EventPublishingRunListener
这个哥们在SpringBoot的启动过程中尤其重要,由其在SpringBoot启动过程的不同阶段发射不同的SpringBoot的生命周期事件,即SpringApplicationRunListeners
对象没有承担广播事件的职责,而最终是委托EventPublishingRunListener
这个哥们来广播事件的。
因为从spring.factories
中加载EventPublishingRunListener
类后还会实例化该类,那么我们再跟进EventPublishingRunListener
的源码,看看其是如何承担发射SpringBoot生命周期事件这一职责的?
// EventPublishingRunListener.java
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
private final SpringApplication application;
private final String[] args;
/**
* 拥有一个SimpleApplicationEventMulticaster事件广播器来广播事件
*/
private final SimpleApplicationEventMulticaster initialMulticaster;
public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
// 新建一个事件广播器SimpleApplicationEventMulticaster对象
this.initialMulticaster = new SimpleApplicationEventMulticaster();
// 遍历在构造SpringApplication对象时从spring.factories配置文件中获取的事件监听器
for (ApplicationListener<?> listener : application.getListeners()) {
// 将从spring.factories配置文件中获取的事件监听器们添加到事件广播器initialMulticaster对象的相关集合中
this.initialMulticaster.addApplicationListener(listener);
}
}
@Override
public int getOrder() {
return 0;
}
// 》》》》》发射【ApplicationStartingEvent】事件
@Override
public void starting() {
this.initialMulticaster.multicastEvent(
new ApplicationStartingEvent(this.application, this.args));
}
// 》》》》》发射【ApplicationEnvironmentPreparedEvent】事件
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
this.application, this.args, environment));
}
// 》》》》》发射【ApplicationContextInitializedEvent】事件
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
this.initialMulticaster.multicastEvent(new ApplicationContextInitializedEvent(
this.application, this.args, context));
}
// 》》》》》发射【ApplicationPreparedEvent】事件
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
for (ApplicationListener<?> listener : this.application.getListeners()) {
if (listener instanceof ApplicationContextAware) {
((ApplicationContextAware) listener).setApplicationContext(context);
}
context.addApplicationListener(listener);
}
this.initialMulticaster.multicastEvent(
new ApplicationPreparedEvent(this.application, this.args, context));
}
// 》》》》》发射【ApplicationStartedEvent】事件
@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationStartedEvent(this.application, this.args, context));
}
// 》》》》》发射【ApplicationReadyEvent】事件
@Override
public void running(ConfigurableApplicationContext context) {
context.publishEvent(
new ApplicationReadyEvent(this.application, this.args, context));
}
// 》》》》》发射【ApplicationFailedEvent】事件
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
ApplicationFailedEvent event = new ApplicationFailedEvent(this.application,
this.args, context, exception);
if (context != null && context.isActive()) {
// Listeners have been registered to the application context so we should
// use it at this point if we can
context.publishEvent(event);
}
else {
// An inactive context may not have a multicaster so we use our multicaster to
// call all of the context's listeners instead
if (context instanceof AbstractApplicationContext) {
for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
.getApplicationListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}
this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
this.initialMulticaster.multicastEvent(event);
}
}
// ...省略非关键代码
}
可以看到EventPublishingRunListener
类实现了SpringApplicationRunListener
接口,SpringApplicationRunListener
接口定义了SpringBoot启动时发射生命周期事件的接口方法,而EventPublishingRunListener
类正是通过实现SpringApplicationRunListener
接口的starting
,environmentPrepared
和contextPrepared
等方法来广播SpringBoot不同的生命周期事件,我们直接看下SpringApplicationRunListener
接口源码好了:
// SpringApplicationRunListener.java
public interface SpringApplicationRunListener {
void starting();
void environmentPrepared(ConfigurableEnvironment environment);
void contextPrepared(ConfigurableApplicationContext context);
void contextLoaded(ConfigurableApplicationContext context);
void started(ConfigurableApplicationContext context);
void running(ConfigurableApplicationContext context);
void failed(ConfigurableApplicationContext context, Throwable exception);
}
我们再接着分析EventPublishingRunListener
这个类,可以看到其有一个重要的成员属性initialMulticaster
,该成员属性是SimpleApplicationEventMulticaster
类对象,该类正是承担了广播SpringBoot启动时生命周期事件的职责,即EventPublishingRunListener
对象没有承担广播事件的职责,而最终是委托SimpleApplicationEventMulticaster
这个哥们来广播事件的。 从EventPublishingRunListener
的源码中也可以看到在starting
,environmentPrepared
和contextPrepared
等方法中也正是通过调用SimpleApplicationEventMulticaster
类对象的multicastEvent
方法来广播事件的。
思考 SpringBoot启动过程中发射事件时事件广播者是层层委托职责的,起初由
SpringApplicationRunListeners
对象承担,然后SpringApplicationRunListeners
对象将广播事件职责委托给EventPublishingRunListener
对象,最终EventPublishingRunListener
对象将广播事件的职责委托给SimpleApplicationEventMulticaster
对象。为什么要层层委托这么做呢? 这个值得大家思考。
前面讲到从spring.factories
中加载出EventPublishingRunListener
类后会实例化,而实例化必然会通过EventPublishingRunListener
的构造函数来进行实例化,因此我们接下来分析下EventPublishingRunListener
的构造函数源码:
// EventPublishingRunListener.java
public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
// 新建一个事件广播器SimpleApplicationEventMulticaster对象
this.initialMulticaster = new SimpleApplicationEventMulticaster();
// 遍历在构造SpringApplication对象时从spring.factories配置文件中获取的事件监听器
for (ApplicationListener<?> listener : application.getListeners()) {
// 将从spring.factories配置文件中获取的事件监听器们添加到事件广播器initialMulticaster对象的相关集合中
this.initialMulticaster.addApplicationListener(listener);
}
}
可以看到在EventPublishingRunListener
的构造函数中有一个for
循环会遍历之前从spring.factories
中加载的监听器们,然后添加到集合中缓存起来,用于以后广播各种事件时直接从这个集合中取出来即可,而不用再去spring.factories
中加载,提高效率。
3.2 广播SpringBoot的内置生命周期事件
从spring.factories
配置文件中加载并实例化EventPublishingRunListener
对象后,那么在在SpringBoot的启动过程中会发射一系列SpringBoot内置的生命周期事件,我们再来回顾下SpringBoot启动过程中的源码:
// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
// 【0】新建一个SpringApplicationRunListeners对象用于发射SpringBoot启动过程中的生命周期事件
SpringApplicationRunListeners listeners = getRunListeners(args);
// 【1】》》》》》发射【ApplicationStartingEvent】事件,标志SpringApplication开始启动
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 【2】》》》》》发射【ApplicationEnvironmentPreparedEvent】事件,此时会去加载application.properties等配置文件的环境变量,同时也有标志环境变量已经准备好的意思
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 【3】》》》》》发射【ApplicationContextInitializedEvent】事件,标志context容器被创建且已准备好
// 【4】》》》》》发射【ApplicationPreparedEvent】事件,标志Context容器已经准备完成
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
// 【5】》》》》》发射【ApplicationStartedEvent】事件,标志spring容器已经刷新,此时所有的bean实例都已经加载完毕
listeners.started(context);
callRunners(context, applicationArguments);
}
// 【6】》》》》》发射【ApplicationFailedEvent】事件,标志SpringBoot启动失败
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 【7】》》》》》发射【ApplicationReadyEvent】事件,标志SpringApplication已经正在运行即已经成功启动,可以接收服务请求了。
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
可以看到在SpringBoot的启动过程中总共会发射7种不同类型的生命周期事件,来标志SpringBoot的不同启动阶段,同时,这些生命周期事件的监听器们也会执行一些启动过程中的初始化逻辑,关于这些监听器的初始化逻辑将在下一篇内容中会分析。以下是SpringBoot启动过程中要发射的事件类型,其中ApplicationFailedEvent
在SpringBoot启动过程中遇到异常才会发射:
ApplicationStartingEvent
ApplicationEnvironmentPreparedEvent
ApplicationContextInitializedEvent
ApplicationPreparedEvent
ApplicationStartedEvent
ApplicationFailedEvent
ApplicationReadyEvent
我们以listeners.starting();
这句代码为例,看看EventPublishingRunListener
对象发射事件的源码:
// SpringApplicationRunListeners.java
public void starting() {
// 遍历listeners集合,这里实质取出的就是刚才从spring.factories中取出的SPI实现类EventPublishingRunListener
// 而EventPublishingRunListener对象承担了SpringBoot启动过程中负责广播不同的生命周期事件
for (SpringApplicationRunListener listener : this.listeners) {
// 调用EventPublishingRunListener的starting方法来广播ApplicationStartingEvent事件
listener.starting();
}
}
继续跟进listener.starting();
的源码:
EventPublishingRunListener.java
// 》》》》》发射【ApplicationStartingEvent】事件
public void starting() {
// EventPublishingRunListener对象将发布ApplicationStartingEvent这件事情委托给了initialMulticaster对象
// 调用initialMulticaster的multicastEvent方法来发射ApplicationStartingEvent事件
this.initialMulticaster.multicastEvent(
new ApplicationStartingEvent(this.application, this.args));
}
可以看到,EventPublishingRunListener
对象将发布ApplicationStartingEvent
这件事情委托给了SimpleApplicationEventMulticaster
对象initialMulticaster
,
,而initialMulticaster
对象最终会调用其multicastEvent
方法来发射ApplicationStartingEvent
事件。关于SimpleApplicationEventMulticaster
类如何广播事件,笔者已经在Spring是如何实现事件监听机制的? Spring源码(二)这篇文章已经详细分析,这里不再赘述。
关于SpringBoot启动过程中发射其他生命周期事件的源码这里不再分析
4 SpringBoot的内置生命周期事件总结
好了,前面已经分析了SpringBoot启动过程中要发射的各种生命周期事件,下面列一个表格总结下:
5 小结
SpringBoot启动过程中广播生命周期事件的源码分析就到此结束了,下一篇会继续介绍监听这些生命周期事件的监听器们。我们再回顾本篇内容总结下关键点:
SpringBoot启动过程中会发射7种类型的生命周期事件,标志不同的启动阶段,然后相应的监听器会监听这些事件来执行一些初始化逻辑工作;
SpringBoot内置生命周期事件详解 SpringBoot源码(十)
SpringBoot中文注释项目Github地址:
https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE
本篇接 SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)
1 温故而知新
温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBoot启动时广播生命周期事件的原理,现将关键步骤再浓缩总结下:
- 为广播SpringBoot内置生命周期事件做前期准备:1)首先加载
ApplicationListener
监听器实现类;2)其次加载SPI扩展类EventPublishingRunListener
。 - SpringBoot启动时利用
EventPublishingRunListener
广播生命周期事件,然后ApplicationListener
监听器实现类监听相应的生命周期事件执行一些初始化逻辑的工作。
2 引言
上篇文章的侧重点是分析了SpringBoot启动时广播生命周期事件的原理,此篇文章我们再来详细分析SpringBoot内置的7种生命周期事件的源码。
3 SpringBoot生命周期事件源码分析
分析SpringBoot的生命周期事件,我们先来看一张类结构图:
由上图可以看到事件类之间的关系:
- 最顶级的父类是JDK的事件基类
EventObject
; - 然后Spring的事件基类
ApplicationEvent
继承了JDK的事件基类EventObject
; - 其次SpringBoot的生命周期事件基类
SpringApplicationEvent
继承了Spring的事件基类ApplicationEvent
; - 最后SpringBoot具体的7个生命周期事件类再继承了SpringBoot的生命周期事件基类
SpringApplicationEvent
。
3.1 JDK的事件基类EventObject
EventObject
类是JDK的事件基类,可以说是所有Java事件类的基本,即所有的Java事件类都直接或间接继承于该类,源码如下:
// EventObject.java
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
/**
* The object on which the Event initially occurred.
*/
protected transient Object source;
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @exception IllegalArgumentException if source is null.
*/
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
/**
* The object on which the Event initially occurred.
*
* @return The object on which the Event initially occurred.
*/
public Object getSource() {
return source;
}
/**
* Returns a String representation of this EventObject.
*
* @return A a String representation of this EventObject.
*/
public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}
可以看到EventObject
类只有一个属性source
,这个属性是用来记录最初事件是发生在哪个类,举个栗子,比如在SpringBoot启动过程中会发射ApplicationStartingEvent
事件,而这个事件最初是在SpringApplication
类中发射的,因此source
就是SpringApplication
对象。
3.2 Spring的事件基类ApplicationEvent
ApplicationEvent
继承了DK的事件基类EventObject
类,是Spring的事件基类,被所有Spring的具体事件类继承,源码如下:
// ApplicationEvent.java
/**
* Class to be extended by all application events. Abstract as it
* doesn't make sense for generic events to be published directly.
*
* @author Rod Johnson
* @author Juergen Hoeller
*/
public abstract class ApplicationEvent extends EventObject {
/** use serialVersionUID from Spring 1.2 for interoperability. */
private static final long serialVersionUID = 7099057708183571937L;
/** System time when the event happened. */
private final long timestamp;
/**
* Create a new ApplicationEvent.
* @param source the object on which the event initially occurred (never {@code null})
*/
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
/**
* Return the system time in milliseconds when the event happened.
*/
public final long getTimestamp() {
return this.timestamp;
}
}
可以看到ApplicationEvent
有且仅有一个属性timestamp
,该属性是用来记录事件发生的时间。
3.3 SpringBoot的事件基类SpringApplicationEvent
SpringApplicationEvent
类继承了Spring的事件基类ApplicationEvent
,是所有SpringBoot内置生命周期事件的父类,源码如下:
/**
* Base class for {@link ApplicationEvent} related to a {@link SpringApplication}.
*
* @author Phillip Webb
*/
@SuppressWarnings("serial")
public abstract class SpringApplicationEvent extends ApplicationEvent {
private final String[] args;
public SpringApplicationEvent(SpringApplication application, String[] args) {
super(application);
this.args = args;
}
public SpringApplication getSpringApplication() {
return (SpringApplication) getSource();
}
public final String[] getArgs() {
return this.args;
}
}
可以看到SpringApplicationEvent
有且仅有一个属性args
,该属性就是SpringBoot启动时的命令行参数即标注@SpringBootApplication
启动类中main
函数的参数。
3.4 SpringBoot具体的生命周期事件类
接下来我们再来看一下SpringBoot
内置生命周期事件即SpringApplicationEvent
的具体子类们。
3.4.1 ApplicationStartingEvent
// ApplicationStartingEvent.java
public class ApplicationStartingEvent extends SpringApplicationEvent {
public ApplicationStartingEvent(SpringApplication application, String[] args) {
super(application, args);
}
}
SpringBoot开始启动时便会发布ApplicationStartingEvent
事件,其发布时机在环境变量Environment或容器ApplicationContext创建前但在注册ApplicationListener
具体监听器之后,标志标志SpringApplication
开始启动。
3.4.2 ApplicationEnvironmentPreparedEvent
// ApplicationEnvironmentPreparedEvent.java
public class ApplicationEnvironmentPreparedEvent extends SpringApplicationEvent {
private final ConfigurableEnvironment environment;
/**
* Create a new {@link ApplicationEnvironmentPreparedEvent} instance.
* @param application the current application
* @param args the arguments the application is running with
* @param environment the environment that was just created
*/
public ApplicationEnvironmentPreparedEvent(SpringApplication application,
String[] args, ConfigurableEnvironment environment) {
super(application, args);
this.environment = environment;
}
/**
* Return the environment.
* @return the environment
*/
public ConfigurableEnvironment getEnvironment() {
return this.environment;
}
}
可以看到ApplicationEnvironmentPreparedEvent
事件多了一个environment
属性,我们不妨想一下,多了environment
属性的作用是啥?
答案就是ApplicationEnvironmentPreparedEvent
事件的environment
属性作用是利用事件发布订阅机制,相应监听器们可以从ApplicationEnvironmentPreparedEvent
事件中取出environment
变量,然后我们可以为environment
属性增加属性值或读出environment
变量中的值。
举个栗子:
ConfigFileApplicationListener
监听器就是监听了ApplicationEnvironmentPreparedEvent
事件,然后取出ApplicationEnvironmentPreparedEvent
事件的environment
属性,然后再为environment
属性增加application.properties
配置文件中的环境变量值。
当SpringApplication已经开始启动且环境变量Environment
已经创建后,并且为环境变量Environment
配置了命令行和Servlet
等类型的环境变量后,此时会发布ApplicationEnvironmentPreparedEvent
事件。
监听ApplicationEnvironmentPreparedEvent
事件的第一个监听器是ConfigFileApplicationListener
,因为是ConfigFileApplicationListener
监听器还要为环境变量Environment
增加application.properties
配置文件中的环境变量;此后还有一些也是监听ApplicationEnvironmentPreparedEvent
事件的其他监听器监听到此事件时,此时可以说环境变量Environment
几乎已经完全准备好了。
思考: 监听同一事件的监听器们执行监听逻辑时是有顺序的,我们可以想一下这个排序逻辑是什么时候排序的?还有为什么要这样排序呢?
3.4.3 ApplicationContextInitializedEvent
// ApplicationContextInitializedEvent.java
public class ApplicationContextInitializedEvent extends SpringApplicationEvent {
private final ConfigurableApplicationContext context;
/**
* Create a new {@link ApplicationContextInitializedEvent} instance.
* @param application the current application
* @param args the arguments the application is running with
* @param context the context that has been initialized
*/
public ApplicationContextInitializedEvent(SpringApplication application,
String[] args, ConfigurableApplicationContext context) {
super(application, args);
this.context = context;
}
/**
* Return the application context.
* @return the context
*/
public ConfigurableApplicationContext getApplicationContext() {
return this.context;
}
}
可以看到ApplicationContextInitializedEvent
事件多了个ConfigurableApplicationContext
类型的context
属性,context
属性的作用同样是为了相应监听器可以拿到这个context
属性执行一些逻辑,具体作用将在3.4.4
详述。
ApplicationContextInitializedEvent
事件在ApplicationContext
容器创建后,且为ApplicationContext
容器设置了environment
变量和执行了ApplicationContextInitializers
的初始化方法后但在bean定义加载前触发,标志ApplicationContext已经初始化完毕。
扩展: 可以看到
ApplicationContextInitializedEvent
是在为context
容器配置environment
变量后触发,此时ApplicationContextInitializedEvent
等事件只要有context
容器的话,那么其他需要environment
环境变量的监听器只需要从context
中取出environment
变量即可,从而ApplicationContextInitializedEvent
等事件没必要再配置environment
属性。
3.4.4 ApplicationPreparedEvent
// ApplicationPreparedEvent.java
public class ApplicationPreparedEvent extends SpringApplicationEvent {
private final ConfigurableApplicationContext context;
/**
* Create a new {@link ApplicationPreparedEvent} instance.
* @param application the current application
* @param args the arguments the application is running with
* @param context the ApplicationContext about to be refreshed
*/
public ApplicationPreparedEvent(SpringApplication application, String[] args,
ConfigurableApplicationContext context) {
super(application, args);
this.context = context;
}
/**
* Return the application context.
* @return the context
*/
public ConfigurableApplicationContext getApplicationContext() {
return this.context;
}
}
同样可以看到ApplicationPreparedEvent
事件多了个ConfigurableApplicationContext
类型的context
属性,多了context
属性的作用是能让监听该事件的监听器们能拿到context
属性,监听器拿到context
属性一般有如下作用:
- 从事件中取出
context
属性,然后可以增加一些后置处理器,比如ConfigFileApplicationListener
监听器监听到ApplicationPreparedEvent
事件后,然后取出context
变量,通过context
变量增加了PropertySourceOrderingPostProcessor
这个后置处理器; - 通过
context
属性取出beanFactory
容器,然后注册一些bean
,比如LoggingApplicationListener
监听器通过ApplicationPreparedEvent
事件的context
属性取出beanFactory
容器,然后注册了springBootLoggingSystem
这个单例bean
; - 通过
context
属性取出Environment
环境变量,然后就可以操作环境变量,比如PropertiesMigrationListener
。
ApplicationPreparedEvent
事件在ApplicationContext
容器已经完全准备好时但在容器刷新前触发,在这个阶段bean
定义已经加载完毕还有environment
已经准备好可以用了。
3.4.5 ApplicationStartedEvent
// ApplicationStartedEvent.java
public class ApplicationStartedEvent extends SpringApplicationEvent {
private final ConfigurableApplicationContext context;
/**
* Create a new {@link ApplicationStartedEvent} instance.
* @param application the current application
* @param args the arguments the application is running with
* @param context the context that was being created
*/
public ApplicationStartedEvent(SpringApplication application, String[] args,
ConfigurableApplicationContext context) {
super(application, args);
this.context = context;
}
/**
* Return the application context.
* @return the context
*/
public ConfigurableApplicationContext getApplicationContext() {
return this.context;
}
}
ApplicationStartedEvent
事件将在容器刷新后但ApplicationRunner
和CommandLineRunner
的run
方法执行前触发,标志Spring
容器已经刷新,此时容器已经准备完毕了。
扩展: 这里提到了
ApplicationRunner
和CommandLineRunner
接口有啥作用呢?我们一般会在Spring
容器刷新完毕后,此时可能有一些系统参数等静态数据需要加载,此时我们就可以实现了ApplicationRunner
或CommandLineRunner
接口来实现静态数据的加载。
3.4.6 ApplicationReadyEvent
// ApplicationReadyEvent.java
public class ApplicationReadyEvent extends SpringApplicationEvent {
private final ConfigurableApplicationContext context;
/**
* Create a new {@link ApplicationReadyEvent} instance.
* @param application the current application
* @param args the arguments the application is running with
* @param context the context that was being created
*/
public ApplicationReadyEvent(SpringApplication application, String[] args,
ConfigurableApplicationContext context) {
super(application, args);
this.context = context;
}
/**
* Return the application context.
* @return the context
*/
public ConfigurableApplicationContext getApplicationContext() {
return this.context;
}
}
ApplicationReadyEvent
事件在调用完ApplicationRunner
和CommandLineRunner
的run
方法后触发,此时标志SpringApplication
已经正在运行。
3.4.7 ApplicationFailedEvent
// ApplicationFailedEvent.java
public class ApplicationFailedEvent extends SpringApplicationEvent {
private final ConfigurableApplicationContext context;
private final Throwable exception;
/**
* Create a new {@link ApplicationFailedEvent} instance.
* @param application the current application
* @param args the arguments the application was running with
* @param context the context that was being created (maybe null)
* @param exception the exception that caused the error
*/
public ApplicationFailedEvent(SpringApplication application, String[] args,
ConfigurableApplicationContext context, Throwable exception) {
super(application, args);
this.context = context;
this.exception = exception;
}
/**
* Return the application context.
* @return the context
*/
public ConfigurableApplicationContext getApplicationContext() {
return this.context;
}
/**
* Return the exception that caused the failure.
* @return the exception
*/
public Throwable getException() {
return this.exception;
}
}
可以看到ApplicationFailedEvent
事件除了多了一个context
属性外,还多了一个Throwable
类型的exception
属性用来记录SpringBoot启动失败时的异常。
ApplicationFailedEvent
事件在SpringBoot启动失败时触发,标志SpringBoot启动失败。
4 小结
此篇文章相对简单,对SpringBoot内置的7种生命周期事件进行了详细分析。我们还是引用上篇文章的一张图来回顾一下这些生命周期事件及其用途:
5 写在最后
由于有一些小伙伴们建议之前有些源码分析文章太长,导致耐心不够,看不下去,因此,之后的源码分析文章如果太长的话,笔者将会考虑拆分为几篇文章,这样就比较短小了,比较容易看完,嘿嘿。
【源码笔记】Github地址:
https://github.com/yuanmabiji/Java-SourceCode-Blogs
Java是如何实现Future模式的?万字详解!
JDK1.8源码分析项目(中文注释)Github地址:
https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs
1 Future是什么?
先举个例子,我们平时网购买东西,下单后会生成一个订单号,然后商家会根据这个订单号发货,发货后又有一个快递单号,然后快递公司就会根据这个快递单号将网购东西快递给我们。在这一过程中,这一系列的单号都是我们收货的重要凭证。
因此,JDK的Future就类似于我们网购买东西的单号,当我们执行某一耗时的任务时,我们可以另起一个线程异步去执行这个耗时的任务,同时我们可以干点其他事情。当事情干完后我们再根据future这个"单号"去提取耗时任务的执行结果即可。因此Future也是多线程中的一种应用模式。
扩展: 说起多线程,那么Future又与Thread有什么区别呢?最重要的区别就是Thread是没有返回结果的,而Future模式是有返回结果的。
2 如何使用Future
前面搞明白了什么是Future,下面我们再来举个简单的例子看看如何使用Future。
假如现在我们要打火锅,首先我们要准备两样东西:把水烧开和准备食材。因为烧开水是一个比较漫长的过程(相当于耗时的业务逻辑),因此我们可以一边烧开水(相当于另起一个线程),一边准备火锅食材(主线程),等两者都准备好了我们就可以开始打火锅了。
// DaHuoGuo.java
public class DaHuoGuo {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + ":" + "开始烧开水...");
// 模拟烧开水耗时
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":" + "开水已经烧好了...");
return "开水";
}
});
Thread thread = new Thread(futureTask);
thread.start();
// do other thing
System.out.println(Thread.currentThread().getName() + ":" + " 此时开启了一个线程执行future的逻辑(烧开水),此时我们可以干点别的事情(比如准备火锅食材)...");
// 模拟准备火锅食材耗时
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + ":" + "火锅食材准备好了");
String shicai = "火锅食材";
// 开水已经稍好,我们取得烧好的开水
String boilWater = futureTask.get();
System.out.println(Thread.currentThread().getName() + ":" + boilWater + "和" + shicai + "已经准备好,我们可以开始打火锅啦");
}
}
执行结果如下截图,符合我们的预期:
从以上代码中可以看到,我们使用Future主要有以下步骤:
- 新建一个
Callable
匿名函数实现类对象,我们的业务逻辑在Callable
的call
方法中实现,其中Callable的泛型是返回结果类型; - 然后把
Callable
匿名函数对象作为FutureTask
的构造参数传入,构建一个futureTask
对象; - 然后再把
futureTask
对象作为Thread
构造参数传入并开启这个线程执行去执行业务逻辑; - 最后我们调用
futureTask
对象的get
方法得到业务逻辑执行结果。
可以看到跟Future使用有关的JDK类主要有FutureTask
和Callable
两个,下面主要对FutureTask
进行源码分析。
扩展: 还有一种使用
Future
的方式是将Callable
实现类提交给线程池执行的方式,这里不再介绍,自行百度即可。
3 FutureTask类结构分析
我们先来看下FutureTask
的类结构:
可以看到FutureTask
实现了RunnableFuture
接口,而RunnableFuture
接口又继承了Future
和Runnable
接口。因为FutureTask
间接实现了Runnable
接口,因此可以作为任务被线程Thread
执行;此外,最重要的一点就是FutureTask
还间接实现了Future
接口,因此还可以获得任务执行的结果。下面我们就来简单看看这几个接口的相关api
。
// Runnable.java
@FunctionalInterface
public interface Runnable {
// 执行线程任务
public abstract void run();
}
Runnable
没啥好说的,相信大家都已经很熟悉了。
// Future.java
public interface Future<V> {
/**
* 尝试取消线程任务的执行,分为以下几种情况:
* 1)如果线程任务已经完成或已经被取消或其他原因不能被取消,此时会失败并返回false;
* 2)如果任务还未开始执行,此时执行cancel方法,那么任务将被取消执行,此时返回true;TODO 此时对应任务状态state的哪种状态???不懂!!
* 3)如果任务已经开始执行,那么mayInterruptIfRunning这个参数将决定是否取消任务的执行。
* 这里值得注意的是,cancel(true)实质并不能真正取消线程任务的执行,而是发出一个线程
* 中断的信号,一般需要结合Thread.currentThread().isInterrupted()来使用。
*/
boolean cancel(boolean mayInterruptIfRunning);
/**
* 判断任务是否被取消,在执行任务完成前被取消,此时会返回true
*/
boolean isCancelled();
/**
* 这个方法不管任务正常停止,异常还是任务被取消,总是返回true。
*/
boolean isDone();
/**
* 获取任务执行结果,注意是阻塞等待获取任务执行结果。
*/
V get() throws InterruptedException, ExecutionException;
/**
* 获取任务执行结果,注意是阻塞等待获取任务执行结果。
* 只不过在规定的时间内未获取到结果,此时会抛出超时异常
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future
接口象征着异步执行任务的结果即执行一个耗时任务完全可以另起一个线程执行,然后此时我们可以去做其他事情,做完其他事情我们再调用Future.get()
方法获取结果即可,此时若异步任务还没结束,此时会一直阻塞等待,直到异步任务执行完获取到结果。
// RunnableFuture.java
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
RunnableFuture
是Future
和Runnable
接口的组合,即这个接口表示又可以被线程异步执行,因为实现了Runnable
接口,又可以获得线程异步任务的执行结果,因为实现了Future
接口。因此解决了Runnable
异步任务没有返回结果的缺陷。
接下来我们来看下FutureTask
,FutureTask
实现了RunnableFuture
接口,因此是Future
和Runnable
接口的具体实现类,是一个可被取消的异步线程任务,提供了Future
的基本实现,即异步任务执行后我们能够获取到异步任务的执行结果,是我们接下来分析的重中之重。FutureTask
可以包装一个Callable
和Runnable
对象,此外,FutureTask
除了可以被线程执行外,还可以被提交给线程池执行。
我们先看下FutureTask
类的api
,其中重点方法已经红框框出。
上图中FutureTask
的run
方法是被线程异步执行的方法,get
方法即是取得异步任务执行结果的方法,还有cancel
方法是取消任务执行的方法。接下来我们主要对这三个方法进行重点分析。
思考:
FutureTask
覆写的run
方法的返回类型依然是void
,表示没有返回值,那么FutureTask
的get
方法又是如何获得返回值的呢?FutureTask
的cancel
方法能真正取消线程异步任务的执行么?什么情况下能取消?
因为FutureTask
异步任务执行结果还跟Callable
接口有关,因此我们再来看下Callable
接口:
// Callable.java
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*/
V call() throws Exception;
}
我们都知道,Callable<V>
接口和Runnable
接口都可以被提交给线程池执行,唯一不同的就是Callable<V>
接口是有返回结果的,其中的泛型V
就是返回结果,而Runnable
接口是没有返回结果的。
思考: 一般情况下,
Runnable
接口实现类才能被提交给线程池执行,为何Callable
接口实现类也可以被提交给线程池执行?想想线程池的submit
方法内部有对Callable
做适配么?
4 FutureTask源码分析
4.1 FutureTask成员变量
我们首先来看下FutureTask
的成员变量有哪些,理解这些成员变量对后面的源码分析非常重要。
// FutureTask.java
/** 封装的Callable对象,其call方法用来执行异步任务 */
private Callable<V> callable;
/** 在FutureTask里面定义一个成员变量outcome,用来装异步任务的执行结果 */
private Object outcome; // non-volatile, protected by state reads/writes
/** 用来执行callable任务的线程 */
private volatile Thread runner;
/** 线程等待节点,reiber stack的一种实现 */
private volatile WaitNode waiters;
/** 任务执行状态 */
private volatile int state;
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 对应成员变量state的偏移地址
private static final long stateOffset;
// 对应成员变量runner的偏移地址
private static final long runnerOffset;
// 对应成员变量waiters的偏移地址
private static final long waitersOffset;
这里我们要重点关注下FutureTask
的Callable
成员变量,因为FutureTask
的异步任务最终是委托给Callable
去实现的。
思考:
FutureTask
的成员变量runner
,waiters
和state
都被volatile
修饰,我们可以思考下为什么这三个成员变量需要被volatile
修饰,而其他成员变量又不用呢?volatile
关键字的作用又是什么呢?- 既然已经定义了成员变量
runner
,waiters
和state
了,此时又定义了stateOffset
,runnerOffset
和waitersOffset
变量分别对应runner
,waiters
和state
的偏移地址,为何要多此一举呢?
我们再来看看stateOffset
,runnerOffset
和waitersOffset
变量这三个变量的初始化过程:
// FutureTask.java
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = FutureTask.class;
stateOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("state"));
runnerOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("runner"));
waitersOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("waiters"));
} catch (Exception e) {
throw new Error(e);
}
}
4.2 FutureTask的状态变化
前面讲了FutureTask
的成员变量,有一个表示状态的成员变量state
我们要重点关注下,state
变量表示任务执行的状态。
// FutureTask.java
/** 任务执行状态 */
private volatile int state;
/** 任务新建状态 */
private static final int NEW = 0;
/** 任务正在完成状态,是一个瞬间过渡状态 */
private static final int COMPLETING = 1;
/** 任务正常结束状态 */
private static final int NORMAL = 2;
/** 任务执行异常状态 */
private static final int EXCEPTIONAL = 3;
/** 任务被取消状态,对应cancel(false) */
private static final int CANCELLED = 4;
/** 任务中断状态,是一个瞬间过渡状态 */
private static final int INTERRUPTING = 5;
/** 任务被中断状态,对应cancel(true) */
private static final int INTERRUPTED = 6;
可以看到任务状态变量state
有以上7种状态,0-6分别对应着每一种状态。任务状态一开始是NEW
,然后由FutureTask
的三个方法set
,setException
和cancel
来设置状态的变化,其中状态变化有以下四种情况:
NEW -> COMPLETING -> NORMAL
:这个状态变化表示异步任务的正常结束,其中COMPLETING
是一个瞬间临时的过渡状态,由set
方法设置状态的变化;NEW -> COMPLETING -> EXCEPTIONAL
:这个状态变化表示异步任务执行过程中抛出异常,由setException
方法设置状态的变化;NEW -> CANCELLED
:这个状态变化表示被取消,即调用了cancel(false)
,由cancel
方法来设置状态变化;NEW -> INTERRUPTING -> INTERRUPTED
:这个状态变化表示被中断,即调用了cancel(true)
,由cancel
方法来设置状态变化。
4.3 FutureTask构造函数
FutureTask
有两个构造函数,我们分别来看看:
// FutureTask.java
// 第一个构造函数
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
可以看到,这个构造函数在我们前面举的“打火锅”的例子代码中有用到,就是Callable
成员变量赋值,在异步执行任务时再调用Callable.call
方法执行异步任务逻辑。此外,此时给任务状态state
赋值为NEW
,表示任务新建状态。
我们再来看下FutureTask
的另外一个构造函数:
// FutureTask.java
// 另一个构造函数
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
这个构造函数在执行Executors.callable(runnable, result)
时是通过适配器RunnableAdapter
来将Runnable
对象runnable
转换成Callable
对象,然后再分别给callable
和state
变量赋值。
注意,这里我们需要记住的是FutureTask
新建时,此时的任务状态state
是NEW
就好了。
4.4 FutureTask.run方法,用来执行异步任务
前面我们有讲到FutureTask
间接实现了Runnable
接口,覆写了Runnable
接口的run
方法,因此该覆写的run
方法是提交给线程来执行的,同时,该run
方法正是执行异步任务逻辑的方法,那么,执行完run
方法又是如何保存异步任务执行的结果的呢?
我们现在着重来分析下run
方法:
// FutureTask.java
public void run() {
// 【1】,为了防止多线程并发执行异步任务,这里需要判断线程满不满足执行异步任务的条件,有以下三种情况:
// 1)若任务状态state为NEW且runner为null,说明还未有线程执行过异步任务,此时满足执行异步任务的条件,
// 此时同时调用CAS方法为成员变量runner设置当前线程的值;
// 2)若任务状态state为NEW且runner不为null,任务状态虽为NEW但runner不为null,说明有线程正在执行异步任务,
// 此时不满足执行异步任务的条件,直接返回;
// 1)若任务状态state不为NEW,此时不管runner是否为null,说明已经有线程执行过异步任务,此时没必要再重新
// 执行一次异步任务,此时不满足执行异步任务的条件;
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
// 拿到之前构造函数传进来的callable实现类对象,其call方法封装了异步任务执行的逻辑
Callable<V> c = callable;
// 若任务还是新建状态的话,那么就调用异步任务
if (c != null && state == NEW) {
// 异步任务执行结果
V result;
// 异步任务执行成功还是始遍标志
boolean ran;
try {
// 【2】,执行异步任务逻辑,并把执行结果赋值给result
result = c.call();
// 若异步任务执行过程中没有抛出异常,说明异步任务执行成功,此时设置ran标志为true
ran = true;
} catch (Throwable ex) {
result = null;
// 异步任务执行过程抛出异常,此时设置ran标志为false
ran = false;
// 【3】设置异常,里面也设置state状态的变化
setException(ex);
}
// 【3】若异步任务执行成功,此时设置异步任务执行结果,同时也设置状态的变化
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
// 异步任务正在执行过程中,runner一直是非空的,防止并发调用run方法,前面有调用cas方法做判断的
// 在异步任务执行完后,不管是正常结束还是异常结束,此时设置runner为null
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
// 线程执行异步任务后的任务状态
int s = state;
// 【4】如果执行了cancel(true)方法,此时满足条件,
// 此时调用handlePossibleCancellationInterrupt方法处理中断
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
可以看到执行异步任务的run
方法主要分为以下四步来执行:
- 判断线程是否满足执行异步任务的条件:为了防止多线程并发执行异步任务,这里需要判断线程满不满足执行异步任务的条件;
- 若满足条件,执行异步任务:因为异步任务逻辑封装在
Callable.call
方法中,此时直接调用Callable.call
方法执行异步任务,然后返回执行结果; - 根据异步任务的执行情况做不同的处理:1) 若异步任务执行正常结束,此时调用
set(result);
来设置任务执行结果;2)若异步任务执行抛出异常,此时调用setException(ex);
来设置异常,详细分析请见4.4.1小节
; - 异步任务执行完后的善后处理工作:不管异步任务执行成功还是失败,若其他线程有调用
FutureTask.cancel(true)
,此时需要调用handlePossibleCancellationInterrupt
方法处理中断,详细分析请见4.4.2小节
。
这里值得注意的是判断线程满不满足执行异步任务条件时,runner
是否为null
是调用UNSAFE
的CAS
方法compareAndSwapObject
来判断和设置的,同时compareAndSwapObject
是通过成员变量runner
的偏移地址runnerOffset
来给runner
赋值的,此外,成员变量runner
被修饰为volatile
是在多线程的情况下, 一个线程的volatile
修饰变量的设值能够立即刷进主存,因此值便可被其他线程可见。
4.4.1 FutureTask的set和setException方法
下面我们来看下当异步任务执行正常结束时,此时会调用set(result);
方法:
// FutureTask.java
protected void set(V v) {
// 【1】调用UNSAFE的CAS方法判断任务当前状态是否为NEW,若为NEW,则设置任务状态为COMPLETING
// 【思考】此时任务不能被多线程并发执行,什么情况下会导致任务状态不为NEW?
// 答案是只有在调用了cancel方法的时候,此时任务状态不为NEW,此时什么都不需要做,
// 因此需要调用CAS方法来做判断任务状态是否为NEW
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 【2】将任务执行结果赋值给成员变量outcome
outcome = v;
// 【3】将任务状态设置为NORMAL,表示任务正常结束
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
// 【4】调用任务执行完成方法,此时会唤醒阻塞的线程,调用done()方法和清空等待线程链表等
finishCompletion();
}
}
可以看到当异步任务正常执行结束后,且异步任务没有被cancel
的情况下,此时会做以下事情:将任务执行结果保存到FutureTask
的成员变量outcome
中的,赋值结束后会调用finishCompletion
方法来唤醒阻塞的线程(哪里来的阻塞线程?后面会分析),值得注意的是这里对应的任务状态变化是NEW -> COMPLETING -> NORMAL。
我们继续来看下当异步任务执行过程中抛出异常,此时会调用setException(ex);
方法。
// FutureTask.java
protected void setException(Throwable t) {
// 【1】调用UNSAFE的CAS方法判断任务当前状态是否为NEW,若为NEW,则设置任务状态为COMPLETING
// 【思考】此时任务不能被多线程并发执行,什么情况下会导致任务状态不为NEW?
// 答案是只有在调用了cancel方法的时候,此时任务状态不为NEW,此时什么都不需要做,
// 因此需要调用CAS方法来做判断任务状态是否为NEW
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 【2】将异常赋值给成员变量outcome
outcome = t;
// 【3】将任务状态设置为EXCEPTIONAL
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
// 【4】调用任务执行完成方法,此时会唤醒阻塞的线程,调用done()方法和清空等待线程链表等
finishCompletion();
}
}
可以看到setException(Throwable t)
的代码逻辑跟前面的set(V v)
几乎一样,不同的是任务执行过程中抛出异常,此时是将异常保存到FutureTask
的成员变量outcome
中,还有,值得注意的是这里对应的任务状态变化是NEW -> COMPLETING -> EXCEPTIONAL。
因为异步任务不管正常还是异常结束,此时都会调用FutureTask
的finishCompletion
方法来唤醒唤醒阻塞的线程,这里阻塞的线程是指我们调用Future.get
方法时若异步任务还未执行完,此时该线程会阻塞。
// FutureTask.java
private void finishCompletion() {
// assert state > COMPLETING;
// 取出等待线程链表头节点,判断头节点是否为null
// 1)若线程链表头节点不为空,此时以“后进先出”的顺序(栈)移除等待的线程WaitNode节点
// 2)若线程链表头节点为空,说明还没有线程调用Future.get()方法来获取任务执行结果,固然不用移除
for (WaitNode q; (q = waiters) != null;) {
// 调用UNSAFE的CAS方法将成员变量waiters设置为空
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
// 取出WaitNode节点的线程
Thread t = q.thread;
// 若取出的线程不为null,则将该WaitNode节点线程置空,且唤醒正在阻塞的该线程
if (t != null) {
q.thread = null;
//【重要】唤醒正在阻塞的该线程
LockSupport.unpark(t);
}
// 继续取得下一个WaitNode线程节点
WaitNode next = q.next;
// 若没有下一个WaitNode线程节点,说明已经将所有等待的线程唤醒,此时跳出for循环
if (next == null)
break;
// 将已经移除的线程WaitNode节点的next指针置空,此时好被垃圾回收
q.next = null; // unlink to help gc
// 再把下一个WaitNode线程节点置为当前线程WaitNode头节点
q = next;
}
break;
}
}
// 不管任务正常执行还是抛出异常,都会调用done方法
done();
// 因为异步任务已经执行完且结果已经保存到outcome中,因此此时可以将callable对象置空了
callable = null; // to reduce footprint
}
finishCompletion
方法的作用就是不管异步任务正常还是异常结束,此时都要唤醒且移除线程等待链表的等待线程节点,这个链表实现的是一个是Treiber stack
,因此唤醒(移除)的顺序是"后进先出"即后面先来的线程先被先唤醒(移除),关于这个线程等待链表是如何成链的,后面再继续分析。
4.4.2 FutureTask的handlePossibleCancellationInterrupt方法
在4.4小节
分析的run
方法里的最后有一个finally
块,此时若任务状态state >= INTERRUPTING
,此时说明有其他线程执行了cancel(true)
方法,此时需要让出CPU
执行的时间片段给其他线程执行,我们来看下具体的源码:
// FutureTask.java
private void handlePossibleCancellationInterrupt(int s) {
// It is possible for our interrupter to stall before getting a
// chance to interrupt us. Let's spin-wait patiently.
// 当任务状态是INTERRUPTING时,此时让出CPU执行的机会,让其他线程执行
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
// assert state == INTERRUPTED;
// We want to clear any interrupt we may have received from
// cancel(true). However, it is permissible to use interrupts
// as an independent mechanism for a task to communicate with
// its caller, and there is no way to clear only the
// cancellation interrupt.
//
// Thread.interrupted();
}
思考: 为啥任务状态是
INTERRUPTING
时,此时就要让出CPU执行的时间片段呢?还有为什么要在义务任务执行后才调用handlePossibleCancellationInterrupt
方法呢?
4.5 FutureTask.get方法,获取任务执行结果
前面我们起一个线程在其`run`方法中执行异步任务后,此时我们可以调用`FutureTask.get`方法来获取异步任务执行的结果。
// FutureTask.java
public V get() throws InterruptedException, ExecutionException {
int s = state;
// 【1】若任务状态<=COMPLETING,说明任务正在执行过程中,此时可能正常结束,也可能遇到异常
if (s <= COMPLETING)
s = awaitDone(false, 0L);
// 【2】最后根据任务状态来返回任务执行结果,此时有三种情况:1)任务正常执行;2)任务执行异常;3)任务被取消
return report(s);
}
可以看到,如果任务状态state<=COMPLETING
,说明异步任务正在执行过程中,此时会调用awaitDone
方法阻塞等待;当任务执行完后,此时再调用report
方法来报告任务结果,此时有三种情况:1)任务正常执行;2)任务执行异常;3)任务被取消。
4.5.1 FutureTask.awaitDone方法
FutureTask.awaitDone
方法会阻塞获取异步任务执行结果的当前线程,直到异步任务执行完成。
// FutureTask.java
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 计算超时结束时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 线程链表头节点
WaitNode q = null;
// 是否入队
boolean queued = false;
// 死循环
for (;;) {
// 如果当前获取任务执行结果的线程被中断,此时移除该线程WaitNode链表节点,并抛出InterruptedException
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
// 【5】如果任务状态>COMPLETING,此时返回任务执行结果,其中此时任务可能正常结束(NORMAL),可能抛出异常(EXCEPTIONAL)
// 或任务被取消(CANCELLED,INTERRUPTING或INTERRUPTED状态的一种)
if (s > COMPLETING) {
// 【问】此时将当前WaitNode节点的线程置空,其中在任务结束时也会调用finishCompletion将WaitNode节点的thread置空,
// 这里为什么又要再调用一次q.thread = null;呢?
// 【答】因为若很多线程来获取任务执行结果,在任务执行完的那一刻,此时获取任务的线程要么已经在线程等待链表中,要么
// 此时还是一个孤立的WaitNode节点。在线程等待链表中的的所有WaitNode节点将由finishCompletion来移除(同时唤醒)所有
// 等待的WaitNode节点,以便垃圾回收;而孤立的线程WaitNode节点此时还未阻塞,因此不需要被唤醒,此时只要把其属性置为
// null,然后其有没有被谁引用,因此可以被GC。
if (q != null)
q.thread = null;
// 【重要】返回任务执行结果
return s;
}
// 【4】若任务状态为COMPLETING,此时说明任务正在执行过程中,此时获取任务结果的线程需让出CPU执行时间片段
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
// 【1】若当前线程还没有进入线程等待链表的WaitNode节点,此时新建一个WaitNode节点,并把当前线程赋值给WaitNode节点的thread属性
else if (q == null)
q = new WaitNode();
// 【2】若当前线程等待节点还未入线程等待队列,此时加入到该线程等待队列的头部
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
// 若有超时设置,那么处理超时获取任务结果的逻辑
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
// 【3】若没有超时设置,此时直接阻塞当前线程
else
LockSupport.park(this);
}
}
FutureTask.awaitDone
方法主要做的事情总结如下:
- 首先
awaitDone
方法里面是一个死循环; - 若获取结果的当前线程被其他线程中断,此时移除该线程WaitNode链表节点,并抛出InterruptedException;
- 如果任务状态
state>COMPLETING
,此时返回任务执行结果; - 若任务状态为
COMPLETING
,此时获取任务结果的线程需让出CPU执行时间片段; - 若
q == null
,说明当前线程还未设置到WaitNode
节点,此时新建WaitNode
节点并设置其thread
属性为当前线程; - 若
queued==false
,说明当前线程WaitNode
节点还未加入线程等待链表,此时加入该链表的头部; - 当
timed
设置为true时,此时该方法具有超时功能,关于超时的逻辑这里不详细分析; - 当前面6个条件都不满足时,此时阻塞当前线程。
我们分析到这里,可以直到执行异步任务只能有一个线程来执行,而获取异步任务结果可以多线程来获取,当异步任务还未执行完时,此时获取异步任务结果的线程会加入线程等待链表中,然后调用调用LockSupport.park(this);
方法阻塞当前线程。直到异步任务执行完成,此时会调用finishCompletion
方法来唤醒并移除线程等待链表的每个WaitNode
节点,这里这里唤醒(移除)WaitNode
节点的线程是从链表头部开始的,前面我们也已经分析过。
还有一个特别需要注意的就是awaitDone
方法里面是一个死循环,当一个获取异步任务的线程进来后可能会多次进入多个条件分支执行不同的业务逻辑,也可能只进入一个条件分支。下面分别举两种可能的情况进行说明:
情况1:
当获取异步任务结果的线程进来时,此时异步任务还未执行完即state=NEW
且没有超时设置时:
- 第一次循环:此时
q = null
,此时进入上面代码标号【1】
的判断分支,即为当前线程新建一个WaitNode
节点; - 第二次循环:此时
queued = false
,此时进入上面代码标号【2】
的判断分支,即将之前新建的WaitNode
节点加入线程等待链表中; - 第三次循环:此时进入上面代码标号
【3】
的判断分支,即阻塞当前线程; - 第四次循环:加入此时异步任务已经执行完,此时进入上面代码标号
【5】
的判断分支,即返回异步任务执行结果。
情况2:
当获取异步任务结果的线程进来时,此时异步任务已经执行完即state>COMPLETING
且没有超时设置时,此时直接进入上面代码标号【5】
的判断分支,即直接返回异步任务执行结果即可,也不用加入线程等待链表了。
4.5.2 FutureTask.report方法
在get
方法中,当异步任务执行结束后即不管异步任务正常还是异常结束,亦或是被cancel
,此时获取异步任务结果的线程都会被唤醒,因此会继续执行FutureTask.report
方法报告异步任务的执行情况,此时可能会返回结果,也可能会抛出异常。
// FutureTask.java
private V report(int s) throws ExecutionException {
// 将异步任务执行结果赋值给x,此时FutureTask的成员变量outcome要么保存着
// 异步任务正常执行的结果,要么保存着异步任务执行过程中抛出的异常
Object x = outcome;
// 【1】若异步任务正常执行结束,此时返回异步任务执行结果即可
if (s == NORMAL)
return (V)x;
// 【2】若异步任务执行过程中,其他线程执行过cancel方法,此时抛出CancellationException异常
if (s >= CANCELLED)
throw new CancellationException();
// 【3】若异步任务执行过程中,抛出异常,此时将该异常转换成ExecutionException后,重新抛出。
throw new ExecutionException((Throwable)x);
}
4.6 FutureTask.cancel方法,取消执行任务
我们最后再来看下FutureTask.cancel
方法,我们一看到FutureTask.cancel
方法,肯定一开始就天真的认为这是一个可以取消异步任务执行的方法,如果我们这样认为的话,只能说我们猜对了一半。
// FutureTask.java
public boolean cancel(boolean mayInterruptIfRunning) {
// 【1】判断当前任务状态,若state == NEW时根据mayInterruptIfRunning参数值给当前任务状态赋值为INTERRUPTING或CANCELLED
// a)当任务状态不为NEW时,说明异步任务已经完成,或抛出异常,或已经被取消,此时直接返回false。
// TODO 【问题】此时若state = COMPLETING呢?此时为何也直接返回false,而不能发出中断异步任务线程的中断信号呢??
// TODO 仅仅因为COMPLETING是一个瞬时态吗???
// b)当前仅当任务状态为NEW时,此时若mayInterruptIfRunning为true,此时任务状态赋值为INTERRUPTING;否则赋值为CANCELLED。
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
// 【2】如果mayInterruptIfRunning为true,此时中断执行异步任务的线程runner(还记得执行异步任务时就把执行异步任务的线程就赋值给了runner成员变量吗)
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
// 中断执行异步任务的线程runner
t.interrupt();
} finally { // final state
// 最后任务状态赋值为INTERRUPTED
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
// 【3】不管mayInterruptIfRunning为true还是false,此时都要调用finishCompletion方法唤醒阻塞的获取异步任务结果的线程并移除线程等待链表节点
} finally {
finishCompletion();
}
// 返回true
return true;
}
以上代码中,当异步任务状态state != NEW
时,说明异步任务已经正常执行完或已经异常结束亦或已经被cancel
,此时直接返回false
;当异步任务状态state = NEW
时,此时又根据mayInterruptIfRunning
参数是否为true
分为以下两种情况:
- 当
mayInterruptIfRunning = false
时,此时任务状态state
直接被赋值为CANCELLED
,此时不会对执行异步任务的线程发出中断信号,值得注意的是这里对应的任务状态变化是NEW -> CANCELLED。 - 当
mayInterruptIfRunning = true
时,此时会对执行异步任务的线程发出中断信号,值得注意的是这里对应的任务状态变化是NEW -> INTERRUPTING -> INTERRUPTED。
最后不管mayInterruptIfRunning
为true
还是false
,此时都要调用finishCompletion
方法唤醒阻塞的获取异步任务结果的线程并移除线程等待链表节点。
从FutureTask.cancel
源码中我们可以得出答案,该方法并不能真正中断正在执行异步任务的线程,只能对执行异步任务的线程发出中断信号。如果执行异步任务的线程处于sleep
、wait
或join
的状态中,此时会抛出InterruptedException
异常,该线程可以被中断;此外,如果异步任务需要在while
循环执行的话,此时可以结合以下代码来结束异步任务线程,即执行异步任务的线程被中断时,此时Thread.currentThread().isInterrupted()
返回true
,不满足while
循环条件因此退出循环,结束异步任务执行线程,如下代码:
public Integer call() throws Exception {
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑代码
System.out.println("running...");
}
return 666;
}
注意:调用了FutureTask.cancel
方法,只要返回结果是true
,假如异步任务线程虽然不能被中断,即使异步任务线程正常执行完毕,返回了执行结果,此时调用FutureTask.get
方法也不能够获取异步任务执行结果,此时会抛出CancellationException
异常。请问知道这是为什么吗?
因为调用了FutureTask.cancel
方法,只要返回结果是true
,此时的任务状态为CANCELLED
或INTERRUPTED
,同时必然会执行finishCompletion
方法,而finishCompletion
方法会唤醒获取异步任务结果的线程等待列表的线程,而获取异步任务结果的线程唤醒后发现状态s >= CANCELLED
,此时就会抛出CancellationException
异常了。
5 总结
好了,本篇文章对FutureTask
的源码分析就到此结束了,下面我们再总结下FutureTask
的实现逻辑:
- 我们实现
Callable
接口,在覆写的call
方法中定义需要执行的业务逻辑; - 然后把我们实现的
Callable
接口实现对象传给FutureTask
,然后FutureTask
作为异步任务提交给线程执行; - 最重要的是
FutureTask
内部维护了一个状态state
,任何操作(异步任务正常结束与否还是被取消)都是围绕着这个状态进行,并随时更新state
任务的状态; - 只能有一个线程执行异步任务,当异步任务执行结束后,此时可能正常结束,异常结束或被取消。
- 可以多个线程并发获取异步任务执行结果,当异步任务还未执行完,此时获取异步任务的线程将加入线程等待列表进行等待;
- 当异步任务线程执行结束后,此时会唤醒获取异步任务执行结果的线程,注意唤醒顺序是"后进先出"即后面加入的阻塞线程先被唤醒。
- 当我们调用
FutureTask.cancel
方法时并不能真正停止执行异步任务的线程,只是发出中断线程的信号。但是只要cancel
方法返回true
,此时即使异步任务能正常执行完,此时我们调用get
方法获取结果时依然会抛出CancellationException
异常。
扩展: 前面我们提到了
FutureTask
的runner
,waiters
和state
都是用volatile
关键字修饰,说明这三个变量都是多线程共享的对象(成员变量),会被多线程操作,此时用volatile
关键字修饰是为了一个线程操作volatile
属性变量值后,能够及时对其他线程可见。此时多线程操作成员变量仅仅用了volatile
关键字仍然会有线程安全问题的,而此时Doug Lea老爷子没有引入任何线程锁,而是采用了Unsafe
的CAS
方法来代替锁操作,确保线程安全性。
6 分析FutureTask源码,我们能学到什么?
我们分析源码的目的是什么?除了弄懂FutureTask
的内部实现原理外,我们还要借鉴大佬写写框架源码的各种技巧,只有这样,我们才能成长。
分析了FutureTask
源码,我们可以从中学到:
- 利用
LockSupport
来实现线程的阻塞\唤醒机制; - 利用
volatile
和UNSAFE
的CAS
方法来实现线程共享变量的无锁化操作; - 若要编写超时异常的逻辑可以参考
FutureTask
的get(long timeout, TimeUnit unit)
的实现逻辑; - 多线程获取某一成员变量结果时若需要等待时的线程等待链表的逻辑实现;
- 某一异步任务在某一时刻只能由单一线程执行的逻辑实现;
FutureTask
中的任务状态satate
的变化处理的逻辑实现。- ...
以上列举的几点都是我们可以学习参考的地方。
若您觉得不错,请无情的转发和点赞吧!
【源码笔记】Github地址:
springboot项目层次结构_构建多模块的Spring Boot项目步骤全纪录
前言
众所周知,在多个项目中可能会相同的模块,如果每个项目都去创建一遍的话,这样开发效率会很低。比如在开发一个APP应用的时候,有供APP使用的接口项目、后台管理系统,两个项目共用一套数据库,分开的话需要配置多次mybatis,如果有些表需要改动的话,则都需要改动,所以使用多模块管理这些模块的话,会非常的方便。
通过阅读本文你将了解到:如何将已有SpringBoot项目改成多模块 & 如何新构建多模块SpringBoot项目 以下示例基于我正在使用的order(订单服务)进行演示,无论你用的是什么项目,原理都是一样的,这里不要纠结使用的是什么服务。
1、修改最外层pom文件
最外层pom文件是用来管理其他子模块的父级pom,重点将节点修改为pom,修改前可能是jar或war
这里修改成pom是因为打包时,父pom是依赖于子pom进行打包的,父pom会通过节点将所有的子pom模块引入并且进行管理
pom
2、创建多模块
假设你已经设计好子模块的拆分原则,这里开始基于你自己的设计进行子模块的创建
例如:我将order项目拆分成三个不通的子模块,即common、client、server,每个模块负责的事情是不同的
第一步:右键项目名称,选择New->Module
第二步:在弹出的New Module窗口选择Maven
第三步:点击Next,并输入子模块的ArtifactId,如:common、client、server等,然后点击Next
第四步:检查并确认子模块名称及路径是否正确,如果正确则点击Finish
至此,common模块就创建成功了,你会发现order根目录下多了一个common子模块
3、转移代码
创建了common模块后,如果有需要移动至common子模块下的代码,则根据原来的代码目录结构,在common-src-main-java下创建相同的目录结构即可
例如:我项将ProductInfoOutput移动到common子模块下,原来ProductInfoOutput的保路经为com.imooc.order.common,那么我需要在common-src-main-java下创建com.imooc.order.common包,并将ProductInfoOutput拽进去
补充说明:我在创建com.imooc.order.common包时遇到了一个问题,就是右键java文件夹然后选New->Package并创建com.imooc.order.common时,创建出来的包路径时没有层次结构的,也就是说IDEA单纯的为我创建了一个名为com.imooc.order.common的文件夹
经研究发现,是视图的问题,将左上角的Project改成Project Files,然后按照上面的方式创建即可
在转移test目录下的代码时,同样需要注意test目录下的包结构要与转移前的结构一致,这里以server模块为例,因为common下的test目录没有代码
当我们创建好多有的子模块后,观察最外层的父级pom文件内容,发现父级pom已经将我们创建的子模块作为modules引入进来了
4、修改子模块pom文件
当我们创建好多个子模块后,子模块间可能会存在依赖关系,例如我的server模块会依赖common模块下的ProductInfoOutput类
那么我需要在server模块的pom文件中将common模块引入
其他模块间的依赖引用同理
修改完子pom的依赖关系后,一定要刷新pom文件,不然依赖不会生效
5、配置打包插件
SpringBoot项目一般都会打成jar包部署,所以需要在pom中引入spring-boot-maven-plugin的maven插件
例如:我现在要打包order项目,此时我的SpringBoot运行主类在server模块下,那么我就需要以server模块为主要的打包对象
因为打包时,maven插件会去找SpringBoot的启动类,如果没有启动类会打包失败
那么,此时我就需要在server模块的pom中增加spring-boot-maven-plugin的相关配置
org.springframework.boot
spring-boot-maven-plugin
注意:在最外层的父级pom中如果同样存在spring-boot-maven-plugin的配置,记得一定要删除掉
6、打包并运行服务
在maven窗口一次执行Lifecycle下的clean和install(如果你会maven命令,也可以用命令操作)
可以看到,这几个模块都是SUCCESS,说明构建成功了
那我们要的可以运行的jar文件在server模块的target目录下,我们找到它
最后放到服务器上运行这个jar包
使用命令:nohuo java -jarorder-server-0.0.1-SNAPSHOT.jar > order.log 2>&1 &
可以看到,项目已经成功启动了,端口号为默认的8080
至此,构建多模块的SpringBoot项目就已经告一段落了