Spring IoC源码探索(一)

一、探索前:谈谈我对IoC容器的了解

IoC容器主要用于管理Bean的生命周期和对象间的关系,通过依赖注入(DI)对容器中的Bean所需要依赖的其他对象进行注入。而这一切都是在Ioc容器里边进行的,假设A对象依赖B对象,如果IoC容器里只有A没有B,那么将会抛出bean找不到的异常;或者说A对象不在IoC容器,而B对象在IoC容器,那么将达不到自动注入的效果。

二、探索源码:以ClassPathXMLApplicationContext为例看IoC容器的创建过程——Spring版本为5.1.7.RELEASE

1)ClassPathXMLApplicationContext的创建

2)进入构造器,发现其调了另一个更全面(复杂)的构造器

3)很容易从字面上得到信息,spring的配置文件允许多个,是否要刷新(重载加载容器),还有一个是父级容器(可以延伸理解springMVC的容器和应用顶层IoC之间的父子关系),当然我们目前是一个被强转的null,等于当前容器为顶级容器(Root)。

4)深入super可以看到,在AbstractApplicationContext抽象类中,调用了setParent方法,如果父级容器不为空,还会进行了一些环境变量的合并操作,这里就不再深入。

5)追踪setConfigLocations方法,发现所有配置文件路径都只会保留在当前容器里,而且在保存配置文件时,还会对路径字符串进行一个trim的操作,也就是说,在传入配置文件路径时,两端有空格也是不碍事的。

OK,在保存配置文件路径前,还有一个resolvePath解析配置文件的路径,具体就不深入,我猜应该是将环境变量(如:classpath)解析出来吧。

6)最后跟踪refresh,因为只传入路径的构造器默认传入的refresh形参为true,因此是必然会调用refresh()方法的。而且,父容器设置了,配置文件路径设置了,肯定要开始初始化了。

这里是父级抽象类里的方法,方法实现自更上一级的ConfigurableApplicationContext接口。

可以看到,方法一上来就是一个sync,用于做锁的对象是一个new Object();

然后会有一个刷新前的准备操作,比如记录刷新开始时间啦,一些状态变量啦,打印日志啦。

这里还是看下getEnvironment().validateRequiredProperties();具体是什么,因为在其它地方都常见到Enviroment这个类,比如解析路径的时候就有它。

OK,可以看到它属性解析器里有el表达式的 ${ } 和 : 符号,而且属性源列表里有两个类型的属性源,打开其中一个,有很多K/V键值对,结合各种类、属性的各种语义,看来我之前的猜想无误,确实是用于把变量解析出值的。

行,下一个。

7)告诉子类刷新内部bean工厂(百度翻译过来的)

1 // Tell the subclass to refresh the internal bean factory.
2 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

跟踪一下obtainFreshBeanFactory();

OK长话短说吧,如果有BeanFactory,那么关闭它构建过的单例,再把它关闭掉(置为null让GC回收它),然后在创建一个新的BeanFactory,并进行一些初始化操作,用sync上锁赋值。值得一提的是,创建BeanFactory时,会先尝试获取父容器的BeanFactory作为parentBeanFactory,不过现在是顶级容器,所以注定为null。

8)差点忽略重点,上面讲到创建BeanFactory,第133行代码的位置,loadBeanDefintion这一行至关重要,正是我想了解的地方,接着追踪查看。

 兜兜转转,终于来到了解析xml定义的bean的代码位置。

 

OK,解读这一段:

首先,获取<bean>节点上的id属性;

接着,获取<bean>节点上的name属性;那么,name属性呢,会被作为别名,多个别名可以允许用英文半角符号下的逗号和分号进行分割(, ;)。

接下来,就是以(,;)分割name属性上的值,将每个别名都分割出来,虽然它在内部对分割出来的每个别名做了trim操作,但我还是建议不要留有空格。

再接着,确定beanName,如果id属性有的话,就用id属性,没有的话,就会从别名中抽出第一个别名作为beanName,我说的抽出,就是它里边的aliases.remove(0)。

然后呢,会进行一个对beanName的查重操作,这里边可以看出,beanName和aliase是存放在同一个命名空间的(Set<String>集合),因此,在上一步aliases需要用remove(),否则自己就会抛名称重复异常。

接下来都差不多,就是一些属性的解析,子节点的解析。然后都封装到org.springframework.beans.factory.support.DefaultListableBeanFactory的beanDefinitionMap属性里,当然还有很多其它的细节我没有去深究,水平有限,太细了脑袋会爆的。

9)那么什么时候开始实例化单例呢,回到refresh()方法.可以看到在获取到beanFactory后,进行了很多准备操作,红框部分为真正实例化Bean的部分,在它前后可以看到onRefresh()和finishRefresh()方法,其中onRefresh是一个空方法,用于扩展实例化Bean前的操作。接下来看红框部分的finishBeanFactoryInitialization。

进入方法后也能看到,Spring的源码里有用到Lambda表达式,之前有遇到过Java8之前版本Lambda报错,于是这里我尝试了把IDEA的Language level改为了7,发现也能正常运行,仔细回想,之前碰到的Lambda报错是因为IDEA允许写Lambda,但是由于选择的java编译器是Java8版本前的版本,于是编译不通过。那么这么一想,我如今加入的依赖实际上是Spring已经编译好的jar包,也就是说Lambda表达式已经被编译成了字节码文件,即使把运行环境切换回Java8之前的版本,也是可以运行的。那么我大胆的猜测,Java8和之后推出的一些语法糖在编译后,同样能在低版本的jvm上运行(这个离题了,之后再试,到时候顺便去看看官方文档)。

咱们继续分析上面的代码,最后一行就是初始化单例的操作了,那么在它的上一行,还有一个冻结配置的操作,代码粘贴出来,都能看懂,冻结后具体有什么用,咱先不看。

继续,重点来了。

 

循环迭代所有定义的Bean,获取到Bean在Root容器中的定义 (这块的合并定义有点东西,有空再看仔细),接着就是判断Bean的类型是否为org.springframework.beans.factory.FactoryBean接口的派生类,如果是,还会经过一些系统权限特殊处理。当然,最后都会到达getBean(beanName)。

看到这个getBean(beanName),我忽然猜想,RootBeanDefinition是有一个lazyInit属性的,这个属性默认为false,为的就是让Bean在需要时,才会初始化,那么getBean是否就是为需要Bean时调用的方法,这样的话一切逻辑都能说得通,在getBean的时候,如果Bean没初始化,那么给Bean初始化再返回Bean,因此,非懒加载的Bean单例直接调用getBean即可完成初始化。Ok,带着猜想继续往下看。

10)而getBean的底层全是org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

11)那继续深入,doGetBean,先看下方法下的代码

 

嗯,方法还挺长的,截了好几次图。

简单理解下:

1.尝试获取单例,如果单例已经存在了,那么做一些验证后没有问题直接返回这个单例,里边的逻辑还涉及一些工厂Bean和普通Bean的选择问题,也挺复杂的,就不细说。这其实已经可以证明了我上面的猜测了。

2.1如果示例并不存在,那么首先检查它是不是在构建中,如果已经在构建中了这里还来构建, 那可能已经进入了一个循环构建的状态了,这时候就会直接抛出【BeanCurrentlyInCreationException】,后边我会针对这个异常进行测试。

2.2先获取父级BeanFactory,如果存在父级的BeanFactory并且自身有没有该beanName对应的BeanDefinition的话,那用父级的BeanFactory来提供这个Bean,如果一直到Root级的容器也提供不了这个Bean,那就是【NoSuchBeanDefinitionException】了。那么这里的话可以参考上面的第7大点,创建BeanFactory的时候,会将父级容器里的BeanFactory作为自己的patentBeanFactory,那我们这里的是Root级别的容器,BeanFactory也就是Root级别的,所以只能自己构建了。

2.3判断下是否要类型检查,一般都是false,我看来一下,只有一个地方用了true,位置在方法org.springframework.beans.factory.support.AbstractBeanFactory#getTypeForFactoryBean。 咱不深究,就以现在是false的状态继续吧,会执行markBeanAsCreated(beanName),这里在真正构造Bean之前,先记录一下Bean已经构建了,然后还把mergedBeanDefinitions集合里的Bean的定义给remove掉了。

2.4分析Bean定义开始,调用的getMergedLocalBeanDefinition(beanName),哇塞,上一步刚吧BeanDefinition从mergedBeanDefinitions集合中remove掉,这一步又给加回去了,感觉删掉那一步有些多此一举,就像是个BUG,不过倒不会引起什么异常。

接下来注意了注意了,这里要开始构建了,如果忽略掉多级容器,多线程什么什么的,我觉得这个位置算是最核心部分了,在我看来接下来这块就是IoC和DI的具体实现。

2.5从RootBeanDefinition定义中获取到构建依赖(dependsOn),这个dependsOn,其实就是在配置<bean>的时候可以配置一个"depend-on"这么一个属性,里边只能配置其他的beanName,如果这个属性有值,就会先等待依赖的beanName先构建好,再继续构建自身,如果配置的是自己,或者是依赖自身的其他类,那么就会陷入死循环,抛出异常比如:【BeanCreationException】。

2.6如果存在依赖,那么注册依赖关系,这依赖关系是双向维护的,如果A依赖B,那么A所需依赖里有B,B的被他依赖里有A。

2.7然后先把所需依赖给初始化了,也就是A依赖B,那么先把B给初始化了,这里就算是为DI做前置准备了。

2.8接着,就判断Bean的scope,如果是singleton,getSingleton

如果是prototype,那就重新构建一个Bean的实例。

再else,还有别的特殊处理,比如request、session,是需要webApplicationContext下才生效的这里就不翻那么多了。

这里咱先关注singleton,可以看到getSingleton()穿了两个参数,一个是beanName,一个是Lambda表达式,嗯,就是构建了一个匿名内部类的实例作为参数。嗯,函数式编程杠杠的。

 

2.8.1这里同样是,如果单例已经存在了,就直接返回,如果没有存在,就调用专属的ObjectFactory#getObject构建一个实例再返回。

2.8.2跟踪createBean方法。

再跟踪doCreateBean,

在一连串繁琐的处理后,终于还是来到了BeanUtils……

先给构造器设置为可访问的,还检查一下是否为Kotlin的类,是就用Kotlin的方式构造实例,不是就用调用原生的newInstance。唉,再深入就是反射的源代码了,前段时间刚看到一篇文章说反射的对象调用超过一定次数后会被生成class字节码加载到jvm里,今天看到了那段代码,但是让脑子缓缓吧,改天再研究反射的实现。

最后,对象构建完成后,还有属性的注入。

在doCreateBean方法内,有这么一段代码:

其中populateBean则是对属性进行赋值的,一直找到org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyPropertyValues

会看到遍历属性赋值这一段:

如果有关联(即ref),则会在valueResolver.resolveValueIfNecessary(pv, originalValue);内进行检查。

跟踪查看

 

再跟踪到resolveReference里:

可以看到它会在这里getBean,如果ref的bean已存在,自然会直接得到,如果没有存在,则会先初始化。嗯,然后上面的初始化操作又来一遍。

属性值得到后,会经过一系列类型转换的处理完后,装好在属性值映射集合里,再通过反射调用Setter方法给属性赋值,当然了,类型转换的处理主要是基本属性类型和包装类,至于复杂属性类型(引用类型)直接强转就OK。

备注:

用于类型转换的类型修改器默认只有overriddenDefaultEditors,共12个,均为与IO相关的比如URL、InputSteam。

基本类型的修改器在首次查到没有时,才会创建,共47个,比如int、long、String,当前Spring版本为5.1.7.RELEASE。

12)至于注解方式的注入,我下次再探究,但我推测,只需要在扫描包的时候,将注解式修饰的类、方法、属性等解析成BeanDefinition,一样可以进入这个流程。

三、这里我们对一些依赖异常进行测试和笔记。

1)实例的属性依赖自身:

 

① scope="singleton"时,Spring能处理好这个关系,成功允许。

② scope="prototype"时,会抛出异常【BeanCurrentlyInCreationException】。也之前我们提到的那段检查的代码,就是用来检查scope="prototype"的。

 2)<bean> 的depends-on依赖自身:

 解析成BeanDefinition的时候是不抛异常的,只有在运行到构建dependOn时,才会抛出异常【BeanCreationException】,无论scope是prototype还是singleton。

3)构造器中依赖自身:

 同样,不管scope是prototype还是singleton,都会抛出异常【BeanCurrentlyInCreationException】。

4)两个Bean构造器中相互依赖:

 

双方是 都是scope="prototype"时,抛出异常【BeanCurrentlyInCreationException】。只要有一方是 scope="singleton",则可正常运行。

如果是多个,比如:

 

 那Role必须是单例的情况下,才能正常运行了。

四、总结:

从容器结构来看,理论上IoC容器可以达到无限嵌套,在子容器维护着父级容器的关系,父子容器各自定义的Bean的单例都会缓存在各自的BeanFactory的singletonObjects里,当子容器在singletonObjects中找不到Bean时,会往父容器里找,或者说子容器中可以定义新的Bean屏蔽掉父级的Bean,使得切换不同的Bean实现可以更加灵活。但是呢,父容器因为没有维护与子容器的关系,因此父容器里是无法通过getBean获取到子容器的Bean的。

Bean的定义上,会将Bean的构建条件都解析封装到BeanDefinition中,才开始初始化单例。定义时,要规避死循环一般的依赖,也就是在实例化Bean前,避免依赖关系又回当前Bean。

从IoC容器中获取一个未实例化的Bean A时,会先将它所有的必要依赖(depend-on和constructor-arg)加载到容器,再实例化Bean A,之后将属性中的依赖(property ref="xxx")从IoC容器中获取得到,再将准备好的所有自动装配的属性值通过反射调用Setter方法赋值给Bean A。

自个画了两张图:

① 父子容器间的关系图:

 

② 容器初始化大致流程

 

水平有限,如果有哪里写的不对的,欢迎指出,我会及时改正,避免误导大家。

 

posted @ 2019-06-22 17:22  Supalle  阅读(457)  评论(0编辑  收藏  举报