依赖注入容器

  |  

摘要: 本文主要是关于依赖注入容器的原理和应用。依赖注入容器同时也是 Spring 的一个核心功能。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


问题背景

在文章 Java接口作为模块间通信的协议 中,我们实现了一个模拟计算机组装的例子。

主类的代码如下:

1
2
3
4
5
6
7
8
9
10
package computer;

public class Computer {
public static void main(String[] args) {
Mainboard mv = new MainBoard();
mb.setCpu(new InterCPU());
mb.setGraphicsCard(new NVIDIACard());
mb.run();
}
}

上面的代码中,主要工作是主板对象的 run 方法,但是主板对象依赖 CPU/GPU 两个对象。具体做法是创建实现了 CPU/GPU 接口的对象,然后通过 setCpu/setGraphicsCard 方法将对象注入进去。

如果要更换一个 CPU,那么需要新编写一个类实现 CPU 接口,例如 AMDCPU,然后修改 Computer 类的代码,创建一个 AMDCPU 的对象,调用 Mainboard 对象的 setCpu 方法,将新的 CPU 对象注入进去,重新编译 Computer 类。


工厂类或者装配器类负责对象组装

为了避免上面这种因依赖关系发生变化而导致的程序修改,可以把依赖关系剥离出来,交给一个工厂类或者装配器类来负责对象组装,工厂类根据属性文件的配置动态创建接口的对象,然后将对象传递给 Mainboard。此时的工厂类就是依赖注入容器类

将原有的代码文件赋值到 ioc 目录下,此时 ioc 目录的文件如下:

1
2
3
4
5
CPU.java           
GraphicsCard.java
Mainboard.java
IntelCPU.java
NVIDIACard.java

下面我们增加主类文件、属性文件、工厂类文件,如下:

1
2
3
Computer.java        
computer.properties
MainboardFactory.java

(1) 属性文件

  • ioc/computer.properties
1
2
cpu=computer.IntelCPU
grahicsCard=computer.NVIDIACard

(2) 工厂类

  • ioc/MainboardFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package computer;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import java.lang.reflect.InvocationTargetException;

public class MainboardFactory {
public static Mainboard getMainboard() {
Mainboard mb = new Mainboard();
try {
FileInputStream in = new FileInputStream("computer.properties");
Properties pps = new Properties();
pps.load(in);

String cpuClassName = pps.getProperty("cpu");
String gCardClassName = pps.getProperty("graphicsCard");

Class<?> cpuCl = Class.forName(cpuClassName);
Class<?> gCardCl = Class.forName(gCardClassName);

if(CPU.class.isAssignableFrom(cpuCl)) {
CPU cpu = (CPU) cpuCl.getConstructor().newInstance();
mb.setCpu(cpu);
}

if(GraphicsCard.class.isAssignableFrom(gCardCl)) {
GraphicsCard gCard = (GraphicsCard) gCardCl.getConstructor().newInstance();
mb.setGraphicsCard(gCard);
}
} catch ( IOException
| ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InstantiationException
| InvocationTargetException e) {
e.printStackTrace();
}
return mb;
}
}

其中用到了 Class 类的 isAssignableFrom 方法:

1
public boolean isAssignableFrom(Class<?> cls)

判定当前 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。如果是,则返回 true,否则返回 false。用于判定 cls 所表示的类型能否转换为当前 Class 对象所表示的类型。

(3) 主类

  • ioc/Computer.java
1
2
3
4
5
6
7
8
package computer;

public class Computer {
public static void main(String[] args) {
Mainboard mb = MainboardFactory.getMainboard();
mb.run();
}
}

此时主类中所有的依赖关系都不存在了,以后在更换 CPU 或 GraphicsCard,就不用修改代码了。

编译和运行

1
2
javac -d .*.java
java computer.Computer

结果如下:

1
2
3
Starting computer...
Intel CPU calculate.
Display something.

依赖注入容器类

使用了工厂类的代码的可重用性、可扩展性都满足了。

现在想更进一步:能否将 MainboradFactory 改造成一个通用的依赖注入容器类,可以动态创建对象,并自动建立对象之间的依赖关系

(1) 反射 API

反射 API 可以创建对象,可以调用方法,在功能上能满足需求。但是需要知道对象的类名、构造方法名和方法名,以及调用方法时传递的参数,还有对象之间的依赖关系如何表述

(2) XML

因此需要定义一种数据格式来存储上述的信息。属性文件的格式比较单一,不能很好地表述结构型的数据,XML 文档可以很好地表述层次型、结构型的数据。

bean 元素

我们可以用 <bean> 元素来表示一个类的信息,属性 id 表示类的唯一标识,它的值可以作为保存创建的对象的 key,属性 class 表示类的完整限定名。例如:

1
2
3
<bean id="IntelCPU" class="computer.IntelCPU">
...
</bean>

constructor 子元素

<bean> 的子元素 <constructor> 表示构造方法,<constructor> 的子元素 <value> 表示要向构造方法传递的参数值,如果有多个参数,就使用多个 <value> 子元素;如果没有参数,就不使用元素。例如:

1
2
3
4
5
<bean id="IntelCPU" class="computer.IntelCPU">
<constructor>
<value>...</value>
</constructor>
</bean>

property 子元素

<bean> 的子元素 <property> 表示类中的属性(即去掉 set/get 后将首字母小写的名字)。

<property> 元素的属性 name 表示属性名,<property> 的子元素 <value> 表示调用 setXxx 方法传入的参数值。

对于依赖关系,使用 <ref> 元素来表示,用 bean 属性指定依赖的类的 id 值。例如:

1
2
3
4
5
6
7
8
<bean id="mainboard" class="computer.Mainboard">
<property name="cpu">
<ref bean="intelCPU"/>
</property>
<property name="xxx">
<value>...</value>
</property>
</bean>

唯一根元素 beans

XML 文档需要有一个唯一的根元素,我们可以用 <beans> 元素作为根元素,包裹所有的 <bean> 子元素。

在设计好 XML 文档的数据格式后,针对计算机组装的例子,XML 配置文件如下:

  • ioc/beans.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>

<beans>
<bean id="intelCPU" class="computer.IntelCPU"/>
<bean id="nvCard" class="computer.NVIDIACard"/>

<bean id="mainboard" class="computer.Mainboard">
<property name="cpu">
<ref bean="intelCPU"/>
</property>
<property name="graphicsCard">
<ref bean="nvCard"/>
</property>
</bean>
</beans>

(3) dom4j

开源的 XML 框架 dom4j,可以方便地解析 XML 文档。详细信息看 官网

(4) 解析 beans.xml 的依赖注入容器类

解析 beans.xml,使用反射 API 创建类的对象,并根据配置的依赖关系装配对象。代码细节见注释。

  • ioc/BeanFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package computer;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

public class BeanFactory {
// 用于保存创建的对象的 Map,key 是配置文件中的 id 值,value 是对象
private Map<String, Object> beans = new HashMap<String, Object>();

/**
* BeanFactory 的构造方法,解析 XML,装配对象
*/
public BeanFactory(String fileName) {
// dom4j 的 SAX 解析器对象
SAXReader saxReader = new SAXReader();
// 得到类加载器
ClassLoader clsLoader = this.getClass().getClassLoader();
// 得到用于读取配置文件的输入流
InputStream is = clsLoader.getResourceAsStream(fileName);
try {
// 构建 dom4j 树
Document doc = saxReader.read(is);
// 得到 XML 文档的根元素
Element root = doc.getRootElement();
// 得到所有的 <bean> 元素
List<Element> beanList = root.elements("bean");
// 循环解析 <bean> 元素
for(Element beanElt: beanList) {
// 得到 <bean> 元素 id 属性的值
String id = beanElt.attributeValue("id");
// 得到 <bean> 元素 class 属性的值
String className = beanElt.attributeValue("class");
Class<?> cls = Class.forName(className);
// 得到 <bean> 元素所有 <constructor> 子元素
List<Element> consList = beanElt.elements("constructor");
// 如果不存在 <constructor> 子元素,则用默认构造方法创建对象
if(consList.isEmpty()) {
Object obj = cls.getConstructor().newInstance();
// 以 <bean> 元素的 id 属性为 key,对象为 value
beans.put(id, obj);
} else {
// 如果 <bean> 下存在 <constructor> 子元素,则找对应的构造方法
int i = 0;
Class[] argsCls = new Class[consList.size()];
Object[] args = new Object[consList.size()];
for(Iterator it = consList.iterator(); it.hasNext(); i++) {
Element consElt = (Element) it.next();
argsCls[i] = String.class;
args[i] = consElt.element("value").getText();
}
Constructor cons = cls.getConstructor(argsCls);
Object obj = cons.newInstance(args);
beans.put(id, obj);
}
// 查看 <bean> 下是否有 property 子元素
List<Element> propList = beanElt.elements("property");
// 如果有,则准备调用 setXxx 方法,传入依赖的对象
for(Element propElt: propList) {
String name = propElt.attributeValue("name");
StringBuffer sb = new StringBuffer();

// 拼接方法名:set + name
sb.append("set")
.append(name.substring(0, 1).toUpperCase())
.append((name.substring(1)));

// 得到依赖的对象的 id 值
String objName = propElt.element("ref").attributeValue("bean");

// 从 Map 中取出对象
Object obj2 = beans.get(objName);
// 得到 setXxx 方法对应的 Method 对象
Method mth = cls.getMethod(sb.toString(), obj2.getClass().getInterfaces()[0]);
// 调用 setXxxx 方法,传入以来的对象
mth.invoke(beans.get(id), obj2);
}
}
} catch ( DocumentException
| ClassNotFoundException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException e) {
e.printStackTrace();
}
}

/**
* 根据配置文件中 <bean> 元素 id 属性的值,从 Map 中得到对象
*/
public Object getBean(String name) {
return beans.get(name);
}
}

主类方法中通过 BeanFactory 得到 Mainboard 对象

修改 Computer 类 main 方法的实现,使用 BeanFactory 得到 Mainboard 对象。

  • ioc/Computer.java
1
2
3
4
5
6
7
8
9
package computer;

public class Computer {
public static void main(String[] args) {
BeanFactory bf = new BeanFactory("beans.xml");
Mainboard mb = (Mainboard) bf.getBean("mainboard");
mb.run();
}
}

编译和运行

由于 BeanFactory 使用了 dom4j,所以在编译程序时,需要通过 CLASSPATH 环境变量配置一下 dom4j 的 JAR 文件的路径。

设置 CLASSPATH 环境变量的值为当前目录和 dom4j-2.1.3.jar 的完整路径(以分号作为分隔),如下:

1
2
3
export CLASSPATH=.:/home/ppp/codes/java/corejava/ioc/dom4j-2.1.3.jar
javac -d . *.java
java computer.Computer

结果如下:

1
2
3
Starting computer...
Intel CPU calculate.
Display something.

此后,可以给出 CPU 和 GraphicsCard 接口的任意实现,只需要在 beans.xml 文件进行配置就可以了,程序代码不需要做任何的改动。

BeanFactory 是通用的,可以应用于任何类对象的依赖注入。


Share