Spring IOC 启动过程
1. 引言
本篇博文主要介绍 IOC 容器的启动过程,启动过程分为两个步骤,第一个阶段是容器的启动阶段,第二个阶段是 Bean 实例化阶段,这两个阶段各自需要执行的步骤如下图,接下来会一一介绍。
需要注意的是,在 Spring 中,最基础的容器接口方法是由 BeanFactory 定义的,而 BeanFactory 的实现类采用的是 延迟加载
,也就是说,容器启动时,只会进行第一个阶段的操作, 当需要某个类的实例时,才会进行第二个阶段的操作。而 ApplicationContext(另一个容器的实现类)在启动容器时就完成了所有初始化,这就需要更多的系统资源,我们需要根据不同的场景选择不同的容器实现类。
2. 容器启动阶段
2.1 BeanDefinitionRegistry 介绍
在介绍如何加载配置文件之前,先了解一些基础知识。BeanFactory 只是一个接口,它需要一个实现类,DefaultListableBeanFactory
就是一个比较常用的实现类,它还实现了 BeanDefinitionRegisitry
接口,该接口在容器中担任 Bean 注册的角色。
打个比方,BeanDefinitionRegistry 就像图书馆上的书架,所有的书是放在书架上的。虽然还书借书都是跟图书馆(也就是 BeanFactory,或者说 BookFactory)打交道,但书架才是图书馆存放各类图书的地方。所以,书架对于图书馆来说,就是他的 BookDefinitionRegistry。
而每一本书都应该有自己唯一的标识,在容器中每个实例也应该有这样的标识,这些标识是由 BeanDefinition
存储的。它负责保存对象的所有必要信息,例如对象的 class 类型、是否是抽象类、构造方法参数以及其他属性等等,当客户端向 BeanFactory 请求相应对象时,BeanFactory 会通过这些信息返回一个完备可用的对象实例。
2.2 加载配置文件
接下来介绍如何加载配置文件。IOC 的理念是 Don't call us, we will call you
。当一个类中需要另外一个类的实例时,我们并不用手动去创建,IOC 容器会帮我们完成这个任务,但我们需要告诉它哪些类之间存在依赖,例如 A 类中依赖的是哪个类,B 类依赖的类是在哪个包下面,而这些信息我们可以使用注解或者配置文件的方式告诉 IOC 容器。
这里主要讲下读取配置 IOC 是如何读取配置文件的。IOC 容器读取配置文件的接口为 BeanDefinitionReader
,它会根据配置文件格式的不同给出不同的实现类,将配置文件中的内容读取并映射到 BeanDefinition
中,整个过程可以通过如下代码表示:
public static void main(String[] args){
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaPropertiesFile(beanRegistry);
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaPropertiesFile(BeanDefinitionRegistry registry){
BeanDefinitionRegistry beanRegistry = <某个 BeanDefinitionRegistry 实现类,通常为 DefaultListableBeanFactory>;
BeanDefinitionReader beanDefinitionReader = new BeanDefinitionReaderImpl(beanRegistry);
// 读取配置文件的核心方法
beanDefinitionReader.loadBeanDefinitions("配置文件路径");
return (BeanFactory)registry;
}
2.3 解析配置文件
在上面读取配置文件的步骤中,仅仅是将 <bean> 中的属性值读取出来,这只是一些字符串,最终应用程序却是由各种类型的对象实例构成的,我们需要将这些字符串转换成类信息,这个转化过程就需要定义一个规则,这些规则是由 PropertyEditor 定义,由 CustomEditorConfigurer
帮我们传递给 Spring 容器。经过 PropertyEditor 定义的规则将字符串转换为对应的类型信息之后并存储在 BeanDefinition
中,交给 BeanDefinitionRegistry 管理,容器的启动过程就完成了。其初始化阶段可以用一张图来表示:
3. Bean 实例化阶段
Bean 实例化过程如下图:
3.1 Bean 的实例化
容器在内部实现 Bean 实例化时,采用 策略模式 来决定使用何种方式初始化 bean 实例。InstantiationStrategy 定义了实例化策略的接口,SimpleInstantiationStrategy 继承了它,主要通过 反射 来实现对象的实例化。CglibSubclassingInstantiation 继承了 SimpleInstantiationStrategy 以反射方式实例化的功能,并且还有 CGLIB 动态字节码 生成实例的功能。容器默认采用后者实现。
但是需要注意的是,InstantiationStrategy 实例化对象后并没有直接将对象返回,而是用 BeanWrapper 进行包装,方便后续对此实例进行 属性的设置。其设置的依据是通过上面所讲的 PropertyEditor 接口,在第一步构造完成对象之后,Spring 会根据对象实例构造一个 BeanWrapperImpl 实例,然后将之前 CustomEditor-
Configurer 注册的 PropertyEditor 复制一份给 BeanWrapperImpl 实例这样,当 BeanWrapper 转换类型、设置对象属性值时,就不会无从下手了。
3.2 BeanPostProcessor
当对象实例化完成之后,会将对象实例传到 BeanPostProcessor,这个接口的定义如下:
public interface BeanPostProcessor{
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
这个接口为我们扩展对象实例的行为提供了极大的便利,其中 postProcessBeforeInitialization 对应的就是上图中 BeanPostProcessor 的前置处理,postProcessAfterInitialization 对应的就是 上图中 BeanPostProcessor 的后置处理。Spring 的 AOP 就是使用 BeanPostProcessor 来为对象生成相应的代理对象。
3.3 Bean 的初始化
在对象实例话过程调用 BeanPostProcessor 的前置处理 之后,会接着检测对象是否实现了 InitializingBean 接口,如果是,则会调用该接口的 afterPropertiesSet 方法进一步调整对象实例的状态。但是,如果仅仅为了做一个初始化动作而去实现一个接口这样未免有点小题大做,因此 Spring 有提供了另一种方法,就是在 <bean> 中配置 init-method 属性,指定一个方法做对象初始化前的操作。到了这个步骤,对象实例化也快接近尾声了。
3.4 Bean 的销毁
当所有的一切,该设置的设置,该注入的注入,该调用的调用之后,容器将会检查 singleton 类型的 bean 实例,看起是否实现了 DisposableBean 接口,或者查看对应的 bean 定义是否通过 <bean> 的 destroy-method 属性指定了自定义的对象销毁方法。如果是,就会为该实例注册一个用于对象销毁的回调(Callback),以便在这些 singleton 类型的对象实例销毁之前,执行销毁逻辑。至此,Bean 对象的实例化阶段也完成了。
4. 总结
本篇博文主要是对 Spring IOC 容器初始化过程中涉及到的重点接口进行了讲解,对于整个启动过程并没有做一个清晰的梳理,有需要还是建议解析其他的博客以前学习,我觉得这样效果更佳。