本身动手编写IOC框架(一)

  博客建立了2年多了,一直没敢写点东西,怕技术不够误导了别人。2年多后的今天我已经颇有信心可以为须要帮助的人作点微薄的贡献了。这是我第一次写博客,先自我介绍一下。本人网名泪滴,一个很是伤心的名字,生活中除了代码一无全部,平时喜欢看开源框架的源码,今天也为开源贡献一份本身的力量。java

  此次项目叫作IOC框架,是根据spring的IoC的使用风格使用本身的代码实现。项目的目的不是为了推销个人框架,只是为了让目前正在使用IoC和将要使用IoC的小伙伴们对IoC有一个全新的认识,相信泪滴,我不会浪费大家宝贵的时间,无论你是新手仍是大牛都会从中有所收获。因为我有朝九晚五的工做,精力有限,因此本次项目采用连载的方式,尽可能保证每周至少2,3次更新,每次更新至少解决一个阶段性的问题。废话我就很少说了,下面开始正题。web

  IoC框架,估计使用过java,使用过spring的人都不会陌生,它就是一个依赖注入的功能,通俗的说就是能够在项目或者服务启动使用的时候给咱们一次性准备好咱们程序中配置好的所须要的对象,而且自动将对象set到须要的对象中。例如A类中的成员变量中有一个B类的引用,那么生成对象的时候会生成一个A的对象和B的对象,而且将B的对象经过A的构造方法或者set方法将B对象设置到A对象中。其实看来完成的功能很简单就是为了让咱们在代码中减小使用new关键字的次数,让大多数程序中的new的性能开销和代码开销集中在项目启动或者服务启动的时候。在程序的编写过程当中不会一次又一次的写A a = new A()这种没有养分的代码,在程序运行的时候不会由于屡次的new对象浪费时间。spring

  注意上面咱们提到的IoC的功能咱们能够发现几个很关键的地方。第一:该框架产生的动做就是生成对象,第二:生成了对象还要考虑该对象是否是须要依赖别的对象,若是须要要给他set好须要的对象,第三:前两个动做产生的时机通常就是项目启动或者服务启动的时候。这时候咱们是否是大脑中已经有一个很简单的想法了,咱们将项目中所须要的生成的对象所有配置在配置文件中,而后在main方法中或者是web中的监听器Listener调用一个方法去读取配置文件而后将他们的对象所有生成出来。至于配置文件采用什么格式的配置文件也是须要考虑的地方,因为有些对象须要依赖其余对象,并且依赖的对象个数也不必定,也有可能A依赖B,B又依赖C...这种层级关系栩雅保存起来,放眼配置文件类型,可以达到保存这种层级关系的文件很明显就能够用xml文件。至于怎么生成对象又是一个须要考虑的问题,因为咱们要生成哪些对象是从配置文件中读取出来的,都出来后对象的表示确定都是一个个字符串了,很显然咱们不能使用new去生成对象了。java中什么技术可使用字符串表示的类生成对象的,显然Class.forName(className)这个做为反射入口的方法是彻底知足要求的。tomcat

  通过上面的分析是否对IoC的实现有了一个大概的想法。咱们再联想使用spring框架的IoC的流程,咱们看看以下码:服务器

/**首先加载配置文件并解析放到ApplicationContext中*/
ApplicationContext ac = new FileSystemXmlApplicationContext("applicationContext.xml");
/**从ApplicationContext中get出来id为“beanId”的对象*/
ac.getBean("beanId");

可能有人在说本身在写web代码的时候没有写这种代码,可是大家集成spring到web中的是否配置过一个监听器Listener呢,而后启动tomcat那样的服务器的时候是否是会发现日志中或多或少会打印出一些生成对象的日志呢,难道这些代码他不能放到那个监听服务器启动的Listener中去吗。若是不信个人话能够去看看那个Listener的源码就知道了。退一万步讲,spring若是之后哪一个版本采用了什么高大上的技术,让你看不到这些代码了。可是毫无疑问做为咱们本身编写的IoC这种策略是彻底没有任何问题的。app

  上面分析了这么久,咱们或多或少对于IoC框架该怎么实现都有了各自的方案了。下面咱们结合spring框架的使用方式开始咱们本身的IoC的框架之旅吧。首先spring框架抛开注解方式先不说,咱们都是将须要生成的对象以bean的方式配置在xml文件中。说到xml咱们就会想到xml格式的验证是须要dtd或者schema的,spring中xml文件的验证使用的是schema,因为schema的学习成本比dtd大,dtd又彻底能够知足咱们框架的要求,因此咱们的IoC选用dtd文件做为校验xml的文件。结合spring配置文件中涉及到的标签元素咱们定义的dtd文件以下:框架

<!--指定xml文档的根元素为beans,beans里面能够有多个子元素bean,仿照spring -->
<!ELEMENT beans (
    bean*
)>
<!--指定根元素beans的两个属性,一个是延迟加载,一个是自动装配,默认为后面的值 -->
<!ATTLIST beans default-lazy-init (true | false) "false">
<!ATTLIST beans default-autowire (no | byName) "no">
<!-- 指定bean元素的子元素 -->
<!ELEMENT bean (
    (constructor-arg | property)*
)>
<!-- 指定bean元素的属性值 ,这些属性和spring里面的相似-->
<!ATTLIST bean id CDATA #REQUIRED>
<!ATTLIST bean class CDATA #REQUIRED>
<!ATTLIST bean lazy-init (true | false | default) "default">
<!ATTLIST bean singleton (true | false) "true">
<!ATTLIST bean autowire (no | byName | default) "default">
<!-- 声明constructor-arg子元素 -->
<!ELEMENT constructor-arg (
    (ref | value )
)>
<!-- 声明property元素的子元素 -->
<!ELEMENT property (
    (ref | value | collection )?
)>
<!-- 指定collection元素的子元素 -->
<!ELEMENT collection (
    (ref | value)+
)>
<!--声明collection的属性 -->
<!ATTLIST collection type CDATA #REQUIRED>
<!-- 声明property的属性 -->
<!ATTLIST property name CDATA #REQUIRED>
<!-- 声明property的属性 -->
<!ATTLIST value type CDATA #REQUIRED>
<!-- 声明ref元素 -->
<!ELEMENT ref EMPTY>
<!-- 声明ref的属性 -->
<!ATTLIST ref bean CDATA #REQUIRED>
<!-- 声明value元素 -->
<!ELEMENT value (#PCDATA)>

对于上面的dtd有了详细的注释,若是仍是以为看的吃力,小伙伴们能够本身网上找些dtd的资料话半小时彻底能够轻松搞定。这里咱们的重点不在这里。至于上面的dtd所校验下的xml文件的格式是个什么样子咱们能够联想下spring的applicationContext.xml中的内容就能知道了。这边和那个大同小异。为了方便下面代码的讲解,咱们说下此次项目的包结构dom

其中项目的根包为com.tear.ioc,bean包下面放一些项目须要使用的bean,dtd下放dtd文件,xml包下放处理xml文件的相关包或者类。下面的讲解所有是以这个包为基础进行的。ide

  dtd文件已经有了,可是咱们怎么让这个dtd去约束xml呢?又怎么解析xml呢,本来的想法个人这个IoC框架不想去依赖任何jdk之外的包的,可是后来发现jdk自带的dom和sax解析xml用起来很繁琐,无奈下引用了一个目前使用比较多的dom4j.jar包,咱们将获取xml文件的dom4j里面的document对象的代码放在xml.document包下:性能

下面咱们定义一个Document对象的持有接口DocumentHolder

package com.tear.ioc.bean.xml.document;
import org.dom4j.Document;
public interface DocumentHolder {
    /**
     * 根据xml文件的路径获得dom4j里面的Document对象
     * @param filePath
     * @return
     */
    public Document getDocument(String filePath);
}

持有接口的实现类XmlDocumentHolder以下

package com.tear.ioc.bean.xml.document;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

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

public class XmlDocumentHolder implements DocumentHolder {
    /**
     * 因为可能配置多个配置文件因此定义一个Map类型的成员变量用配置文件的路径关联他们的Document对象
     * Map的实际类型定义成了HashMap
     */
    private Map<String, Document> documents = new HashMap<String, Document>();
    
    /**
     * 根据xml文件的路径获得dom4j里面的Document对象
     * @param filePath
     * @return
     */
    @Override
    public Document getDocument(String filePath) {
        /**
         * 经过xml文件的路径获取出Map里保存的Document对象
         */
        Document doc = this.documents.get(filePath);
        /**
         * 若是根据xml文件的路径从Map中取出的Document对象为空,则调用本类里面定义的
         * readDocument方法得到该路径所对应文件的Document对象后,在将路径和Document
         * 对象这样一对信息保存到Map中去
         */
        if (doc == null) {
            //使用SAXReader来读取xml文件
            this.documents.put(filePath, this.readDocument(filePath));
        }
        /**
         * 返回Map中该xml文档路径所对应的Document对象
         */
        return this.documents.get(filePath);
    }
    /**
     * 根据文件的路径读取出Document对象,该方法是准备被下面的getDocument方法调用的
     * 因此定义成了private
     * @param filePath
     * @return
     */
    private Document readDocument(String filePath) {
        try {
            /**
             * new一个带dtd验证的SaxReader对象
             */
            SAXReader reader = new SAXReader(true);
            /**
             * 设置用来验证的dtd的输入源
             */
            reader.setEntityResolver(new XmlEntityResolver());
            /**
             * 根据xml的路径读取出Document对象
             */
            File xmlFile = new File(filePath);
            Document document = reader.read(xmlFile);
            return document;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

 使用指定dtd验证xml的resolver类XmlEntityResolver以下

package com.tear.ioc.bean.xml.document;

import java.io.IOException;
import java.io.InputStream;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
 * 自定义的XmlEntityResolver实现dom4j里的EntityResolver接口并实现里面的
 * resolveEntity方法,来得到一个dtd的输入源
 * @author rongdi
 */
public class XmlEntityResolver implements EntityResolver {

    @Override
    public InputSource resolveEntity(String publicId, String systemId)
            throws SAXException, IOException {
        /**
         * 若是本身写的xml配置文件中引入dtd的时候publicId与"-//RONGDI//DTD BEAN//CN"相同
         * 而且systemId与"http://www.cnblogs.com/rongdi/beans.dtd"相同,就从本地的相对项目的路径
         * 寻找dtd,返回一个dtd的输入源,若果找不到该dtd就会尝试到对应的网址上寻找
         */
        if ("-//RONGDI//DTD BEAN//CN".equals(publicId)&&"http://www.cnblogs.com/rongdi/beans.dtd".equals(systemId)) {
            InputStream stream = this.getClass().
            getResourceAsStream("/com/tear/ioc/bean/dtd/beans.dtd");
            return new InputSource(stream);
        } 
        return null;
    }
}

注释在代码中写的很详细了这里就很少废话了。下面使用jUnit测试一下上面获取document的代码

在test包下创建包com.tear.ioc.xml.document包和resources包,resources.document包存放须要使用的xml文件等资源

resources.document包下测试获取document对象的正确文件xmlDocumentHolder.xml以下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//RONGDI//DTD BEAN//CN" 
    "http://www.cnblogs.com/rongdi/beans.dtd">
<beans>
    <bean id="test" class="test"></bean>
</beans>

xml经不过dtd验证的文件xmlDocumentHolder2.xml以下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//RONGDI//DTD BEAN//CN" 
    "http://www.cnblogs.com/rongdi/beans.dtd">
<beans>
    <bean id="test" ></bean>
</beans>

xml中publicId写错了的xml文件xmlDocumentHolder3.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//ABABAB//DTD BEAN//CN" 
    "http://www.cnblogs.com/rongdi/beans.dtd">
<beans>
    <bean id="test" class="test"></bean>
</beans>

上面现阶段最主要关注的是DOCTYPE部分,下面的beans和bean部分属性中的内容对于现阶段随便写什么均可以。

单元测试代码以下

package com.tear.ioc.xml.document;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.tear.ioc.bean.xml.document.XmlDocumentHolder;

public class XmlDocumentHolderTest {
	private XmlDocumentHolder xmlHolder;
	@Before
	public void setUp() throws Exception {
		xmlHolder = new XmlDocumentHolder();
	}

	@After
	public void tearDown() throws Exception {
		xmlHolder = null;
	}
	//测试正常状况
	@Test
	public void testGetDocument1() {
		String filePath = "test/resources/document/xmlDocumentHolder.xml";
		//得到Document对象
		Document doc1 = xmlHolder.getDocument(filePath);
		//看是否为空,为空测试失败
		assertNotNull(doc1);
		//获得xml文档根元素
		Element root = doc1.getRootElement();
		//判断根元素是否为beans,不是beans测试失败
		assertEquals(root.getName(), "beans");
		//再获取一次Document对象,看是否一致
		Document doc2 = xmlHolder.getDocument(filePath);
		System.out.println(doc1);
		System.out.println(doc1);
		assertEquals(doc1, doc2);
	}
	//测试一个读取DTD验证不合格的xml文件看是否抛出异常
	@Test(expected = DocumentException.class)
	public void testGetDocument2(){
		/*
		 * 定义一个dtd验证不合格的xml文件,该xml文件的bean元素id和class是必须,
		 * 可是少了一个class应该抛出异常
		 */
		String filePath = "test/resources/document/xmlDocumentHolder2.xml";
		//得到Document对象
		Document doc = xmlHolder.getDocument(filePath);
	}
	//测试一个读取不到DTD的状况(DTD里面的publicId或systemId写错了没法再本地获取dtd就会
	//尝试到网上下载,可是自定义的网站根本不存在就会报错了)
	@Test(expected = DocumentException.class)
	public void testGetDocument3() throws DocumentException{
		/*
		 * 定义一个dtd验证不合格的xml文件,该xml文件的bean元素id和class是必须,
		 * 可是少了一个class应该抛出异常
		 */
		String filePath = "test/resources/document/xmlDocumentHolder3.xml";
		//得到Document对象
		Document doc = xmlHolder.getDocument(filePath);
	}
}

  

  好了对于IoC的开始部分,定义dtd即如何使用dtd校验xml的代码部分已经结束了。下次再见。 

  源码百度云地址http://pan.baidu.com/s/1sjHMT33 若是有问题能够留言我会在看到后第一时间回复

相关文章
相关标签/搜索