因查找ht项目中一个久未解决spring内部异常,翻了一段时间源码。以此文总结springIOC,容器初始化过程。
语言背景是C#。网上有一些基于java的spring源码分析文档,大而乱,乱而不全,干脆自己梳理下。
废话不多说,进正题。
打开spring.core .dll,这是核心库,找到ContextRegisty类,此类为密封类,无继承,
本类实现对spring容器进行管理,获取一个容器均会通过此类来打交道,以,此类相当于我们使用IOC容器的入口。
注意我圈红的地方
图1
1处是一个字典,管理父子容器,IOC容器中可以有任意类型,容器中有子容器这是允许的,虽然生产中极少看见,但是,我翻看Spring源码的时候,确实看到了这种情况。不多说。
2处Context管理类ContextRegisty实例,单列模式。
3处,bool变量,标识根容器对象是否正在创建,默认false
4处 ,用于线程同步的资源锁,Object类型即可
5处,根容器对象的名称
6处,这两个方法是我们用到最多的,我们获取IOC容器的入口
打开类型构造器
图2
可以看见,对12345处的变量进行了初始化。
打开实例构造器
图3
发现这里对管理容器的字典进行了实例化。
从字面上,毫无疑问,一个hash表,键对大小小敏感。
在我阅读的大量技术资料中,提到众多缓存组件均采用的hash表这种数据结构。包括大名鼎鼎的分布式缓存Memcache,redis等,原因是hash表查找效率极高,易管理,并且线程安全。Spring中对容器对象的管理也采用了hash表的数据结构,不多说。
dll入口内容说完,看spring配置文件
这段代码是spring源码中的一段配置,形式已经固定。不多说。
在configSections中自定义配置节。并且配置节点处理器
图4
Huatong生产中的配置,一样的
图5
不同仅仅在于context节点的节点处理器不一样,这个后面再说。
因为前面源码中的那个配置,是跑单元测试要用的config,所以自定义了一个节点处理器。这里跟我要讲的不会有很大关系。不多说。
在看下context和object,还有parser节点的详细配置
图6
1处资源解析器
2容器配置,注意本处出现了容器嵌套,注意resource节点,指定资源为config类型,并且指定了child和parent下的objects
3处,指定父容器所管理配置的对象
4处,容器所管理的对象
Dll预览和config配置预览结束。来看下生产环境是如何用spring.net IOC的,看图
图7
图7圈起来的就是我们用容器的入口,在这个方法内部,就进行了容器初始化处理。
要讲的就是整个容器是如何一步步初始化的。
拿源码中的单元测试代码
图8
这个是单元测试的入口,要注意的就是1处。1处是原开发者写的测试代码,2处是我改过的。这里的using,无非就是重新指定context的节点处理器,完全就是指定一个委托实例。
还记得springcontext节点处理器么
图9
打开guard方法,guard方法是分配context的委托实例。
图10
1处指一个默认的context节点处理器。这个是ContextHandle是所有context节点处理器的父类。自己是可以自定义节点处理器的。
回到单元测试。现在我们通过调用我无参数guid()方法,不分配具体的委托实例,那么context节点处理器则会按照HookableContextHandler内部默认处理器实例来处理。
对了,在开始调试之前,有的人对配置的节点处理器有疑问。简单说下,是这样的。我自己查阅资料并实验。发现,当ConfigManager.GetSection(string name)这个方法调用的时候,代码会触发进入到ContextHandle内部。说明什么呢?说明getSection内部有一个委托,而我们配置的节点处理器,都是满足这个委托的类型(签名和返回值),一旦getSecion读取指定节点,那么将会回调对应的Handle,相当于一个触发的作用。
具体就是,当我们从config从读取context节点内容时候,将调用context节点处理器处理
读取Object节点的时候,将调用Objects节点处理器,当读取parser节点的时候,将调用parser节点处理器。
那么既然这样。我们可以预言,spring容器的初始化一定是在contextHandle内部完成的,真的如此么,擦亮眼睛,一步步看。
开始调试,睁大眼睛
代码已经进来,单步,go
Context初始化,go
初始根容器对象名称为null,从结构上看,修改容器初始化标,rootContextCurrentlyInCreation,初始是false。
注意我圈红的地方,contextSectionname,默认为”spring/context”,打开spring.core.dll,看下
看源码
都是作为常量定死的。我看到了大神rod Johnson,和griffincaprio,的名字,spring的缔造者。众多geek的偶像。神一般的人物,后者现在是一家公司的cto,刚FQ去加了他的twitter。嗯,写了不少技术和管理的文章。最厉害的是Johnson,学音乐的,我擦,居然成了码神,让我等码农情何以堪。不吐槽了,继续正题。
马上进入try块,开始读取配置节,单步go,此时将读取context配置节,将进入contexthandler
代码进来,默认的节点处理是ContexHandle实例,继续go
进来,代码进到create方法内部来 了。也就是说,当读取config指定节点的时候,会调用Handle处理器中的Create方法。
三个参数,parent父对象,configcontext配置上下文对象,section,指定节点下所有的内容。这个是ms封装好的。自定义节点处理器必须实现IConfigurationSectionHandler接口
其实就是实现上面提到的create方法。不多说,继续go
2处,未发现自定义context节点处理器(无委托方法),使用基类ContextHandle处理器处理。Go
注意我圈红的地方,查看innerxml,发现context节点内部的内容,ok,看下config是否一致
嗯,完全一致,继续,go
对context名字进行处理,即将进入容器配置的对象加载过程,睁大眼睛
Go
从上到下,初始化要返回的context,看我的注释,好理解。最重要的是resources的获取,
睁大眼睛,go
获取context类型,本处默认xmlApplicationContext
继续Go,读取context的一个的配置类型属性,再次去读context配置节,再次触发context节点处理器,代码会进对应的handle,获取真实类型
Go
1处观察到真实类型为xmlApplicationcontext,2处读config,读context节点配置是否大小写敏感。默认true
Go
取到值是true
Go
接下来是非常重要的一步,加载context节点下配置的objects,这里context节点下读取的配置支持多重协议,http,uri,config,ftp等等,resources就是要将这些所有支持的配置协议类型文件中配置的对象全部读取出来。
Go
注意contextElement
Go
发现context下有三个子节点
Config中是不是有三个子节点,看config
圈起来的123,两个资源节点,一个文本节点
Go
这里发现resourcenodes返回只有一个节点,那么意味着,父容器配置的资源节点全部被检索出来了,而子容器配置的资源节点和文本节点均被舍弃。注意,当前初始化的容器是父容器。现在要做的工作是,为父容器注入要管理的对象类型
Go
1处初始化父容器,go
进入InstantiateContext内部,1处定义要返回的容器对象,2处定义一个容器初始化器
继续go。
用已经得到的资源和相关参数,创建一个容器初始化器实例,紧接着干什么,用容器初始化器进行容器初始化,
Go,进入InstantiateContext方法
1处,发现,这里需要容器的构造器信息,我们要初始化一个容器,必然要知道他的构造器是什么样子的,1处的这个方法,就是获取容器构造器信息,到这里,代码越来越难,越来越超过我们的学习范围,没事,走流程,看懂每一步就ok
进入GetContextConstructor
传入参数类型数组作为参数,ContextType为XmlContextApplication类型
Go,进入GetContextConstructor内部
拿到构造器信息ctor,得到类型和参数
继续go,调用InvokeContextConstructor,传入构造器信息ctor
继续go,接下来的代码是我有耳闻但是从来不知道是什么的东西代码,继续
拿ctor创建一个safeConstructor,究竟他是干嘛的,我也不清楚,继续
从代码和源码注释可以看处,这里有两个字段,分别接受构造器信息,通过构造器信息动态创建一个构造器,应该属于反射的内容
继续,go进入GetOrCreateDynamicConstructor
有一个构造方法委托,首先进来从缓存中取构造方法,没有的话,通过DynamicReflectionManager. CreateConstructor(constructorInfo)得到一个构造函数,并且安全缓存下来
继续go,进CreateConstructor内部
返回类型是一个委托类型。内部的代码,应该是使用emit直接写IL代码。查看ctor
我也看不懂。继续,返回一个委托类型,这里已经可以看到ctor的类型了。
继续go
SafeConstrutor构造结束,俩属性分别接收了构造器信息和构造器
继续go
调用SafeConstrutor的Invoke方法,内部是拿构造器,和参数进行XmlApplicationContext实例创建的过程。参数中是有对象配置项的。看索引为2的参数。可以预料的是,这个时候,创建context实例。必然会再次读object配置节。会再次触发handle
继续,获取配置的resource资源项解析器
大神也卖萌,看注释。
进入resourcehandle注册管理类
发现有各种资源项的处理器,ftp,config,file,assembly等、继续
注意getSection,当我进入这个方法内部的时候,并没有调入到某个handle里面,为什么呢?是因为我的config文件中,spring节点下并没有配置resourcehandlers节点,那么委托没有实例。也就是读这个节点的时候,不会触发之前我们看到的那种操作。不会进入到某个handerle处理程序里。
继续go当前的context中的是资源配置项,协议走的是config,
那么根据协议类型获取一个SafeConstructor
看前3图,预注册了6种协议的资源处理器,这里只需要根据config类型,取出一个SafeConstrutor就行了,跟前面介绍的一样。有了这个东西,我们可以以反射的形式初始化一个实例,而且他是通过Emit直接写的IL,效率很高,虽然不怎么懂,但是感觉很牛逼J
继续
返回一个IResource资源对象
继续
读spring/objects节点下的内容
和前面一样,触发一个handle,代码进入,为什么这里会触发呢,因为我在config中对objects节点配置了节点处理器
再次证明这里是指定委托类型。读取节点触发调用。
拿到object的xml
装载配置的Object对象,返回配置的object的个数
继续
在读取以xml信息存在的的object时候,发现读取了parser节点,进入
看注释说,这个方法没啥用,不管了。
继续
在这里方法里面,里面的层级非常深,以前跟踪进去过。代码很复杂,就不跟踪进去了。
我知道的是,他会装载所有的配置的object对象
继续,代码最后回到这里
当所有object对象装载完毕以后,那么这里一个xmlApplicationContext实例就构造出来了,转换成IApplicationContext,返回
看到这里,context’容器对象被构造出来了。紧接着,无非是堆栈地址弹出一次返回。当返回到context配置节的时候,会检测子节点有无context,如果有,那么重复以上过程,构造子容器对象,并装填子容器管理的对象Objects,然后返回。
在子容器对象创建的时候,父子容器管理的数据结构是一棵树结构,可以层层深入,并且这些容器对象都被注册在一个容器字典里,hash查找非常之快,不同容器内的对象互不影响。而且这种容器管理结构线程安全。内部在创建构造器,资源解析器,还有对象装填,均使用了缓存,效率也不会低。尤其是在创建构造方法的使用使用了emit。直接写IL,比常规反射性能高得多。
一步步跟踪了spring容器的创建代码。各种设计模式的灵活使用,缓存和同步方法的使用,在安全,高效率的前提下保证了巧妙和灵活。
兴许看了源码,你才能体会到spring真正精髓所在。里面对代码可靠性,安全性,性能,灵活性的控制,达到了一种极致的状态
认真一步步看了IOC容器初始化部分的源码,顿觉之前接触的任何项目。写的任何代码均是浮云,只能算作toy
这还是只是冰山一角。
如果仅仅会配置,会使用,你哪里来的底气说了解,熟悉,精通?
贴上《庄子.秋水》的一段,与君共勉
“秋水时至,百川灌河;泾流之大,两涘渚崖之间不辩牛马。于是焉河伯欣然自喜,以天下之美为尽在己。顺流而东行,至于北海,东面而视,不见水端。于是焉河伯始旋其面目,望洋向若而叹曰:“野语有之曰,‘闻道百,以为莫己若’者,我之谓也。且夫我尝闻少仲尼之闻而轻伯夷之义者,始吾弗信;今我睹子之难穷也,吾非至于子之门则殆矣,吾长见笑于大方之家。”