http://www.dollarstorearticle.com

Spring之spring容器容器底层原理

  所谓的将组件注入容器中包含两方面,一是组件最先肯定是通过org.x.ClassName这样的类限定名写在xml配置中,二是所谓的容器就是之类的“容器”,通过xml中的类限定径,对类实例化,然后存储在“容器”中。

  而在Spring中,我们将容器给与一个称呼,而不是直呼其名为什么,什么Map之类的,而是叫Spring的容器为ApplicationContext或BeanFactory,没错,Spring中有两种类型的容器。而两个容器有什么区别,容器内部究竟是通过ArrayList或Map怎么样实现的容器功能,在后面详解。

  而后在代码中通过主动“拉取”的形式,将实例化后的组件拉入代码逻辑中。如:@Resource,@Autowired。

  AnnotationConfigApplicationContext 是基于注解来使用的,它不需要配置文件,采用 java 配置类和各种注解来配置,是比较简单的方式,也是大势所趋。

  由以上例子引出本文的主题,怎么样通过配置文件来启动 Spring 的 ApplicationContext?也就是我们今天要的 IOC 的核心了。ApplicationContext 启动过程中,会负责创建实例 Bean,往各个 Bean 中注入依赖等。

  前面说过,Spring中有两种类型的容器,其实前面使用的ApplicationContext实质就是一个BeanFactory,看看两者在类组织结构上的关系。

  接下来,就是refresh(),这里简单说下为什么是 refresh(),而不是 init() 这种名字的方法。因为 ApplicationContext 建立起来以后,其实我们是可以通过调用 refresh() 这个方法重建的,这样会将原来的 ApplicationContext ,然后再重新执行一次初始化操作。

  注意,这个方法是全文最重要的部分之一,这里将会初始化 BeanFactory、加载 Bean、注册 Bean 等等。

  当然,这步结束后,Bean 并没有完成初始化。(这里可能有疑问,我用的ApplicationContext容器,为什么这里又是初始化BeanFactory容器呢,这是因为虽然有两种容器类型的存在,但归根接地ApplicationContext容器是基于BeanFactory容器的,继承于它,可以理解为,其根本就是在创建BeanFactory容器)

  所以,如果有人问你 Bean 是什么的时候,你要知道 Bean 在代码层面上是 BeanDefinition 的实例。

  BeanDefinition 中保存了我们的 Bean 信息,比如这个 Bean 指向的是哪个类、是否是单例的、是否懒加载、这个 Bean 依赖了哪些 Bean 等等。spring容器

  这里接口虽然那么多,但是没有类似 getInstance() 这种方法来获取我们定义的类的实例,真正的我们定义的类生成的实例到哪里去了呢?别着急,这个要很后面才能讲到。

  BeanDefinition 的覆盖问题大家也许会碰到,就是在配置文件中定义 bean 时使用了相同的 id 或 name,默认情况下,allowBeanDefinitionOverriding 属性为 null,如果在同一配置文件中重复了,会抛错,但是如果不是同一配置文件中,会发生覆盖。

  循环引用也很好理解:A 依赖 B,而 B 依赖 A。或 A 依赖 B,B 依赖 C,而 C 依赖 A。spring容器

  默认情况下,Spring 允许循环依赖,当然如果你在 A 的构造方法中依赖 B,在 B 的构造方法中依赖 A 是不行的。

  至于这两个属性怎么配置?我在附录中进行了介绍,尤其对于覆盖问题,很多人都希望出现 Bean 覆盖,可是 Spring 默认是不同文件的时候可以覆盖的。

  现在还在这个类中,接下来用刚刚初始化的 Reader 开始来加载 xml 配置,这块代码读者可以选择性跳过,不是很重要。spring容器也就是说,下面这个代码块,读者可以很轻松地略过。

  // 虽然有两个分支,不过第二个分支很快通过解析径转换为 Resource 以后也会进到这里

  经过漫长的链,一个配置文件终于转换为一颗 DOM 树了,注意,这里指的是其中一个配置文件,不是所有的,读者可以看到有个 for 循环的。下面从根节点开始解析:

  继续往下看怎么解析之前,我们先看下 bean / 标签中可以定义哪些属性:

  有了以上这些知识以后,我们再继续往里看怎么解析 bean 元素,是怎么转换到 BeanDefinitionHolder 的。

  总结一下,到这里已经初始化了 Bean 容器,bean / 配置也相应的转换为了一个个 BeanDefinition,然后注册了各个 BeanDefinition 到注册中心,并且发送了注册事件。

  说到这里,我们回到 refresh() 方法,我重新贴了一遍代码,看看我们说到哪了。是的,我们才说完 obtainFreshBeanFactory() 方法。

  考虑到篇幅,这里开始大幅缩减掉没必要详细介绍的部分,大家直接看下面的代码中的注释就好了。

  注意,后面的描述中,我都会使用初始化或预初始化来代表这个阶段。主要是 Spring 需要在这个阶段完成所有的 singleton beans 的实例化。

  我们来总结一下,到目前为止,应该说 BeanFactory 已经创建完成,并且所有的实现了 BeanFactoryPostProcessor 接口的 Bean 都已经初始化并且其中的

  剩下的就是初始化还没被初始化的 singleton beans 了,我们知道它们是单例的,如果没有设置懒加载,那么 Spring 会在接下来初始化所有的 singleton beans。

  从最后一行往里看,我们又回到 DeultListableBeanFactory 这个类了,这个类大家应该都不陌生了吧。

  接下来,我们就进入到 getBean(beanName) 方法了,这个方法我们经常用来从 BeanFactory 中获取一个 Bean,而初始化的过程也封装到了这个方法里。

  在继续前进之前,读者应该具备 FactoryBean 的知识,如果读者还不熟悉,请移步附录部分了解 FactoryBean。

  第三个参数 args 数组代表创建实例需要的参数,不就是给构造方法用的参数,或者是工厂 Bean 的参数嘛,不过要注意,在我们的初始化阶段,args 是 null。

  到这里,我们已经完了 doCreateBean 方法,总的来说,我们已经说完了整个初始化流程。

  我们先看看 createBeanInstance 方法。需要说明的是,这个方法如果每个分支都下去,必然也是极其复杂冗长的,我们挑重点说。此方法的目的就是实例化我们指定的类。

  大家发现没有,BeanPostProcessor 的两个回调都发生在这边,只不过中间处理了 init-method,是不是和读者原来的认知有点不一样了?

  我们从 Spring 容器中获取 Bean 的时候,可以根据 beanName,也可以通过别名。

  在配置 bean / 的过程中,我们可以配置 id 和 name,看几个例子就知道是怎么回事了。

  以上配置的结果就是:beanName 为 m2,别名有 2 个,分别为 m2、m3。

  我们说过,默认情况下,allowBeanDefinitionOverriding 属性为 null。如果在同一配置文件中 Bean id 或 name 重复了,会抛错,但是如果不是同一配置文件中,会发生覆盖。

  可是有些时候我们希望在系统启动的过程中就严格杜绝发生 Bean 覆盖,因为万一出现这种情况,会增加我们排题的成本。

  添加这两个属性的作者 Juergen Hoeller 在这个 jira 的讨论中说明了怎么配置这两个属性。

  如果以上方式不能满足你的需求,请参考这个链接:解决spring中不同配置文件中存在name或者id相同的bean可能引起的问题

  接下来的问题是,怎么使用特定的 profile 呢?Spring 在启动的过程中,会去寻找 “spring.profiles.active” 的属性值,根据这个属性值来的。那怎么配置这个值呢?

  如果是单元测试中使用的话,在测试类中使用 @ActiveProfiles 指定,这里就不展开了。

  请读者注意 ctory-bean 和 FactoryBean 的区别。这节说的是前者,是说静态工厂或实例工厂,而后者是 Spring 中的特殊接口,代表一类特殊的 Bean,附录的下面一节会介绍 FactoryBean。

  设计模式里,工厂方法模式分静态工厂和实例工厂,我们分别看看 Spring 中怎么配置这两个,来个代码示例就什么都清楚了。

  FactoryBean 适用于 Bean 的创建过程比较复杂的场景,比如数据库连接池的创建。

  我们假设现在需要创建一个 Person 的 Bean,首先我们需要一个 Car 的实例,我们这里假设 Car 的实例创建很麻烦,那么我们可以把创建 Car 的复杂过程包装起来:

  说到这里,我们再来点干货。我们知道,现在还用 xml 配置 Bean 依赖的越来越少了,更多时候,我们可能会采用 java config 的方式来配置,这里有什么不一样呢?

  最有用的场景就是,它用来将前端传过来的参数和后端的 controller 方法上的参数进行绑定的时候用。

  像前端传过来的字符串、整数要转换为后端的 String、Integer 很容易,但是如果 controller 方法需要的是一个枚举值,或者是 Date 这些非基础类型(含基础类型包装类)值的时候,我们就可以考虑采用 ConversionService 来进行转换。

  下面再说一个实现这种转换很简单的方式,那就是实现 Converter 接口。

  只要注册这个 Bean 就可以了。这样,前端往后端传的时间描述字符串就很容易绑定成 Date 类型了,不需要任何操作。

  首先,我们要明白,这里的继承和 java 语法中的继承没有任何关系,不过思是相通的。child bean 会继承 parent bean 的所有配置,也可以覆盖一些配置,当然也可以新增额外的配置。

  但是,如果是 singleton 依赖 prototype 呢?这个时候不能用属性依赖,因为如果用属性依赖的话,我们每次其实拿到的还是第一次初始化时候的 bean。

  一种解决方案就是不要用属性依赖,每次获取依赖的 bean 的时候从 BeanFactory 中取。这个也是大家最常用的方式了吧。怎么取,我就不介绍了,大部分 Spring 项目大家都会定义那么个工具类的。

  Spring 采用 CGLIB 生成字节码的方式来生成一个子类。我们定义的类不能定义为 final class,抽象方法上也不能加 final。

  的返回值用了 MyComnd,当然,如果 Comnd 只有一个实现类,那返回值也可以写 Comnd。

  arg-type 明显不是必须的,除非存在方法重载,这样必须通过参数类型列表来判断这里要覆盖哪个方法。

  我们回到这个接口本身,读者请看第一个方法,这个方法接受的第一个参数是 bean 实例,第二个参数是 bean 的名字,重点在返回值将会作为新的 bean 实例,所以,没事的话这里不能随便返回个 null。

  那意味着什么呢?我们很容易想到的就是,我们这里可以对一些我们想要修饰的 bean 实例做一些事情。但是对于 Spring 框架来说,它会决定是不是要在这个方法中返回 bean 实例的代理,这样就有更大的想象空间了。

  最后,我们说说如果我们自己定义一个 bean 实现 BeanPostProcessor 的话,它的执行时机是什么时候?

  如果仔细看了代码的话,其实很容易知道了,在 bean 实例化完成、属性注入完成之后,会执行回调方法,具体请参见类 AbstractAutowireCapableBeanFactory#initBean 方法。

原文标题:Spring之spring容器容器底层原理 网址:http://www.dollarstorearticle.com/xinwenpindao/2020/0519/14391.html

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。