前言
IoC已经是目前业界非常主流的一种容器技术,全称为Inversion of Control,中文翻译为“控制反转”。它还有一种另外的术语叫Dependency Injection(依赖注入)。这些概念用好莱坞的一个原则来描述就是“Don‘t call us,we will call you.”,这句话其实很好的表达了IoC的含义。但往往让很多初学者难以理解,过于抽象。所以本文的内容将其含义一步步进行剖析和解释,为什么需要IoC,以及如何实现一个简单的IoC容器。
1. 理解概念
在深入学习之前,我们需要先铺垫一些必要的概念,这有助于大家对整个知识面的贯穿和理解。如果你已经掌握了依赖倒置原则和控制反转,可以从第2小节开始阅读。
1.1 依赖倒置原则
依赖倒置原则(DIP)是面向对象的五大设计原则(SOLID)之一,这个原则奠定了IoC的核心思想和理念。DIP的核心告诉我们,“高层模块不依赖于低层模块,两者应该依赖于抽象”。我们如何理解这句话呢?首先我们看看下面的代码片段,并分析一下代码中所存在的一些问题。
public class Wrench {
public void repair(){
System.out.println("Repairing...");
}
}
public class Worker {
public void work(Wrench wrench){
wrench.repair();
}
}
上面的代码很简单,工人(Worker)依赖于一个扳手(Wrench)来完成修理的工作。Worker是调用方,称之为高层模块。Wrench是被调用方,称之为低层模块。
image这个例子从功能实现的层面上看似乎没什么大问题,但仔细思考一下,在实际的业务场景中,Worker在完成一个具体的修理工作时可能需要使用不同的工具,Wrench只是其中之一,如果此时更换另一种工具比如钳子(Pliers),那么就必须修改Worker类,这样就违反了DIP,同时还违反了OCP(对内修改是关闭的,对外扩展是开放的)。如果要遵循DIP,Worker(高层模块)就不应该直接依赖于Wrench(低层模块),让他们两者都依赖一个抽象,我们再来看看修改后的代码。
public abstract class Tools {
public abstract void repair();
}
public class Wrench extends Tools{
public void repair(){
System.out.println("Use wrench repairing...");
}
}
public class Pliers extends Tools{
public void repair(){
System.out.println("Use pliers repairing...");
}
}
public class Worker {
public void work(Tools tools){
tools.repair();
}
}
上面的例子中抽象出了Tools类,并包含一个repair的抽象方法。而Worker类的work方法不再直接依赖Wrench,而是依赖于Tools这个抽象类,它不需要关心Tools的子类有哪些,运行时决定由具体哪个子类对象来执行。Wrench只需要继承抽象Tools类并实现标准的repair方法完成自己相关的业务逻辑。这样高层模块不再依赖具体的低层细节,两者都面向的是一个抽象。当需要更换工具(扩展功能)时,只需要编写新的类(例如Pliers)继承Tools即可,而Worker类是不需要的修改的。这样的编码就很好的遵循了DIP以及OCP。
image1.2 控制反转
前面的例子解释了DIP的思想和原则,如果要运行以上的程序,我们还需要在客户端代码中维护Worker以及Tools的创建以及相关的依赖关系。
public class Main {
public static void main(String[] args){
Tools tools = new Wrench();
Worker worker = new Worker();
worker.work(tools);
}
}
仔细观察以上的代码,我们发现其中包含了对象维护的两个工作:
- 创建Wrench以及Worker的实例
- 完成Worker与Wrench之间的依赖关系
因此这里又出现一个潜在的问题,就是当需要更换Wrench的时候,又需要更改Main这个类的代码(搞半天,又回到了问题的根源)。这其中主要的问题就是上面的两个工作(对象的创建以及对象之间的依赖关系)都是以硬编码的方式出现在程序中。因此可以将这两个工作移交给一个独立的组件去完成,它核心职责就是完成对象的创建以及对象之间依赖关系的维护和管理,那么这个组件我们将其称之为“容器”。我们先看看下面的代码片段。
public abstract class Tools {
public abstract void repair();
}
public class Wrench extends Tools{
public void repair(){
System.out.println("use wrench repairing...");
}
}
public class Worker {
//容器会通过这个方法自动将Tools的子类对象传递进来
private void setTools(Tools tools){
this.tools = tools;
}
public void work(){
tools.repair();
}
}
public class Main {
public static void main(String[] args){
//工厂容器
BeanFactory factory = new BeanFactory();
//从容器中直接获取Worker对象
Worker worker = factory.getBean("worker", Worker.class);
worker.work();
}
}
使用容器的好处在于,当我们需要Worker对象的时候,不需要自己来创建,通过容器提供的getBean方法直接获取即可,因为容器已经帮我们创建好了Worker。如果Worker类需要依赖Tools来完成具体事情,则可以在Worker中提供一个set方法(不一定是set方法,也可以是构造方法或其他的方式),让容器通过这个方法将Tools的具体子类或实现类传递进来,这样就给Worker装配的Tools。而容器在调用set方法并传入一个Tools实例到Worker中的这个过程就是所谓的控制反转,也叫依赖注入(你可以理解为容器通过某种手段将Tools的实例注入到了Worker中),这个容器我们也将其称之为IoC容器,它很好的完成了对象的创建和对象之间依赖的工作。
2. 实现简单的IoC容器
通过前面的学习我们都很清楚IoC容器的核心职责是负责对象的管理以及对象之间的依赖。也可以将其分开理解为容器就是负责对象的创建和管理,IoC则是完成对象之间的依赖注入。而依赖注入的前提是要有容器的支撑,因为任何需要注入的对象都必须从容器中获取,则实现的第一步是先编写一个管理对象容器。
2.1 实现BeanFactory
既然所有对象的创建过程都交给了容器,那么它不就是典型的工厂吗?没错,实现这个容器本身也是对工厂模式的一种体现。
2.1.1 哪些Bean交给容器管理
在一个项目当中不是所有的类都需要纳入容器管理,我们可以配置哪些类需要让容器管理,这个配置的过程可以使用Java的注解或者xml来完成,所有纳入容器管理的对象我们统称为Bean。比如使用xml配置:
<bean id="people" class="edu.demo.People"/>
<bean id="wrench" class="edu.demo.Wrench"/>
id表示这个bean在容器中的唯一标识,class则是这个类的全限定类名,接着就可以使用java对这个xml进行解析。当然,你也可以使用Annotation来配置,那么首先可以先自定义一个注解,然后将这个注解标注在类上面,后续通过反射对注解进行解析(后面的实现过程都以注解配置的方式来实现)。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
public String value();
}
自定义一个@Component注解,@Retention(RetentionPolicy.RUNTIME)表示这个注解会在运行时一直保留,@Target(ElementType.TYPE)表示这个注解只可以标注在类上。并且这个注解有一个value属性类型为String。value属性的作用是用于定义当前类在容器中的唯一标识(类似xml配置中的id)。
@Component(value="people")
public class People {
...
}
定义好注解后就可以应用在类上了。像上面的例子,如果类上面定义了@Component注解,就表示这个类是受容器管理的,并且这个类在容器中有一个唯一的标识“people”。如果注解中只有一个属性,并且属性名为value的情况下,那么在定义时可以省略,例如@Component("people")。
2.1.2 Bean的创建形式
当客户端从容器中获取Bean实例的时候,容器并不一定每一次都新建一个,它可以事先创建一个并一直驻留在容器中,每次获取的时候返回同一个实例,可以达到实例复用的目的,也可以节省内存的开销,这种做法类似与单例模式。但是这种方式也存在一个问题,就是线程安全。由于在运行时的数据区中只有一个实例,在多线程的情况下,这个对象是被多线程所共享的,所以在使用时要求这个实例是线程安全的。当然,在很多场景我们也会要求容器每次都返回一个新的实例。这种方式创建出来的实例并不会驻留在容器中,用完即扔的效果。所以这就必须要求容器对Bean提供不同的管理方式。
2.1.3 Bean的作用域
既然容器可以以不同的方式来构建和管理Bean实例,那么这就涉及到另一个概念“作用域”。你可以理解为作用域就是Bean实例的一个生命周期或者存活时间。试想一下,以单例的方式创建的对象会一直驻留在容器中,那么就表示直到这个容器关闭或者销毁的时候Bean实例才会跟随着销毁。Bean的有效存活时间是在整个容器的创建到关闭的有效范围。如果每次获取的是一个新的实例,由于不会驻留在容器中,那么Bean的有效存活时间为就是这个对象使用完或者无引用的时候等待jvm的回收。我们可以将单例的Bean的作用域命名为singleton,将每次新建的Bean的作用域命名为prototype,其他的作用域还可以包括web中的request或者session等作用域,这里为了简化只对singleton和prototype进行实现。那么问题来了,如何告诉容器Bean的作用域是哪一种呢?没错,还是使用注解配置,我们看看下面的代码片段。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Scope {
String value() default "singleton";
}
@Component(value="people")
@Scope
public class People {
...
}
我们可以自定义一个@Scope注解,value属性用于指定作用域的值,默认为singleton,这个注解同样只能标注在类上。这样容器在解析注解的时候就知道以什么样的方式来创建Bean实例以及管理它的作用域。如果类上没有标注@Scope注解,可以让容器默认以单例的方式来创建。
2.1.4 类扫描
前面我们使用自定义注解来标识容器是如何创建和管理Bean实例,接下来就是进行类扫描。扫描的目的就是为了收集当前项目下以及所依赖的jar文件中所有class的全限定类名。(为什么要扫描依赖的jar文件?因为我们有可能将标识@Component注解的类最终生成jar文件让其他的项目依赖使用)我们看看下面的ScanUtil类。
public class ScanUtil {
private static final Set<String> classNames = new HashSet<String>();
/**
* 依据指定的包名扫描包中以及子包中所有的类
* @param packageName 包名
* @return 全限定类名的集合
*/
public static Set<String> scan(String packageName) {
if(packageName == null){
throw new RuntimeException("The path can not be null.");
}
String packagePath = packageName.replace(".", "/");
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
Enumeration<URL> urls = loader.getResources(packagePath);
while(urls.hasMoreElements()){
URL url= urls.nextElement();
if("file".equals(url.getProtocol())){
scanFromDir(url.getPath(), packageName);
}
if("jar".equals(url.getProtocol())){
JarURLConnection connection = (JarURLConnection)url.openConnection();
scanFromJar(connection.getJarFile());
}
}
} catch (IOException e) {
throw new RuntimeException("Resolve path error.", e);
}
return classNames;
}
/**
* 扫描目录
* @param filePath 文件目录
* @param packageName 包名
*/
private static void scanFromDir(String filePath, String packageName) throws UnsupportedEncodingException{
filePath = URLDecoder.decode(filePath, "utf-8");
packageName = URLDecoder.decode(packageName, "utf-8");
File[] files = new File(filePath).listFiles();
packageName = packageName + ".";
for (File childFile : files) {
if (childFile.isDirectory()) {
scanFromDir(childFile.getPath(), packageName + childFile.getName());
} else {
String fileName = childFile.getName();
if (fileName.endsWith(".class")) {
if(packageName.charAt(0) == '.'){
packageName = packageName.substring(1, packageName.length());
}
String className = packageName + fileName.replace(".class", "");
classNames.add(className);
}
}
}
}
/**
* 扫描jar文件
* @param jarFile
*/
private static void scanFromJar(JarFile jarFile) {
Enumeration<JarEntry> files = jarFile.entries();
while (files.hasMoreElements()) {
JarEntry entry = files.nextElement();
if (entry.getName().endsWith(".class")){
String className = entry.getName().replace("/", ".").replace(".class", "");
classNames.add(className);
}
}
}
}
scan为核心的扫描方法,依据传入的包名将其解析为URL枚举进行遍历,然后根据url对象的Protocol来决定是对目录还是jar进行扫描。如果Protocol是file则调用scanFromDir方法,如果是jar则调用scanFromJar方法。
2.1.5 BeanDefinition
扫描类的目的是为了要过滤出哪些类上面标注了@Component注解,因为只有标注了这个注解的类才能纳入容器的管理。并且还需要将这些类的Class对象以及还有可能标注的@Scope作用域(后面会详细讲解作用的概念)等信息收集保存起来,容器在初始化时需要依据这些信息来构建Bean实例。问题是如何保存这些信息呢?这就是BeanDefinition的作用,每当解析到一个带有@Component注解的类,那么就将它的Class对象以及@Scope信息封装到一个BeanDefinition中保存,容器就会根据这个BeanDefinition来创建Bean实例。
public class BeanDefinition {
/**
* bean的作用域(创建方式)
*/
private String scope;
/**
* bean的Class
*/
private Class<?> beanClass;
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public Class<?> getBeanClass() {
return beanClass;
}
public void setBeanClass(Class<?> beanClass) {
this.beanClass = beanClass;
}
}
通过上面的代码不难看出,BeanDefinition就是一个很普通的Javabean,它仅仅封装了需要管理对象的Class以及作用域信息。每创建一个BeanDefinition都代表一个Bean的描述定义,后续通过描述定义来构建具体的实例。
2.1.6 编写容器
前期的铺垫工作已经差不多了,下面开始将编写最核心的组件"容器"。
public class BeanFactory {
/**
* 存放bean的描述
*/
final Map<String, BeanDefinition> definitionMap = new ConcurrentHashMap<>();
/**
* 存放单例bean的实例
*/
final Map<String, Object> singletonMap = new ConcurrentHashMap<>();
/**
* 在构造方法中初始化并构建所有bean描述
* 以及单例的bean
*
* @param path 扫描路径
*/
public BeanFactory(String path) {
Set<String> classNames = ScanUtil.scan(path);
//初始化原型
initDefinitionMap(classNames);
//初始化单例
initSingleton();
}
/**
* 根据扫描的类名进行解析,找出带有@Component注解的类,并构建成
* BeanDefinition实例,保存到definitionMap集合中
*/
private void initDefinitionMap(Set<String> classNames) {
for (String className : classNames) {
Class<?> beanClass = getClass(className);
//检查beanClass是否标注了@Component注解
if (beanClass.isAnnotationPresent(Component.class)) {
//获取@Component注解的value属性的值,这个值作为bean在容器的唯一标识
String beanName = beanClass.getAnnotation(Component.class).value();
//如果容器已经存在bean,则抛出异常
if (definitionMap.containsKey(beanName)) {
throw new RuntimeException(
"conflicts with existing, non-compatible bean definition of same name and class ["
+ beanClass + "]");
} else {
definitionMap.put(beanName,
createBeanDefinition(beanClass));
}
}
}
}
/**
* 根据权限顶类名获取Class对象
*
* @param className
* @return
*/
private Class<?> getClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Can not find the class name " + className + " to build the description.");
}
}
/**
* 构建bean描述定义,将bean的scope以及类名封装到BeanDefinition中
* 创建的Bean描述会放入definitionMap的集合中保存
* Bean的类名作为集合的key,而整个BeanDefinition对象作为value
*
* @param beanClass
*/
private BeanDefinition createBeanDefinition(Class<?> beanClass) {
// 创建BeanDefinition
BeanDefinition definition = new BeanDefinition();
//设置Bean的Class对象
definition.setBeanClass(beanClass);
//设置Bean的作用域
definition.setScope(resolveScope(beanClass));
return definition;
}
/**
* 解析Scope,如果bean的class上指定了Scope注解,则将@Scope的value属性值作为Bean的创建方式
* 否则Bean的默认创建方式将使用单例
*/
private String resolveScope(Class<?> beanClass) {
String scope = (beanClass.isAnnotationPresent(Scope.class)) ? beanClass
.getAnnotation(Scope.class).value() : "singleton";
return scope;
}
/**
* 初始化SINGLETON实例放入bean容器中
*/
private void initSingleton() {
for (String beanName : definitionMap.keySet()) {
BeanDefinition definition = definitionMap.get(beanName);
if ("singleton".equals(definition.getScope())) {
Object bean = newInstance(definition);
singletonMap.put(beanName, bean);
}
}
}
/**
* 根据描述定义创建Bean实例
* @param definition
* @return
*/
private Object newInstance(BeanDefinition definition) {
try {
return definition.getBeanClass().newInstance();
} catch (InstantiationException e) {
throw new RuntimeException("Create bean instance fail.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Create bean instance fail.", e);
}
}
/**
* 获取bean实例
*
* @param beanName
* @return
*/
public Object getBean(String beanName) {
return doGetBean(beanName);
}
/**
* 获取bean实例(泛型)
*
* @param beanName
* @param clazz
* @return
*/
public <T> T getBean(String beanName, Class<T> clazz) {
return (T) doGetBean(beanName);
}
/**
* 从容器中获取Bean的BeanDefinition
* 如果Bean的BeanDefinition的scope为singleton,则从singletonMap中获取单例
* 否则以原型的方式创建并返回
*/
private Object doGetBean(String beanName) {
BeanDefinition definition = definitionMap.get(beanName);
if("singleton".equals(definition.getScope())){
return singletonMap.get(beanName);
}
return newInstance(definition);
}
说一下整体思路,BeanFactory中维护了definitionMap和singletonMap两个map集合,这两个集合都作为容器的一部分存在,只是存放的内容不一样。definitionMap用来存放所有Bean的描述定义(BeanDefinition),singletonMap则存放所有Scope为singleton的Bean实例。在创建BeanFactory的同时,通过构造方进行容器的初始化。依据传入的包名进行类扫描,接着调用initDefinitionMap方法执析并初始化所有的BeanDefinition保存到definitionMap中。然后调用initSingleton方法初始化所有单例的Bean(注意,在初始化所有单例的过程中也是依据先前构建好的BeanDefinition来创建)。完成这些步骤,容器就初始化完成了。但容器还要需要对外提供一个可以让客户端从容器中获取Bean实例的方法。注意观察代码中的两个getBean方法(两个方法区别只是对泛型的支持),它们都调用了doGetBean的私有方法,这个方法会判断是以单例还是原型的方式来构建Bean实例,如果scope为singleton。那么直接从singletonMap中获取先前初始化好的对象并返回,否则调用createBean方法依据BeanDefinition创建一个原型的实例并返回。到此,简单的容器就实现好了。
2.1.7 测试容器
容器编写好后我们需要测试一下容器的运行效果。首先将编写好的所有源码编译并导出为一个jar文件,在测试项目中依赖进来。这里使用先前的案例来进行简单的单元测试。
@Component("worker")
public class Worker {
public void work(Tools tools){
tools.repair();
}
}
在Worker类上标注@Component注解,并给value属性赋值一个worker,表示在容器中的唯一标识。这里没有使用@Scope注解,因此容器默认就是以单例的方式来构建Worker实例。
public class BeanFactoryTest {
@Test
public void testGetBean(){
//创建工厂容器
BeanFactory factory = new BeanFactory("edu.demo");
//从容器中直接获取Worker对象
Worker w1 = factory.getBean("worker", Worker.class);
Worker w2 = factory.getBean("worker", Worker.class);
System.out.println(w1 == w2);
}
}
上面的代码中创建了一个BeanFactory,并两次调用getBean方法获取Worker对象w1和w2并比较他们的引用地址是否相等。
true
从结果显示,两个地址是一样的,表示两个引用指向的是容器中同一个实例。
然后我们再给Worker类加上@Scope注解并指定为prototype。
@Component("worker")
@Scope("prototype")
public class Worker {
public void work(Tools tools){
tools.repair();
}
}
再次执行单元测试查看结果。
false
从结果得知,每次调用getBean方法时,容器都是新建了一个实例并返回。
2.2 实现依赖注入
前面已经完成了容器的基础功能,但只有基础功能并不能称之为IoC容器,因此还需要给容器添加注入的能力。容器在装配Bean的过程需要对当前Bean实例本身进行依赖检查,看看它有没有对其他对象有依赖,而整个检查的过程是向下递归的。举个例子,容器在构建A对象时,需要检查是否依赖的了B对象,如果存在依赖,那么回到容器中查找B实例,如果此时B实例未创建,那么就根据定义的Scope来构建B实例(如果是单例会保存到容器中),接着对B实例进行依赖检查,看看是否依赖的C对象,如果存在依赖,那么又回到容器中查找C,以此一直往下递归检查,最后将C对象赋值到B对象中,再把B对象赋值到A对象中,直到完成所有对象的依赖装配。
2.2.1 注入的形式
既然依赖注入是容器对实例检查并赋值的一个过程,那么我们在开发的过程中需要告知容器可以通过哪些形式来注入。但不管哪种形式,目的都是为容器提供一个赋值的入口,容器会在装配Bean实例的时候通过这些入口将需要注入的对象传入进来。常见的注入形式分为以下几种:
- 构造方法注入
- set方法注入
- 接口注入
- 字段注入
为了简单起见,文章中只实现基于注解的set方法注入和字段注入两种方式。
2.2.2 定义@Inject注解
如果注入的形式有多种,怎么告诉容器使用哪种方式注入呢?其实我们同样可以使用一个自定义的注解来进行配置,例如下面的代码。
@Component("worker")
public class Worker {
private Tools tools;
@Inject("wrench")
public void setTools(Tools tools){
this.tools = tools;
}
}
当容器解析到set方法上有@Inject注解时,表示通过调用set方法来注入一个在容器中标识为wrench的Bean实例。当然,这个注解同样可以标注在字段上,那么解析到此字段的时候通过容器查找标识为wrench的Bean实例并赋值给当前的字段。
@Component("worker")
public class Worker {
@Inject("wrench")
private Tools tools;
public void setTools(Tools tools){
this.tools = tools;
}
}
那么下面我们先自定义@Inject的注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Inject {
String name();
}
@Target({ElementType.FIELD, ElementType.METHOD})表示此注解可以同时标注在字段和方法上。
2.2.3 定义抽象注入处理器
为什么需要抽象的注入接口?因为字段注入和set方法注入的实现过程是不一样(将来可能还会扩展构造方法注入),因此这里定义一个抽象的注入器InjectHandler。代码如下。
public interface InjectHandler {
// 抽象注入行为,便于不同的注入实现,例如字段注入或方法注入
void handle(Object target, Class<?> targetClass, BeanFactory factory);
}
handle方法的第一个参数是被注入对象的实例,例如A需要注入一个B,而target指的就是A的实例。第二个参数是被注入对象的Class对象,也就是A的Class。第三个参数是Bean容器的实例。这个参数非常关键,因为当A需要注入B的时候,那么需要从容器中获取B实例,这是注入非常关键的一步。
2.2.4 实现set方法和字段注入
接下来我们看看字段和set方法注入的具体实现。
- 实现字段注入
public class FieldInjectHandler implements InjectHandler {
public void handle(Object target, Class<?> targetClass, BeanFactory factory) {
// 遍历当前类中的字段
for (Field field : targetClass.getDeclaredFields()) {
// 判断字段是否定义了@Inject注解类型
if (field.isAnnotationPresent(Inject.class)) {
// 获取该属性上的Inject注解
Inject annotation = field.getAnnotation(Inject.class);
// 根据注解name属性的值,从容器获取bean实例
Object property = factory.getBean(annotation.name());
// 给当前的field属性赋值(注入)
injectField(field, target, property);
}
}
}
private void injectField(Field field, Object target, Object property) {
try {
//打开访问开关
if(!field.isAccessible()) {
field.setAccessible(true);
}
//给字段赋值
field.set(target, property);
} catch (IllegalAccessException e) {
throw new RuntimeException("Field inject fail.", e);
}
}
}
- 实现set方法注入
public class MethodInjectHandler implements InjectHandler {
public void handle(Object target, Class<?> targetClass, BeanFactory factory) {
try {
BeanInfo beanInfo = Introspector.getBeanInfo(targetClass,
Object.class);
PropertyDescriptor[] propertyDescriptors = beanInfo
.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
targetClass.getDeclaredField(propertyDescriptor.getName());
//获取属性描述符的set方法
Method setMethod = propertyDescriptor.getWriteMethod();
//判断set方法上是否标注了@Inject注解
if (setMethod != null && setMethod.isAnnotationPresent(Inject.class)) {
// 获取该方法上的Inject注解
Inject annotation = setMethod.getAnnotation(Inject.class);
// 根据注解name属性的值,从容器获取bean实例
Object property = factory.getBean(annotation.name());
// 回调set方法将property注入
setMethod.invoke(target, property);
}
}
} catch (Exception e) {
new RuntimeException("Set method inject fail.", e);
}
}
}
2.2.5 编写InjectHandlerInvoker类
在一个类中可能同时出现一个或多个Field注入和set方法注入的情况,例如下面代码片段中wrench是通过Field注入的,而pliers则是通过set方法注入。
@Component("worker")
public class Worker {
@Inject("wrench")
private Tools wrench;
private Tools pliers;
@Inject("pliers")
public void setPliers(Tools pliers){
this.pliers = pliers;
}
}
如何同时满足这些不同的注入方式呢?因此在InjectHandlerInvoker类中可以维护所有的注入实现类,并统一批量调用所有的InjectHandler实现来完成不同形式的注入。
public class InjectHandlerInvoker {
private static List<InjectHandler> handlers = new ArrayList<>();
/**
* 初始化注入处理器
*/
static {
handlers.add(new FieldInjectHandler());
handlers.add(new MethodInjectHandler());
}
/**
* 执行注入操作
* @param bean 被注入的bean实例
* @param targetClass 被注入的Bean的class
* @param factory 容器
* @return
*/
public static Object inject(Object bean, Class<?> targetClass,BeanFactory factory) {
for(InjectHandler handler : handlers){
handler.handle(bean, targetClass, factory);
}
return bean;
}
}
2.2.6 将注入功能合并到容器中
实现了依赖注入的基本功能之后,最后一步就是要将注入功能集成到之前编写好的容器中。这里我们首先要考思考一个问题,容器应该在什么时候对Bean实例进行依赖装配。在前面编写的BeanFactory的代码中我们得知容器在构建Bean实例时分两种形式,一种是以singleton的方式创建,并且在创建容器时一并将所有的单例构建完成并放入容器中。第二种是以prototype的方式创建,并且是在调用getBean的时候才进行构建。因此针对这两种方式我们可以将依赖注入的动作分别在不同的构建周期中来进行。
2.2.7 改造BeanFactory
对于singleton的情况,可以在BefanFactory执行initSingleton方法之后就对所有单例进行依赖装配。修改BeanFactory新增assemblySingletons方法,代码如下:
/**
* 为初始化的singleton实例执行依赖注入
*/
private void assemblySingletons() {
for (String beanName : singletonMap.keySet()) {
Class<?> beanClass = definitionMap.get(beanName).getBeanClass();
Object bean = singletonMap.get(beanName);
InjectHandlerInvoker.inject(bean, beanClass, this);
}
}
接着在BeanFactory构造方法中调用assemblySingletons方法,代码如下:
public BeanFactory(String path) {
Set<String> classNames = ScanUtil.scan(path);
//初始化原型
initDefinitionMap(classNames);
//初始化单例
initSingleton();
//执行singleton实例装配
assemblySingletons();
}
对于prototype的情况,可以在调用doGetBean方法时进行依赖装配。修改BeanFactory新增assemblyPrototype方法,代码如下:
/**
* 为prototype实例执行装配
*/
protected Object assemblyPrototype(BeanDefinition definition){
Object bean = newInstance(definition);
InjectHandlerInvoker.inject(bean, definition.getBeanClass(), this);
return bean;
}
最后修改doGetBean的方法,代码如下:
private Object doGetBean(String beanName) {
BeanDefinition definition = definitionMap.get(beanName);
if("singleton".equals(definition.getScope())){
return singletonMap.get(beanName);
}
return assemblyPrototype(definition);
}
注意,在调用assemblySingletons或者assemblyPrototype方法时,这里会产生递归。因为在FieldInjectHandler和MethodInjectHandler执行注入的过程中需要调用BeanFactory的doGetBean方法从容器中查找需要注入的实例,接着继续对查找出来的对象执行依赖检查和装配的过程。
修改后的BeanFactory代码:
public class BeanFactory {
/**
* 存放bean的描述
*/
final Map<String, BeanDefinition> definitionMap = new ConcurrentHashMap<>();
/**
* 存放单例bean的实例
*/
final Map<String, Object> singletonMap = new ConcurrentHashMap<>();
/**
* 在构造方法中初始化并构建所有bean描述
* 以及单例的bean
*
* @param path 扫描路径
*/
public BeanFactory(String path) {
Set<String> classNames = ScanUtil.scan(path);
//初始化原型
initDefinitionMap(classNames);
//初始化单例
initSingleton();
//执行singleton实例装配
assemblySingletons();
}
/**
* 根据扫描的类名进行解析,找出带有@Component注解的类,并构建成
* BeanDefinition实例,保存到definitionMap集合中
*/
private void initDefinitionMap(Set<String> classNames) {
for (String className : classNames) {
Class<?> beanClass = getClass(className);
//检查beanClass是否标注了@Component注解
if (beanClass.isAnnotationPresent(Component.class)) {
//获取@Component注解的value属性的值,这个值作为bean在容器的唯一标识
String beanName = beanClass.getAnnotation(Component.class).value();
//如果容器已经存在bean,则抛出异常
if (definitionMap.containsKey(beanName)) {
throw new RuntimeException(
"conflicts with existing, non-compatible bean definition of same name and class ["
+ beanClass + "]");
} else {
definitionMap.put(beanName,
createBeanDefinition(beanClass));
}
}
}
}
/**
* 根据权限顶类名获取Class对象
*
* @param className
* @return
*/
private Class<?> getClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Can not find the class name " + className + " to build the description.");
}
}
/**
* 构建bean描述定义,将bean的scope以及类名封装到BeanDefinition中
* 创建的Bean描述会放入definitionMap的集合中保存
* Bean的类名作为集合的key,而整个BeanDefinition对象作为value
*
* @param beanClass
*/
private BeanDefinition createBeanDefinition(Class<?> beanClass) {
// 创建BeanDefinition
BeanDefinition definition = new BeanDefinition();
//设置Bean的Class对象
definition.setBeanClass(beanClass);
//设置Bean的作用域
definition.setScope(resolveScope(beanClass));
return definition;
}
/**
* 解析Scope,如果bean的class上指定了Scope注解,则将@Scope的value属性值作为Bean的创建方式
* 否则Bean的默认创建方式将使用单例
*/
private String resolveScope(Class<?> beanClass) {
String scope = (beanClass.isAnnotationPresent(Scope.class)) ? beanClass
.getAnnotation(Scope.class).value() : "singleton";
return scope;
}
/**
* 初始化SINGLETON实例放入bean容器中
*/
private void initSingleton() {
for (String beanName : definitionMap.keySet()) {
BeanDefinition definition = definitionMap.get(beanName);
if ("singleton".equals(definition.getScope())) {
Object bean = newInstance(definition);
singletonMap.put(beanName, bean);
}
}
}
/**
* 为所有singleton实例执行装配(依赖注入)
*/
private void assemblySingletons() {
for (String beanName : singletonMap.keySet()) {
Class<?> beanClass = definitionMap.get(beanName).getBeanClass();
Object bean = singletonMap.get(beanName);
InjectHandlerInvoker.inject(bean, beanClass, this);
}
}
/**
* 为prototype实例执行装配
*/
protected Object assemblyPrototype(BeanDefinition definition){
Object bean = newInstance(definition);
InjectHandlerInvoker.inject(bean, definition.getBeanClass(), this);
return bean;
}
/**
* 根据描述定义创建Bean实例
* @param definition
* @return
*/
private Object newInstance(BeanDefinition definition) {
try {
return definition.getBeanClass().newInstance();
} catch (InstantiationException e) {
throw new RuntimeException("Create bean instance fail.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Create bean instance fail.", e);
}
}
/**
* 获取bean实例
*
* @param beanName
* @return
*/
public Object getBean(String beanName) {
return doGetBean(beanName);
}
/**
* 获取bean实例(泛型)
*
* @param beanName
* @param clazz
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getBean(String beanName, Class<T> clazz) {
return (T) doGetBean(beanName);
}
/**
* 从容器中获取Bean的BeanDefinition
* 如果Bean的BeanDefinition的scope为singleton,则从singletonMap中获取单例
* 否则装配原型并返回
*/
private Object doGetBean(String beanName) {
BeanDefinition definition = definitionMap.get(beanName);
if("singleton".equals(definition.getScope())){
return singletonMap.get(beanName);
}
return assemblyPrototype(definition);
}
}
2.2.8 综合测试
public abstract class Tools {
public abstract void repair();
}
@Component("wrench")
public class Wrench extends Tools{
@Override
public void repair() {
System.out.println("Use wrench repairing...");
}
}
@Component("pliers")
public class Pliers extends Tools {
@Override
public void repair() {
System.out.println("Use pliers repairing...");
}
}
@Component("worker")
public class Worker {
/**
* 字段注入
*/
@Inject(name = "wrench")
private Tools wrench;
private Tools pliers;
/**
* set方法注入
*/
@Inject(name = "pliers")
public void setPliers(Tools pliers) {
this.pliers = pliers;
}
public void useWrench(){
wrench.repair();
}
public void usePliers(){
pliers.repair();
}
}
测试:
public class Main {
public static void main(String[] args) {
BeanFactory beanFactory = new BeanFactory("edu.demo");
Worker worker = beanFactory.getBean("worker", Worker.class);
worker.useWrench();
worker.usePliers();
}
}
运行结果:
Use wrench repairing...
Use pliers repairing...
3. 结束语
本文只实现了一个极度简化版本的IoC容器,并不适用于生产环境,其目的是为了清楚了解IoC的核心机制以及实现思路。如果希望有更深层次的了解,建议各位读者可以阅读spring-framework源码,里面会有更多你想要的答案。
网友评论