最近在看Spring
的源码,对依赖xml
配置文件的IOC
容器的实现部分阅读起来比较吃力。主要是因为自己对IOC实现机制不是很了解,于是萌生了按其原理实现一个简单的IOC
容器,来以便自己对Spring
的 IOC
实现能有更深入的了解。
什么是 IOC
IOC
是英文控制反转的缩写,是一种设计思想。对于Spring
框架来说,IOC
就是由 Spring
来负责对象的生命周期和对象间的关系。以前需要一个对象,就得 new
一个来保持对象的引用,对象耦合严重。而 Spring IOC
容易就统一负责对象的关系,动态地向某个对象提供它所需要的其它对象。
实现步骤
实现一个 IOC
需要哪些步骤呢,我把它总结为了下面几点。用过 Spring
的都知道,Spring
是用xml
配置文件来配置 bean
的。所以首先是要读取和解析 xml
配置文件来生成 bean 实例。
1. 读取 xml
配置文件。Spring
框架是采用 dom4j
框架来读取 xml
文件。它将 xml 文件生成 Document
对象,xml 配置属性在 Document 对象中都能找到,xml 的配置属性和 Document
对象的属性是一一对应的。
2. 加载Document
对象中的 Element。什么是 Element,这么来说吧,xml 文件中<bean> </bean>
是一个 Element
,<bean>
下面的 <constructor-arg>
也是一个Element
,xml
中每一个配置属性都是用 Document
对象中的 Element
来表示。
3. 解析Element
。将加载的 Element
属性进行解析。比如解析<bean>
的 class
属性值,就可以找到这个 bean
类的位置,并生成他 的对象实例。
4. 生成实例并缓存。根据第三步解析到的数据生成 bean
实例,并缓存。
5. 注入属性。根据第三步解析到属性对 bean
实例进行属性注入。
最后实现一个总的容器类将前几步整合起来,并对外提供获取 bean
实例的接口。现在按照这个步骤依从实现各部分的功能。
读取 xml 配置文件。
配置一个接口类,该接口类有一个 getDocument(String path)
方法。通过传入 xml
文件的位置路径来生成 Document
对象。
// XML 文件读取接口类
public interface DocumentHolder {
Document getDocument(String filePath);
}
来看看该接口的具体实现方法。
public class XMLDocumentHolder implements DocumentHolder {
//建立一个HashMap用来存放字符串和文档
private Map<String, Document> docs = new HashMap<String, Document>();
@Override
public Document getDocument(String filePath) {
Document doc=this.docs.get(filePath);//用HashMap先根据路径获取文档
if (doc==null) {
this.docs.put(filePath, readDocument(filePath)); //如果为空,把路径和文档放进去
}
return this.docs.get(filePath);
}
/**
* 根据路径读Document
* @param filePath
* @return
*/
private Document readDocument(String filePath) {
Document doc =null;
try {
SAXReader reader = new SAXReader(true);//借用dom4j的解析器
reader.setEntityResolver(new IoCEntityResolver());
File xmlFile = new File(filePath); //根据路径创建文件
doc = reader.read(xmlFile);//用dom4j自带的reader读取去读返回一个Document
} catch (Exception e) {
e.printStackTrace();
}
return doc;
}
}
这里面的 IoCEntityResolver
是自己实现的类。这个类是干嘛的呢?还是先介绍一下xml
配置文件的命名空间吧,了解了xml
文件的命名空间,就明白这个类是干什么的了。下面是Spring xml
文件两种配置头。
// xsd 方式验证xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
命名空间相当于java
中的包,xsd
方式中 xmlns
相当于一个保留字,用来申明一个命名空间。上面就申明了别名为 xsi、context、aop
和tx
命名空间,别名后面的url包含了该命名空间的信息。申明了这些命名空间,就可以用该命名空间的元素了。比如上面申明了aop
命名空间,就可以 用<aop:config>
来配置切面了。Spring
使用 xml
配置 bean
信息,在解析 xml
文件时,首先就要验证 xml 文件的正确性。当 xml 文件使用了命名空间中不存在的元素或者使用方式有问题,xml
验证就会报错。那么怎么验证呢?通常一个命名空间的URL 对应着一个 xsd
地址,如上面的 xsi:schemaLocation
所示,所以xsi:schemaLocatio
后面的 URL
都是成对的。通过这个xsd
可以获取到 xsd
文件,xsd
文件包含着该对应命名空间所有的使用规则。所以 xml
验证怎么验证呢,就是通过这些 xsd
文件来验证其相当应命名空间元素使用正确性。
// dtd 方式验证 xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//CRAZYIT//DTD BEAN//EN" "http://www.crazyit.org/beans.dtd">
<beans>
</beans>
上面是 dtd
方式验证xml
文件。申明了beans 命名空间,该命名空间后面包括两部分,分别是PUBLICID
和 SYSTEID
。如上面所示,PUBLICID
是-//CRAZYIT//DTD BEAN//EN
,SYSTEMID
是 http://www.crazyit.org/beans.dtd
。SYSTEMID
的 URL
定义了dtd
文件的位置,跟 xsd
中的 schemaLocation
的URL
一样。这个 DTD
文件功能和 xsd
是一样的,都包含着该对应命名空间所有的使用规则,只是验证xml
文件方式有差异而言。不管是 xsd
方式还是dtd
方式,为了保证本地没联网的时候能获取到xsd
和dtd
文件,Sping
在本地包就保存了各个版本的 xsd
和dtd
文件,当本地获取不到的时候,才通过URL
联网获取。所以 IoCEntityResolver
这个类干嘛呢,就是指定本地 xsd
或者 dtd
文件的位置,让 验证 xml
的时候直接从本地读取。IoCEntityResolver
类的实现如下:
public class IoCEntityResolver implements EntityResolver {
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if ("http://www.crazyit.org/beans.dtd".equals(systemId)){
InputStream stream = IoCEntityResolver.class.getResourceAsStream("spring-beans-2.0.dtd");
return new InputSource(stream);
} else {
return null;
}
}
}
这里采用了 dtd
的验证方式。所以我的xml
配置文件如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//CRAZYIT//DTD BEAN//EN" "http://www.crazyit.org/beans.dtd">
<beans>
<bean id="category" class="com.kdk.action.domain.Category">
<constructor-arg>
<value type="java.lang.Integer">552</value>
</constructor-arg>
<constructor-arg>
<value type="java.lang.String">code</value>
</constructor-arg>
</bean>
<bean id="book" class="com.kdk.action.domain.Book" autowire="byName">
<constructor-arg>
<value type="java.lang.String">kdk</value>
</constructor-arg>
<constructor-arg>
<value type="java.lang.String">c study</value>
</constructor-arg>
</bean>
</beans>
当SYSTEMID
为http://www.crazyit.org/beans.dtd
时,就从本地获取 dtd
文件,我本地 dtd
文件就直接采用 spring
提供的,并没有自定义。自此,就可以将我的xml
文件生成 Document
对象了。
加载 Element 元素
获取到 Document
对象后,接下来就可以加载 Document
中的Element
元素了。定义加载 Element
的接口。
public interface ElementLoader {
//加载 Doucument 中所有的 Element 并缓存在本地
void addElements(Document doc);
//根据 Element 的 id 来获取 Element
Element getElements(String id);
//返回 Document 中所有的 Element
Collection<Element> getElements();
}
按照接口的定义,依次实现接口的方法。
public class ElementLoaderImpl implements ElementLoader {
private Map<String,Element> elements = new HashMap<>();
@Override
public void addElements(Document doc) {
List<Element> eles = doc.getRootElement().elements();
for (Element e : eles){
String id = e.attributeValue("id");
elements.put(id,e);
}
}
@Override
public Element getElements(String id) {
return elements.get(id);
}
@Override
public Collection<Element> getElements() {
return elements.values();
}
}
加载Element
元素比较简单,利用 Document
提供的方法就可以获取所有的根 Element
,并将其缓存在 Map
中。
解析 Element
这个简单的IOC
容器,我只实现了对 bean Element
的部分属性的解析,但是了解这部分的解析,自然也就明白其它属性的解析原理了。 Element 解析接口如下:
public interface ElementReader {
//是否懒加载
boolean isLazy(Element element);
//获得bean元素下面的constructor-arg
List<Element> getConstructorElements(Element element);
//获取属性为name的属性值
String getAttribute(Element element,String name);
boolean isSingleton(Element element);
//获得一个bean元素下面的所有的property元素
List<Element> getPropertyElements(Element element);
//获得bean元