Spring IOC流程解析
概述
工作过程中断断续续的有使用过spring、spring boot框架,平时接触的最多的也是这些,但是由于平时更多的是 使用到这些东西,因此对于原理没有深究,不过随着使用的深入,发现还是有必要了解一些原理性的东西,不然平时 听别人吹逼的时候你只能抓瞎,就问你慌不慌?(当然也有些公司基于spring开发了自己的一套框架,因此源码的学习 还真的不是吹逼就完事了的)
关于spring的介绍,接下来我会分成多个模块来分别总结,本小结主要是针对spring的IOC机制来讲解。 IOC全称叫做inversion of control,翻译过来就叫做控制反转,我们平时说的DI(dependency injection:依赖注入) 可以认为是控制IOC的具体实现,因此当别人提到spring的IOC和DI的时候,我们可以将其理解为是同一种模式,这种模式的 特点是当我们构造一个对象的时候,该对象所需的依赖都由一个controller传进去,这种机制也就解释了控制反转的意义, 将自身对象生成的权限由自身移交给另外一个控制器,控制反转带来的好处就是解耦合,被创建的对象本身并不需要关心 自身的依赖,只需要将依赖设置成接口即可,这样就可以在装配的时候由控制器传入不同的实现,从而快速的切换业务线。
启动过程分析
构建demo
接下来我们先构建一个spring的工程,然后通过断点调试来一步步讲解IOC的流程,项目整体结构如下:
由于本文主要是解析IOC的机制,因此所需的依赖只有spring-context,没有必要把杂七杂八的依赖都引入下来
1 | <dependency> |
然后我们在resource目录下新建一个application-context.xml的文件,具体内容如下:
1 |
|
接下来我们构建工程所需要的类HelloService、WorldService,内容分别如下:
1 | import org.springframework.beans.factory.annotation.Autowired; |
最后我们新建一个MainService用来测试当前的工程,如下:
1 | import org.springframework.context.ApplicationContext; |
最后我们从控制台看到了hello !,整个过程就是这么简单(不过这个过程可是真不简单),可能我们忍不住想问一下,IOC在哪里体现出来呢?
答案就在上面的context.getBean(“hello”);这一行,这里的context可以理解为一个controller,我们想要什么对象就跟其进行申请即可。 多说一句我们在看Spring相关源码的时候经常会看到各种context,也就是各种上下文,起初的时候我是经常会被绕晕,后来就慢慢发现 所谓的上下文可以理解为一个持有指定范围的公共变量的容器,比如我们在上面看到的ApplicationContext就可以认为是持有整个应用的 公共变量,因为持有的是整个应用的公共变量,因此这些变量在整个应用中都是可以共享的,这么说是不是就明白了。
接下来我们会针对上面的代码进行分析,整个分析过程也会分成两部分:
- 容器的实例化
- bean的实例化
容器的实例化
在进入看具体的代码前,我们先来概要的说一下什么是容器,并从主观的角度去考虑一下,如果是我们自己来写框架的话,我们要注意什么。
说到容器我们最先想到的可能就是水杯之类的东西,那么他的主要的作用是什么呢?应该是holder住一些东西吧。嗯,我们在spring里面提到 的容器大致的作用也是如此,用来存放一些资源,或者更准确的说是存放一些对象!那么为什么要存放这些对象呢?我们平时写代码,只管 new一个对象,剩下的回收工作都交给JVM去操作了,这一切看起来都很合理,缓存对象带来的好处就是减少new一个对象和回收一个对象所带来的 开销。
那么有没有什么不太适合这种缓存对象场景的呢?那肯定也是有的,比如说我们生成了一个包含状态的对象,那么一旦我们缓存了这个对象 之后,后面再使用这个对象就会发现已经是一个脏的对象了(这里的脏表示缓存的对象已经缓存了上次调用的信息),因此,缓存一个有状态的 对象显然不是一个好的选择,这也是我们在spring中通常都使用singleton的原因。 相信很多人在spring的工程中使用httpclient都会记得,我们在声明这种类型的java bean的时候我们通常都会指定prototype,指定为prototype的 作用就是当我们再次需要这种类型的对象的时候,会直接生成一个新的对象,并不会复用之前的对象。
言归正传,我们接下来看一下容器的创建创建过程吧:
1 | public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) |
下面我们会针对refresh这个方法进行分析,不过在分析之前我们还需要做足一下功课,梳理一下容器的继承关系,
不然这么多东西还是啃不动。
一眼看去真的是眼花缭乱,本来以为就创建个容器,一下子冒出来这么多东西,也真是没谁了。不过我们也用不着害怕,我们只需要知道
一些关键的东西就可以了。需要注意的是,虽然我们在项目中看到了这么复杂的继承关系,不过ApplicationContext并不应该理解为
BeanFactory的实现类(虽然继承关系上来看是这么回事),而是ApplicationContext内部会持有一个beanFactory,我们所有的
和beanFactory调用相关的都是会通过内部持有的beanFactory来进行调用(从这里来看ApplicationContext更像是BeanFactory的
一个代理),这个beanFactory类型是DefaultListableBeanFactory,其类关系图如下:
另外值得一说的是,不要看某一个类实现了这么多的接口就抓瞎了,接口本身是提供了方法的声明,所以其实是增加了操作某一个对象的方式。
因此一个类继承的接口越多,则从侧面说明该类的功能越齐全,这也是为什么ApplicationContext内部会持有一个DefaultListableBeanFactory
了,功能强大呗。
说了这么多,我们看一下refresh方法吧,如下:
1 | public void refresh() throws BeansException, IllegalStateException { |
看起来也不是太复杂嘛,嗯,也就是乍一看。上面我们有说过容器创建的目的:持有资源,接下来的 分析请牢记这句话,不然看着看着保证能让你跪掉。
按照上面的代码,我们先来看一下obtainFreshBeanFactory这个方法吧, 毕竟这个方法执行完毕之后我们就会得到一个beanFactory,当然也就是 我们的容器。代码如下:
1 | protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { |
如上核心代码自然就是refreshBeanFactory了,我们点进去看一下refresBeanFactory都做了那些操作吧, 不过很遗憾方法是抽象的,那肯定是在子类里面有实现吧。没错方法是在AbstractRefreshableApplicationContext,上代码:
1 | protected final void refreshBeanFactory() throws BeansException { |
上面注释也是比较详细了,核心代码的话就是loadBeanDefinitions:将bean的定义转换成BeanDefinition加载到beanFactory中 ,最终的格式就是<beanName, beanDefinition>,继续跟进代码如下:
1 | protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { |
顺接上文,我们进去loadBeanDefinitions方法查看一下:
1 | protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { |
这里在强调一下,我们的reader内部已经持有beanFactory对象,我们接下来只需要继续跟进loadBeanDefinitions方法即可:
1 | public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { |
上面代码中我们看到,代码最终的执行分成两块:parseDefaultElement、parseCustomElement, 如果解析的节点是beans、bean、import、alias 走的就是parseDefaultElement方法,至于其他的标签, 后文我们会通过spring的扩展机制来具体展示一些这种机制(主要是namespace、xsd、以及namespacehandler)。 由于我们本节主要是探讨IOC的机制,因此我们关心的标签其实是bean标签的解析,如下:
1 | // 以下方法有些跳跃,因为不重要的代码是在太多,到这里其实已经不是太重要了,因为这里涉及到的仅仅是DOM树的解析 |
上面这一步结束之后,我们就已经获取到了beandefinitionHolder对象了,不过请注意,这里有两个循环, 第一个是遍历资源路径加载资源,本例中就是我们的application-context.xml文件,另外一个循环是在某一个 文件加载之后解析标签的过程中循环遍历的。
在完成beandefinitionholder初始化之后,我们就可以注册该beandefinition,并将该事件发送出去,我们接下来看一下beandefinition的注册 究竟是一个什么样的过程吧:
1 | public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) |
整体看下来,主要的流程也就是向beandefinitionmap中添加对应的beanName和beanDefinition,暂时可以先告一段落。到此 我们已经分析了容器的初始化的流程及中间的操作。总结起来如下步骤:
- 创建容器
- 加载文件并创建dom解析器
- 根据标签的namespace选择不同的解析流程
- 将解析之后的beandefinition注册到容器中
bean的实例化
在上文中我们将spring的初始化分为两个部分,本小结我们就来看一下bean的实例化是一个什么样的过程,方法入口为 finishBeanFactoryInitialization(其实在这之前我们实现了beanfactorypostprocessor接口的类的postprocessbeanfactory 不是已经调用过了么?既然方法都已经调用过了,那对应的对象是不是在这之前也已经生成了呢?是的, 在invokeBeanFactoryPostProcessors方法的时候就已经生成了对应的对象了,实例化的方法也和正常的实例化没什么区别), 这一步是会将所有的非lazy-init的singleton都给实例化,所谓的lazy-init是在使用到的时候才回去实例化的一种机制, 注意上面容器初始化是将xml文件映射为beandefinition,而此处的初始化则是生成bean中指定类的具体的对象!
1 | protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { |
接下来会分别看一下容器实例化的三个步骤:
创建bean
千淘万漉虽辛苦,终于看到曙光了,代码如下:
1 | protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) { |
到此为止,我们所需要的bean已经完成创建,只不过这个bean还空空如也,还需要填充相关的属性,当然最重要的是我们在代码里面使用 Autowired的方式装配的一些属性了。
属性填充
填充属性代码如下:
1 | protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { |
回调函数
代码如下:
1 | protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) { |
代码分析到此可以截止一个段落了,或许对我们来说依然是非常凌乱了,那我们还是总结一下上面创建javabean的思路吧: 在容器的初始化阶段我们获取的信息是beandefinitionMap,存放beanname和beandefinition的一个map, 有了这个信息我们就知道了我们要创建哪些bean了,接下来只要遍历这个map的key创建对应的bean就可以了,首先会根据是否是 factorybean及eargeinit来决定是否需要马上创建,总之如果需要创建的话,最终都会走到同一个地方, 接下来会判断是否由于循环引用导致创建该单例的对象提前创建了,如果对象提前创建了那直接返回就完事了,如果没有创建就会走到 正常的创建流程,在正常创建之前还可以通过instinationawarebeanpostprocessor的切面操作来短路掉创建对象的过程, 最后正常的创建流程也比较简单,就是反射生成对象、填充属性、执行回调函数,这里回调函数的顺序是aware、postprocessorbeforeinitinalition 、init-method、postprocessorafterinitinalition(执行的是当前这个bean的回调函数!!)
总结
关于IOC的总结到此为止,后面会整理个小例子演示一下spring的扩展机制