自己动手编写IOC框架(一)
博客创建了2年多了,一直没敢写点东西,怕技术不够误导了别人。2年多后的今天我已经很有信心能够为需要帮助的人做点微薄的贡献了。这是我第一次写博客,先自我介绍一下。本人网名泪滴,一个非常伤心的名字,生活中除了代码一无所有,平时喜欢看开源框架的源码,今天也为开源贡献一份自己的力量。
这次项目叫做IOC框架,是根据spring的IoC的使用风格使用自己的代码实现。项目的目的不是为了推销我的框架,只是为了让目前正在使用IoC和将要使用IoC的小伙伴们对IoC有一个全新的认识,相信泪滴,我不会浪费你们宝贵的时间,不管你是新手还是大牛都会从中有所收获。由于我有朝九晚五的工作,精力有限,所以本次项目采用连载的方式,尽量保证每周至少2,3次更新,每次更新至少解决一个阶段性的问题。废话我就不多说了,下面开始正题。
IoC框架,估计使用过java,使用过spring的人都不会陌生,它就是一个依赖注入的功能,通俗的说就是可以在项目或者服务启动使用的时候给我们一次性准备好我们程序中配置好的所需要的对象,并且自动将对象set到需要的对象中。例如A类中的成员变量中有一个B类的引用,那么生成对象的时候会生成一个A的对象和B的对象,并且将B的对象通过A的构造方法或者set方法将B对象设置到A对象中。其实看来完成的功能很简单就是为了让我们在代码中减少使用new关键字的次数,让大多数程序中的new的性能开销和代码开销集中在项目启动或者服务启动的时候。在程序的编写过程中不会一次又一次的写A a = new A()这种没有营养的代码,在程序运行的时候不会因为多次的new对象浪费时间。
注意上面我们提到的IoC的功能我们可以发现几个很关键的地方。第一:该框架产生的动作就是生成对象,第二:生成了对象还要考虑该对象是不是需要依赖别的对象,如果需要要给他set好需要的对象,第三:前两个动作产生的时机一般就是项目启动或者服务启动的时候。这时候我们是不是大脑中已经有一个很简单的想法了,我们将项目中所需要的生成的对象全部配置在配置文件中,然后在main方法中或者是web中的监听器Listener调用一个方法去读取配置文件然后将他们的对象全部生成出来。至于配置文件采用什么格式的配置文件也是需要考虑的地方,由于有些对象需要依赖其他对象,而且依赖的对象个数也不一定,也有可能A依赖B,B又依赖C...这种层级关系栩雅保存起来,放眼配置文件类型,能够达到保存这种层级关系的文件很明显就可以用xml文件。至于怎么生成对象又是一个需要考虑的问题,由于我们要生成哪些对象是从配置文件中读取出来的,都出来后对象的表示肯定都是一个个字符串了,很显然我们不能使用new去生成对象了。java中什么技术可以使用字符串表示的类生成对象的,显然Class.forName(className)这个作为反射入口的方法是完全满足要求的。
经过上面的分析是否对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这种策略是完全没有任何问题的。
上面分析了这么久,我们或多或少对于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中的内容就能知道了。这边和那个大同小异。为了方便下面代码的讲解,我们说下这次项目的包结构
其中项目的根包为com.tear.ioc,bean包下面放一些项目需要使用的bean,dtd下放dtd文件,xml包下放处理xml文件的相关包或者类。下面的讲解全部是以这个包为基础进行的。
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 如果有问题可以留言我会在看到后第一时间回复