作为一个Java开发框架,控制反转(IoC)是首要基础能力,有了IoC容器,框架的其它能力设计都可以方便的解耦和组装。实现IoC首先需要实现对Java运行环境中所有类进行扫描并建立索引,后续的对象创建和组装都将以类索引为基础。
一、IoC容器原理
控制反转(IoC)从思路上说其实并不复杂,它是一种结合了面向接口编程和模块自动组装的设计模式,以“依赖注入(DI)”为主要实现载体,主要包含规范和实现两个层面。
在规范层面:为了实现IoC,需要制定一套开发规则,用来标记依赖注入相关的行为,这套开发规则可能包含注解、接口、配置等(例如Java中的 @Inject 及Spring中的 @Component、@Autowired等),开发规则指明了对象创建、组合的方式和行为,是IoC实现的基础。
在实现层面:IoC容器框架的行为可以简单理解为初始化、依赖解析、依赖注入三个主要阶段:
初始化阶段通常发生在程序开始运行时,在初始化阶段,通过类扫描掌握程序运行环境中所有需要容器管理的Java类,并为这些类建立索引;
依赖解析阶段发生在通过IoC容器创建、获取对象时,在依赖解析阶段,根据所要创建对象的类型、注解,基于继承、实现关系和其它已定义的规则,在类索引中查找可用于实例化对象的类;
依赖注入阶段发生在新的对象被创建时(依赖解析之后),在依赖注入阶段,IoC容器根据对象类中的注解、配置信息等确定对象实例化的方式(例如临时对象、单例、配置注入等),根据不同的实例化方式创建或获取对象实例,如果对象的构造方法参数或对象成员包含其它依赖,则重复依赖解析和依赖注入阶段,直到所有依赖都被成功注入。
二、IoC规范定义
(一)名词定义
参考Java 依赖注入标准(JSR-330)中的定义,为了便于描述和理解,我们把可通过IoC方式实例化并注入的类称为"组件",把组件实例化时需要的其它可注入组件称为"依赖",依赖需要通过注入注解进行标记,IoC注入器会根据注入注解对依赖进行实例化,对依赖进行实例化的行为称为"注入"。
我们把实现IoC注入的程序称为IoC控制器(JM.ioc), IoC控制器中负责管理类、组件、实例的部分称为IoC容器。 IoC容器管理类的范围由"类扫描范围"决定,"类扫描范围"可由一个或多个包名组成,其中的每个包名代表将此包名之下的所有类定义纳入IoC容器的类管理范围,无论这些类定义在多少个实际的包之下。
(二)组件的定义
在JMJava中, 通过@JMBean注解的可实例化的类都可被视为组件,但组件必须包含在IoC容器类扫描范围之内。
通过@JMScope注解可以设置组件的实例化方式, @JMScope注解需要指定value参数来表明组件的实例化方式:
value取值 | 含义 |
JMScopeDef.Singleton | 组件全局单例,每次注入的对象相同。 |
JMScopeDef.Temporary | 组件在每次注入时,都创建新的实例。 |
当组件未指定@JMScope注解时,scope的默认值为JMScopeDef.Temporary。
通过@JMName注解可以设置组件的名称,@JMName注解需要指定value参数来定义组件的名称,当不使用@JMName注解时,组件的默认名称为组件的完整类名。
@JMBean、@JMScope、@JMName使用实例如下:
/**
* 使用默认的scope和name
**/
@JMBean
public class BeanA {
//……
}
/**
* 指定组件实例创建方式为:每次注入时创建新的对象实例
* 指定组件名称为:someBean
**/
@JMBean(scope = JMScopeDef.Temporary, name="someBean")
@JMScope(JMScopeDef.Temporary)
@JMName("someBean")
public class BeanB {
//……
}
(三)依赖注入规范
实现对组件依赖的注入主要有三种形式,三种形式注入的执行顺序为构造方法注入 > 属性注入 > 方法注入。
1. 构造方法注入
为组件定义包含参数的构造方法并用注入注解进行标记,被标记的构造方法会被用于组件的构造,构造方法的参数会在构造前被自动注入。例如:
@JMBean
public class BeanA {
/**
* 在BeanA被其他组件注入时,@Inject标记的构造方法将作为BeanA实例化时调用的构造方法
* 如果没有任何一个构造方法被@Inject等注入注解标记,则选择用默认构造方法进行实例化
**/
@Inject
public BeanA(CompA compA, CompB compB) {
//……
}
}
在组件BeanA被其他组件注入时,将调用BeanA(CompA compA, CompB compB) 进行实例化,而参数compA、compB将由IoC控制器自动匹配适合的组件进行注入。
2. 属性注入
为组件的属性用注入注解进行标记,被标记的属性会在对象实例化后被注入。例如:
@JMBean
public class BeanA {
/**
* compA的值将在BeanA被实例化后,由IoC控制器自动填入
**/
@Inject
private CompA compA;
//……
}
3. 方法注入
为组件的方法用注入注解进行标记,被标记的方法会在对象实例化后被自动调用,如果方法包含参数,则所有参数会在方法调用前被自动注入。例如:
@JMBean
public class BeanA {
private CompA compA;
/**
* setCompA()将在BeanA被实例化完成且完成了属性注入后被自动调用
* 调用时,参数compA将由IoC控制器自动填入
**/
@Inject
private void setCompA(CompA compA) {
this.compA = compA;
}
//……
}
4. 注入注解
除了Java提供的@Inject注解外,JMJava额外提供@JMDefault、@JMPrefer、@JMProperty三种注入注解,其中
@JMDefault与@Inject功能相同,指采用类型匹配方式进行默认注入,例如:
public interface CompA {
//……
}
@JMBean
public class CompB implements CompA {
//……
}
@JMBean
public class CompC extends CompB {
//……
}
@JMBean
public class BeanA {
@JMDefault
private CompB compB; // 实际注入值为CompB的实例
@JMDefault
private CompA compA; // 抛出异常,因为无法决定应用CompB还是CompC进行实例化
}
@JMPrefer注解可接收clazz参数,用来指定优先匹配的注入类型,当clazz指定的注入类型无法完成注入时,再采用默认注入;
public interface CompA {
//……
}
@JMBean
public class CompB implements CompA {
//……
}
@JMBean
public class CompC extends CompB {
//……
}
@JMBean
public class BeanA {
@JMPrefer(clazz = CompB.class)
private CompB compB; // 实际注入值为CompB的实例
@JMPrefer(clazz = CompC.class)
private CompA compA; // 实际注入值为CompC的实例,
// 但如果CompC不符合注入条件, 则注入值为CompB的实例
}
@JMProperty注解只能用于属性注入,表示要注入的属性值由程序加载的properties配置填充,@JMProperty可接收value属性,value属性可指定用于填充的配置名称前缀。
三、IoC实现概要设计
IoC的实现主要包含三个要点,一是IoC容器的初始化,即通过类扫描和注解解析,使得IoC控制器掌握组件创建、注入的全部信息;二是组件实例的创建,即根据IoC容器初始化时获得的信息,根据需要创建的组件类型或名称,完成依赖解析并正确创建/获取组件实例,包含组件注入和显式获取两种方式;三是IoC控制器开放接口,即以接口、注解等方式,为框架使用者提供在IoC控制器不同生命周期阶段添加自定义处理逻辑的能力。
(一)IoC容器初始化
在Ioc容器初始化阶段,主要解决类扫描、创建组件定义和完成静态注入三项工作,具体流程如下:
类扫描阶段的重点是根据指定的包范围,完整的获取项目运行上下文环境中所有的类定义,这些类可能存在于项目包内、项目引入的jar包等,此阶段应考虑预留给开发者执行自定义类扫描的回调钩子,这些回调钩子可以通过实现指定接口的方式来实现。
创建组件定义阶段是IoC容器初始化的关键,依赖于类扫描的结果,针对每个类,根据类、方法、属性的注解,获取类(组件)创建/获取实例的相关信息(JMIocBeanDefinition),这些信息包含作用范围(是否是单例)、注入点、依赖关系、生命周期回调方法等。
当JMIocBeanDefinition被创建后,会根据组件的作用范围,创建对应的实例生成器(JMIocBeanFactory),实例生成器具体负责后续的组件实例创建和获取。
组件的生命周期回调方法主要关注实例创建后的初始化方法(以@JMInitialize注解)、实例获取回调方法(以@JMRefer注解)和销毁前回调方法(以@JMFinalize注解)。
在完成了组件定义后,原则上IoC容器的初始化已经完成,但由于类的静态方法执行不由IoC容器控制,为了保证在类的静态方法被调用前,所有静态注入都已完成,所以在IoC容器初始化的最后阶段,需要完成组件的静态注入。静态注入包含类的静态属性注入和静态方法注入。
(二)创建/获取组件实例
对于IoC控制器来说,创建/获取组件实例的行为通常来自两种方式,一是依赖注入,即针对组件的静态/非静态属性或方法参数,根据注入注解及自身类型,完成组件实例的获取;二是组件显式获取,即通过JMIoc.getBean()方法获取组件实例。其中,组件显式获取是根本,依赖注入只是组件显式获取的封装使用,组件显式获取可以接受组件名称(字符串类型)或组件类型(Class<?>类型)两种参数之一。
在获取组件时,首先根据组件名称或组件类型查找对应的组件定义(JMIocBeanDefinition),然后通过组件定义中的实例生成器(JMIocBeanFactory)完成组件实例的获取。实例生成器可根据不同的组件作用范围提供不同实现,例如针对作用范围为单例的组件生成器可缓存已生成的组件实例,在每次需要获取时直接提供已有单例实例。
虽然不同作用范围的组件实例具体获取逻辑有所区别,但实例的获取具有共有流程:
实例创建过程中,根据类型匹配组件定义和注入过程中的依赖控制是核心,其中“根据类型匹配组件定义”的要点是指根据依赖注入的目标类型,在可能出现的多个组件类型中选择最符合的一个,“注入过程中的依赖控制”是指在进行依赖注入时,需要优先完成所依赖组件的实例获取。这两个核心点在后续详细设计中讨论。
(三)IoC控制器开放接口
为了使得框架更加灵活和开发,需要面向开发者提供一套在IoC控制器不同生命周期中,插入自定义代码或者定制框架行为的接口,例如在初始化阶段添加自定义组件类型、针对特定组件类型定义实例生成器等。这些接口虽然在IoC控制器常规应用中并不重要,但在与其它框架集成协同、IoC控制器高级应用时至关重要。
IoC控制器开放接口可能包含但不限于以下方面:
1. 生命周期钩子
生命周期钩子指通过框架使用者实现指定的接口(interface),实现在框架特定运行阶段,执行开发者自定义代码。框架的特定运行阶段例如:类扫描完成时、组件定义完成时、IoC控制器初始化完成时、组件实例被创建时等。
2. 自定义类扫描器(JMIocCustomClassLoader)
自定义类扫描器指框架使用者可以通过实现特定接口(JMIocCustomClassLoader),实现在基本类扫描结束后,执行自定义代码向已有组件类索引中添加新的组件类。
3. 自定义实例生成器(JMIocCustomBeanFactory)
自定义实例生成器指框架使用者可以通过实现特定接口(JMIocCustomBeanFactory)并配合特定注解(@JMOrder、@JMTargetType等),为指定类型的组件重新定义生成实例的方法。
下一篇:《JMJava框架设计(一):【主线】IoC容器设计(2)主要接口设计》