IOC 工作原理如下:
- 初始化 IOC 容器.
- 读取配置文件
- 解析 Bean 信息, 存储到一定的数据结构中 (
BeanDefination
). - 根据上述数据结构初始化相应的 Bean.
- 注册对象间的依赖关系.
OK. 那么我们根据上述原理来设计一下基本组件. 为方便, 使用 json 格式的配置文件.
面向接口编程 ( Program to interface ). 即通过接口指定行为, 然后在具体的实现类中实现, 丰富行为.
读取配置文件的本质是加载配置关系. 维护关系的可以是项目 CLASSPATH 下的配置文件或是本机的某个目录, 其它机器的某个目录, 通过网络来读取. 我们将这种配置关系以及需要读取的内容统一称为资源 ( Resource ). 为了方便, 我们默认使用resource 目录, 按名称读取.
配置文件中存储着 Bean 的定义, 以及 Bean 之间的依赖关系. 我们需要用某种数据结构来描述它, 以便于将其嵌入程序, 加载到内存.
class name: BeanDefinition
type : interface
methods :
setName();
setBeanClassName();
getName();
getBeanClassName();
它有一个默认实现: DefaultBeanDefinition
. 然后我们在读取配置文件时, 将其转成 BeanDefinition
的 List. 就完成了配置文件中的 bean 配置到内存中的数据结构. 需要注意的是, BeanDefinition
是个接口, 所以我们在解析 json 时应该保存到其实现类中.
Bean 的初始化我们想要交给第三方来维护, 再需要使用时直接交付给调用方即可. 这可以通过通过工厂方法模式的变体来实现. 普通的工厂方法需要针对每一个产品建立一个工厂, 但 IOC 框架不行. 因为在前一种情况, 产品都是已知的, 所以在编写时可以确定工厂; 而后一种情况, Bean 是随时可能变化的, 不可能每一次修改都相应地增删工厂. 我们用一种 Java 原生的强大技术来创建实例 ---- 反射. 我们称该接口为 BeanFactory
等等! 我们刚存入内存结构的 Bean 定义如何与初始化 Bean 的 BeanFactory
产生联系呢? 毕竟那是初始化的依据啊. 因此, 很自然地想到, 我们要将前者依赖到 BeanFactory
的实现类中. 由于我们是根据 beanName
创建, 获取 Bean. 所以很自然的使用 Map 这种数据结构. 而在许多应用场景中, 都可能存在并发访问的情况, 比如 Web 应用程序. 而 Bean 也是支持延迟加载的, 很可能出现多个线程访问一个未经初始化的 Bean 的情形. 所以需要使用并发版本的 Map ---- ConcurrentHashMap.
为了方便用户使用, 我们对 BeanFactory 进行封装, 而我们是基于 Json
格式的, 因此称其为 JsonApplicationContext
. 在创建其对象时, 会对整个容器初始化 .
- 解析 json 格式的配置文件时, 没有检查格式是否满足. 后续应该加上.
LitableBeanFactory
是 BeanFactory
的扩展, 它的目的是获取某一类型的全部 Bean
实例, 它根据类型枚举其所有的 Bean
实例, 而不用被客户端请求时根据名称一个一个查找.
本版本中提供了泛型版本的 getBeans(Class<T> type)
. 既然 LitableBeanFactory
是 BeanFactory
的扩展, 很容易想到前者继承自后者, 而其实现类 DefaultListableBeanFactory
自然也继承自 DefaultBeanFactory
, 只实现 LitableBeanFactory
中接口即可.
有了 BeanFactory
的经验, 我们很容易想到使用 Map 来缓存这种关系. 然而, 如果想要根据 type
获取全部的 bean names
, 则又不方便了. 但是我们知道, 可以通过 beanMap
根据 name
获取对应的 bean
实例. 所以我们取中庸之道, 缓存 type
与 all bean names
的映射, 这样既可以根据 type 直接获取 bean names
, 也可以方便地获取 bean instances
.
由于我们能够根据 BeanDefinition
获取到 Class, 所以这种映射关系在注册 bean definitions
时一并缓存即可.
-
在阅读 Spring 源码时, 发现了一个不错的编程习惯:
对于超类的方法, 一律以 super 引用; 对于自身的域, 一律以 this 引用, 自身的方法, 直接引用.
// org.springframework.beans.factory.support.DefaultListableBeanFactory#destroySingletons @Override public void destroySingletons() { super.destroySingletons(); this.manualSingletonNames.clear(); clearByTypeCache(); }
此处的"例"指的是实例 ( instance ), 即对象. 单例即单个对象 -- 单例范围的类只会被初始化一次, 也就是在容器中始终只存在它的一个单独的对象, 每次请求都是同一个; 多例即多个对象, 也就是每次请求时都会创建新的对象. 很明显, 我们之前实现的版本都是单例的.
首先我们先来讨论一下单例与多例的问题. 单例的好处在于开销小, 因为对于同一个类只存在其一个实例, 坏处则是存在线程安全问题 -- web 请求通常有多个, 每个请求独占一个线程, 所以是多线程的环境, 因此要时刻注意线程安全问题. 而多例则与之相反, 虽然每次请求都是创建一个新的对象, 也正因为如此, 避免了多请求时的线程安全问题. 如果我们使用单例, 一个很好的手法 ( idiom ) 是: 不要使用不安全的私有域 ( 如 private Map<K,V> map = new HashMap();
使用 Spring 注入的其它依赖不算 ); 总是用其安全的版本替代它 ( 对于刚才的例子来说, 就是使用并发版本的 Map -- ConcurrentHashMap
), 如果非要使用, 要么就得声明该类为多例模式.
赋予 bean 使用单例/多例的属性, 并在初始化时根据属性予以区分. 我们使用 scope ( 范围 ) 来命名这个属性.
说到延迟加载, 那么肯定就存在非延时加载 -- 即时加载. 即时加载指的是在容器初始化时就被一同初始化的 bean. 而与之相对, 延迟加载指的是被请求时才开始初始化的 bean. 很容易想到, 多例模式的 bean 天然就是延迟加载的. 只有单例模式对此存在区分. 当然了, 我们之前的实现都是延迟加载, 因为都是被请求时才创建 bean, 在容器初始化时并没有执行 bean 的初始化工作.
赋予 bean 即时/延迟加载的属性, 并在初始化时根据属性予以区分. 我们使用 lazyInit
来命名这个属性.
我们发现, 单例/多例与延迟加载其实都与容器的初始化过程息息相关. 如果是多例或延迟加载, 那么在容器初始化时就不对该 bean 进行初始化, 反之, 如果是单例且非延迟加载的话, 就需要将其初始化, 并缓存.
首先, 我们会对 bean 增加两个属性: scope
和 lazyInit
分别区分其范围和是否为延迟加载. 相应地, 也要在 BeanDefinition
增加相应的 API, 其实现类中增加相应的字段和实现. 由于 lazyInit
是 boolean
类型, 而且默认为 false
, 与我们的设计相符合. 但是 scope
是 String
类型, 所以我们要为每个 bean
设置一个默认的 scope
, 与 Spring 一样 ,我们设为 singleton
.
现在我们来初步引入容器的生命周期, 它包括 初始化 ( init() ) 与销毁 ( destroy() ). 相应的, Bean 也具有生命周期, 而且通常挂载在容器的生命周期上. 比如, 初始化前后, 销毁之前这些节点, 都可以对注册过的 Bean 执行相应的操作.
初始化我们之前已经实现了, 这次只是优化一下. 而销毁方法是这次需要新加入的.
初始化方法是容器的入口, 可以说是"必经之地", 所以直接顺序调用即可. 而销毁方法我们如何调用? 一个可行的思路是, 当程序能够正常退出时, 我们可以手动输入命令, 触发销毁方法. 但是, 如果执行过程中出现了异常或是 JVM 直接退出了, 那按这种方式销毁方法就无法被执行到了. 所以我们要换一种一定能够被执行到的方式 ---- 由于 Java 程序受 Java 虚拟机的管理, 因此这种方式一定是通过虚拟机实现的. 虚拟机能监听程序的退出或自身的退出, 因此只要将容器的销毁事件注册上去, 那么就可以在程序正常退出或虚拟机退出时执行, 也就是保证能够执行到. 在 java.lang.Runtime
类提供了一个 addShutdownHook()
方法, 它是一个虚拟机提供的钩子方法, 能在程序正常退出或 JVM 退出时并发地被执行 ( 每个钩子都运行在单独的线程中 ). 关闭钩子 ( shutdown hook
) 的本质就是初始化了但未启动的线程, 当虚拟机执行关闭序列时, 所有的关闭钩子都将并发执行.
OK, 那么答案已经很明显了 ---- 只需要在初始化时注册容器的 destroy()
这个行为到关闭钩子上就可以了. 在程序正常退出或 JVM 退出时, JVM 会帮我们用新的线程来执行.
上面讲了容器的生命周期, 是容器的维度, 对容器自身做一些操作. 而我们也想依托容器的生命周期对 Bean 进行一些操作, 比如销毁前释放资源等. 我们随着项目的深入一点一点拼凑这个 Bean 生命周期的拼图. 本期先实现两种: 初始化后 ( postConstruct
) 与销毁之前 (preDestroy
).
虽然 Bean 的行为更多, 有着更多的状态, 但由于 Bean 是受容器管理的, 这些状态无一不是挂载到容器的生命周期的某个节点上. 所以只要在相应的节点嵌入即可.
这是个很好的问题. 我们明白了如何区分, 也就知道了如何引入. 我们当然可以搞成属性, 然后绑定一个方法, 这样比较灵活, 适合自定义的方法; 还有一种固定的但是使用方便的方式: 提供一个接口, 该接口会被嵌入到生命周期中执行 ( 如初始化节点 ). 任何需要注册初始化事件的 Bean 都应该实现该接口, 然后在相应的节点以多态的方式 ( 或反射 ) 被执行.
Java 天然为这种"处理特定对象的统一的程序流程"提供了一种很好的解决方案 ---- 注解. 我们只需要对想要注册的行为用特定的注解修饰, 就可以很容易与未注册的 Bean 区分开来. 自然地, 在嵌入时, 我们也只需要遍历所有的 Bean, 筛选出注册了相应注解的那些, 并执行其方法即可. 容器无法实现知道它们是什么类型, 也不需要知道, 直接用反射, 当做 Method 对象执行即可.
注解是一种显式的"声明", 被注解的对象会被统一的流程处理. 使用某个注解就有点类似实现某个接口, 也就是具有了某种身份. 然后在统一的处理流程中, 只有该身份的实例会被处理. 这种处理特定对象的统一的流程, 常常是嵌入程序的主流程的. 比如在我们的 IOC 框架中, 嵌入到了生命周期中. 反过来想, 只要某个统一的流程处理能够嵌入整个流程中, 那么就可以通过注解的方式简化对待处理对象的标记.
这次我们来做一些小的改动 ---- 引入异常码. 然后再完善下之前的异常. 最后我们来重构下 DefaultPostConstructBean
和 DefaultPreDestroyBean
中的一些方法.
我们之前创建的 Bean 都是不带有初值的, 这次我们来做一些改动, 允许用户显式地设置 Bean 的初值, 并在创建时赋予.
根据我们之前的经验, 每当需要为 Bean 添加一些属性时, 很自然的会想到修改配置文件, 并在 BeanDefinition 中定义相关接口. 本次也是这样. 我们创建一个名为 NewInstanceBean
的接口, 用来创建实例并设置初始值. 由于它的行为是通用的 -- 可以被所有的 BeanDefinition 共享, 因此我们将其实现类设置为单例的.
接口类型的变量作为类的字段时, 解析 Json
就会报这个错误. 暂用 FastJson
代替, 但是它是以代理的 json 对象来代替接口, 而非真正的接口.
- 循环依赖的问题.
- 我们实现了 Bean 自定义实例化方法 ----
FactoryMethod
的方式与预设构造器参数两种方式的初始化. 那么, 如何基于 setter 初始化? - 构造器参数的方式 要求 Bean 必须提供相应的构造器, 但有时用户可能指向指定某些字段的初始值, 还有没有更好些的方法? ( 虽然感觉该方法其实也还可以. )