这篇继续看初始化SpringApplication的时候如何设置初始化器的,上篇看到了getSpringFactoriesInstances()方法,先贴出来:
上篇撸了第一行,理了java的类加载相关,继续往下看:
注释说使用名称并确保唯一,以防止重复,这些names被放进了Set集合,原来大佬们也喜欢用Set防止重复啊,继续看这些names怎么来的,这个SpringFactoriesLoader.loadFactoryNames()方法:
factoryClass就是一开始的参数ApplicationContextInitializer.class,即初始化器的class, 方法一开始是拿到了类名,注意接下来的方法loadSpringFactories()的返回类型是Map,后面也是调用了Map的getOrDefault()方法,看来是要在这个Map中找到key是这个ApplicationContextInitializer类名的value,找不到就是空集合,这个getOrDefault()可以学起来,有的时候可以省去非空判断。看来这个loadSpringFactories()方法单凭一个类加载器加载了一股脑的东西放进了Map里,其中就有初始化器ApplicationContextInitializer,那它从哪里加载的呢,都加载了些什么呢,那就看看下面这个loadSpringFactories()方法。
方法第一行调用了cache.get(),看上去像是个缓存,果然这个cache也是个Map,还是个ConcurrentReferenceHashMap:
这里要注意ConcurrentReferenceHashMap里面存放的对象是软引用,java有强、弱、软、虚四种引用,其中软引用对象在JVM内存不足时就会直接去回收它们,所以它们不会造成内存溢出,同理在Map中也不会造成内存泄漏,这里可以学起来,当然业务中的数据大部分场景还是需要强引用的。关于这四种引用这里就不多介绍了。这个方法最后果然也有这行:
好吧,看来这个单凭一个类加载器加载出来的result还会被放进缓存,key居然是凭借的类加载器。想想也对,一个类加载器辛辛苦苦把一股脑的东西全加载出来了,取用的时候只就一个一个的取,这当然得缓存了。接下来就要看看这个result到底是什么,它从哪里来。接下来是这段:
Enumeration是一个枚举的集合,用起来有点像迭代器,还没有迭代器清晰,这里就理解成一个url的集合就行了,待会要遍历。后面是一个三目运算符,类加载器不为空的时候去classLoader.getResources()寻找静态资源文件路径url,参数路径看一下是什么:
注意这里的路径是不以"/"开头的,还有需要注意这里是getResources()而不是getResource(),这两个方法是有区别的,前者返回所有匹配的资源路径urL,而后者只返回匹配的第一个。而且这两个方法参数路径都不能以"/"开头,因为他们是去classPath路径下去查找,所以必须是相对路径。那源码中的路径拿到的其实就是所有jar包中的spring.factories文件,举一个例子:
这是在IDEA编辑器中,类加载器即AppClassLoader,这里的jar包文件和META-INF也都是在classPath下,如果是打成jar包,别忘了之前说的springBoot自己有一个类加载器LaunchedURLClassLoader,这里面重写了findSource方法,尽管fat jar里面的目录结构不同,还是可以找到这些spring.factories文件。
继续看上图中的loadSpringFactories()中拿到这些spring.factories文件路径后做什么:
可以看到用了Properties类,说明是去读取文件内容了,而且产物result的key是className刚说过了,value就是一个String。随便看一个 spring.factories文件,里面长这样的:
原来这个spring.factories文件里面的配置可以看成一个一个的键值对,而且值是一个类路径,那看来result的值就是这里面给每个键配置的类路径了。
其实到这里已经基本理清了SpringApplication是如何设置初始化器的,过程就是先通过类加载器找到一堆spring.factories资源文件,然后把这些spring.factories文件里面的配置都读取出来,然后再找一个键叫ApplicationContextInitializer.class的看看给他配置的类路径是什么,估计后面就是加载这个类了吧,这一步后面会看到。
这里就可以思考这个spring.factories文件对于SpringBoot的重要性,我们常把SpringBoot的开箱即用特性挂载嘴边,自动配置啊,约定大于配置啊包括starter啊什么的,想用什么功能引个starter就行了,关键就在这个spring.factories文件。
举个例子,比如我们之前用Spring MVC的时候需要配置前端控制器、处理器适配器以及处理器映射器什么的一大堆,而这些都可以在对应starter的spring.factories文件中找到对应的AutoConfiguration自动配置类,再举个例子,我一眼就看到了这个:
不然我们在SpringBoot中访问数据库就只要在yml文件里写一个配置什么也不用做是为什么,难道以前的步骤都消失了?当然不是,只不过是SpringBoot悄悄的做掉了。之所以用spring.factories文件的方式也是考虑到可扩展,你可以自己写starter啊,用自己的starter用自己的方式去做这些事。
好了好了又扯远了,继续看这篇开头那张图,再贴一遍:
好这时候我们已经知道Set<String> names里面是什么了,就是匹配上ApplicationContextInitializer.class的具体类路径,有类路径了那就创建实例instances呗,看一下下面的createSpringFactoriesInstances()方法:
果然上篇文章提到的ClassUtils.forName()出现了,这个方法就是一个加载类再利用构造方法构建实例的过程,具体就不看了。
现在已经理清了getSpringFactoriesInstances()这个方法就是拿指定类的实例,还可能不止一个,现在可以回到SpringApplication的构造方法里面了:
好吧,从上一篇到这里居然只走了一行代码,一个getSpringFactoriesInstances一直说到现在,不过应该的,这个方法里面涉及到了类加载,还涉及到了springBoot的spring.factories配置文件机制,值得详细总结一下。
现在拿到初始化器的实例后就是调用setInitializers()开始设置了,看一下这个方法:
还好没有什么幺蛾子,就是初始化好这个this.initializers,这就算初始化器设置好了,留着用呗,具体怎么用就得看这个初始化器里面是什么了,这里先放一放。
至于下面的setListeners()方法是设置监听器,和setInitializers()一样先拿到ApplicationListener的实例,setListeners()方法里面也是这样:
初始化器和监听器是SpringBoot启动过程中很重要的东西,这里给他们设置好了,具体这两个东西起着什么作用 留着后面看。
SpringApplication的构造方法终于只剩一行了:
推断应用的main入口,可以看一下这个方法:
就是拿到了运行时的线程栈信息,然后判断方法名是不是有一个叫"main"的,找到main方法就加载这个类,就这?
到这里SpringApplication的构造方法终于看完了:
这一行的前半句就足足写了5篇,我还记得当初只是想知道args和java -jar的参数是什么,不过接下来就是run方法了,离我想知道的答案应该不远了。
最后照旧总结一下:这篇主要是介绍了初始化SpringApplication的时候如何设置初始化器和监听器的,留了一个问题,就是这个初始化器和监听器在启动流程中起着什么作用。