自己动手实现一个简单的Spring IOC容器

  最近在学Spring框架,相信学习过Spring的同学都对Spring中的IOC容器印象深刻,它像一个黑盒一样,可以从我们的xml配置文件中读取bean,并将相应的属性注入到bean中。那么这是如何实现的呢,本文将通过一个小例子仿照Spring的方式实现一个简单的IOC容器,使其具有IOC容器的基本功能,包括从xml文件中读取bean,注入属性等功能。

1、首先定义两个POJO:HelloWorldService和User,里面的成员变量和方法非常简单:

HelloWorldService.java

 1 package com.yize.ioc;
 2 
 3 /**
 4  * @author: yize.jjx
 5  * @date: 2018-12-06 21:16
 6  * @desc:
 7  */
 8 public class HelloWorldService {
 9 
10     private String name;
11 
12     private Integer age;
13 
14     public User user;
15 
16     public void showHelloWorld(){
17         System.out.println(name+":"+age);
18     }
19 }

User.java

 1 package com.yize.ioc;
 2 
 3 /**
 4  * @author: yize.jjx
 5  * @date: 2018-12-07 19:44
 6  * @desc:
 7  */
 8 public class User {
 9 
10     private String name;
11 
12     public HelloWorldService helloWorldService;
13 
14     public void showUser(){
15         System.out.println(name);
16     }
17 }

这里解释一下为什么HelloWorldService这个类里面要包含User这个类的成员变量,User里面也包含了HelloWorldService这个成员变量,这样做的目的是让这两个类互相依赖,以测试我们的IOC容器在为bean注入属性的时候是否能解决这种相互依赖的问题。

2、写一个配置文件application.xml,在配置文件中声明这两个bean,并给它们设置相应的属性。

application.xml

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4        xmlns:context="http://www.springframework.org/schema/context"
 5        xsi:schemaLocation="http://www.springframework.org/schema/beans
 6             http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 7             http://www.springframework.org/schema/context
 8             http://www.springframework.org/schema/context/spring-context-3.0.xsd">
 9 
10     <bean id="helloWorldService" class="com.yize.ioc.HelloWorldService">
11         <property name="name" value="jeysin"/>
12         <property name="age" value="13"/>
13         <property name="user" ref="user"/>
14     </bean>
15 
16     <bean id="user" class="com.yize.ioc.User">
17         <property name="helloWorldService" ref="helloWorldService"/>
18         <property name="name" value="yize"/>
19     </bean>
20 
21 </beans>

可以看到这个配置文件的写法其实和Spring中的配置文件的写法是一样的。在这里说一下我们最终要实现的目标,我们希望我们的IOC容器最后能像Spring一样从这个xml中读取bean,然后注入属性,然后我们通过getBean方法可以从中直接取出bean来使用。就像这样:

 1 package com.yize.ioc;
 2 
 3 import com.yize.ioc.context.ApplicationContext;
 4 import com.yize.ioc.context.ClassPathXmlApplicationContext;
 5 import org.junit.Test;
 6 
 7 /**
 8  * @author: yize.jjx
 9  * @date: 2018-12-08 20:40
10  * @desc:
11  */
12 public class TestIOC {
13 
14     @Test
15     public void testMyIOC(){
16         ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
17 
18         HelloWorldService helloWorldService = (HelloWorldService) context.getBean("helloWorldService");
19         helloWorldService.showHelloWorld();
20 
21         User user = (User) context.getBean("user");
22         user.showUser();
23 
24     }
25 }

是不是和spring的用法一样,注意这里用的ApplicationContext和ClassPathXmlApplicationContext并不是Spring中的类,而是我们自己实现的那个IOC容器中的类,只是为了和Spring保持一致才故意命名成这样的,如果你想,完全可以换成别的名字。接下来我们就自己手动实现ApplicationContext和ClassPathXmlApplicationContext。

3、实现ApplicationContext和ClassPathXmlApplicationContext之前,先来实现另一个类:BeanDefinition

BeanDefinition.java

 1 package com.yize.ioc;
 2 
 3 import java.util.Map;
 4 import java.util.concurrent.ConcurrentHashMap;
 5 
 6 /**
 7  * @author: yize.jjx
 8  * @date: 2018-12-06 21:07
 9  * @desc:
10  */
11 public class BeanDefinition {
12 
13     private Object bean;
14 
15     private Class beanClass;
16 
17     private String beanClassName;
18 
19     private Map<String, Object> propertyMap = new ConcurrentHashMap<String, Object>();
20 
21     public BeanDefinition(String beanClassName){
22         setBeanClassName(beanClassName);
23     }
24 
25     public Object getBean(){
26         return this.bean;
27     }
28 
29     public void setBean(Object bean) {
30         this.bean = bean;
31     }
32 
33     public Class getBeanClass() {
34         return beanClass;
35     }
36 
37     public void setBeanClass(Class beanClass) {
38         this.beanClass = beanClass;
39     }
40 
41     public String getBeanClassName() {
42         return beanClassName;
43     }
44 
45     public void setBeanClassName(String beanClassName) {
46         this.beanClassName = beanClassName;
47         try{
48             this.beanClass = Class.forName(beanClassName);
49         }catch (ClassNotFoundException e){
50             e.printStackTrace();
51         }
52     }
53 
54     public void setProperty(String name, Object value){
55         propertyMap.put(name, value);
56     }
57 
58     public Map<String, Object> getPropertyMap(){
59         return this.propertyMap;
60     }
61 }

顾名思义,BeanDefinition的作用就是对bean的一层封装,封装了bean的名字beanClassName,这个beanClassName是我们从配置文件中读取到的,反应到上面那个配置文件中也就是"com.yize.ioc.HelloWorldService"和"com.yize.ioc.User"这样的字符串,我们可以根据这样的字符串把对应的Class对象加载进来,这正是setBeanClassName这个函数干的事。propertyMap里面存的是每一个具体的bean的所有属性值,也就是xml文件中<proterty>标签里的属性名称和属性值,存在map里面方便以后注入。

4、既然要从配置文件中读取,自然要先能根据配置文件的名字找到这个文件,然后打开它,生成一个InputStream共我们读取使用。所以定义一个接口:

Resource.java

1 public interface Resource {
2 
3     InputStream getInputStream() throws Exception;
4 }

以及它的实现:

URLResource.java

 1 package com.yize.ioc.io;
 2 
 3 import java.io.InputStream;
 4 import java.net.URL;
 5 import java.net.URLConnection;
 6 
 7 /**
 8  * @author: yize.jjx
 9  * @date: 2018-12-08 20:53
10  * @desc:
11  */
12 public class URLResource implements Resource{
13 
14     private String configLocation;
15 
16     public URLResource(String configLocation){
17         this.configLocation=configLocation;
18     }
19 
20     @Override
21     public InputStream getInputStream() throws Exception{
22         URL url=this.getClass().getClassLoader().getResource(configLocation);
23         URLConnection urlConnection=url.openConnection();
24         urlConnection.connect();
25         return urlConnection.getInputStream();
26     }
27 
28 }

5、解析配置文件之前,我们要思考一下了,遇到配置文件中的value标签到好办,直接将name和value作为一组键值对直接存入BeanDefinition中的propertyMap就好了,问题是如果遇到ref这种引用怎么办,怎样以键值对的形式也把它存入propertyMap中呢?为了解决这个问题,我们定义一个类:BeanReference

BeanReference.java

 1 package com.yize.ioc;
 2 
 3 /**
 4  * @author: yize.jjx
 5  * @date: 2018-12-07 19:46
 6  * @desc:
 7  */
 8 public class BeanReference {
 9 
10     private String ref;
11 
12     public BeanReference(String ref){
13         this.ref = ref;
14     }
15 
16     public String getRef() {
17         return ref;
18     }
19 }

这样,遇到ref的时候我们就可以把name和BeanReference作为一个键值对存入propertyMap中了。

6、接下来就要解析配置文件了,接口:

BeanDefinitionReader.java

 1 package com.yize.ioc.xml;
 2 
 3 import com.yize.ioc.io.Resource;
 4 
 5 import java.util.Map;
 6 
 7 /**
 8  * @author: yize.jjx
 9  * @date: 2018-12-08 20:46
10  * @desc:
11  */
12 public interface BeanDefinitionReader {
13 
14     void loadBeanDefinition(Resource resource);
15 }

以及相应的实现:

 1 package com.yize.ioc.xml;
 2 
 3 import com.yize.ioc.BeanDefinition;
 4 import com.yize.ioc.BeanReference;
 5 import com.yize.ioc.io.Resource;
 6 import org.w3c.dom.Document;
 7 import org.w3c.dom.Element;
 8 import org.w3c.dom.Node;
 9 import org.w3c.dom.NodeList;
10 
11 import javax.xml.parsers.DocumentBuilder;
12 import javax.xml.parsers.DocumentBuilderFactory;
13 import java.io.InputStream;
14 import java.util.HashMap;
15 import java.util.Map;
16 
17 /**
18  * @author: yize.jjx
19  * @date: 2018-12-07 13:54
20  * @desc:
21  */
22 public class XmlBeanDefinitionReader implements BeanDefinitionReader{
23 
24     private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<String, BeanDefinition>();
25 
26     public XmlBeanDefinitionReader(Resource resource){
27         loadBeanDefinition(resource);
28     }
29 
30     @Override
31     public void loadBeanDefinition(Resource resource) {
32         InputStream inputStream = null;
33         try {
34             DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
35             DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
36             inputStream = resource.getInputStream();
37             Document document = documentBuilder.parse(inputStream);
38             registerBeanDefinitions(document);
39             inputStream.close();
40         }catch (Exception e){
41             e.printStackTrace();
42         }
43     }
44 
45     private void registerBeanDefinitions(Document document){
46         Element root = document.getDocumentElement();
47         NodeList nodeList = root.getChildNodes();
48         for(int i=0; i<nodeList.getLength(); ++i){
49             Node node = nodeList.item(i);
50             if(node instanceof Element){
51                 Element beanNode = (Element) node;
52                 String id = beanNode.getAttribute("id");
53                 String className = beanNode.getAttribute("class");
54                 BeanDefinition beanDefinition = new BeanDefinition(className);
55                 //设置属性
56                 processProperties(beanNode, beanDefinition);
57                 //注册Bean
58                 beanDefinitionMap.put(id, beanDefinition);
59             }
60         }
61     }
62 
63     private void processProperties(Element beanNode, BeanDefinition beanDefinition){
64         NodeList nodeList = beanNode.getChildNodes();
65         for(int i=0; i<nodeList.getLength(); ++i){
66             Node node = nodeList.item(i);
67             if(node instanceof Element){
68                 Element propertyNode = (Element) node;
69                 String propertyName = propertyNode.getAttribute("name");
70                 String propertyValue = propertyNode.getAttribute("value");
71                 if(null!=propertyValue && propertyValue.length()>0) {
72                     beanDefinition.setProperty(propertyName, propertyValue);
73                 }else{
74                     String propertyRef = propertyNode.getAttribute("ref");
75                     beanDefinition.setProperty(propertyName, new BeanReference(propertyRef));
76                 }
77             }
78         }
79     }
80 
81     public Map<String, BeanDefinition> getBeanDefinitionMap(){
82         return beanDefinitionMap;
83     }
84 }

可以看到在processProperties这个函数中,遇到ref标签的时候,做法就是new一个BeanReference,然后把它和propertyName一起作为一个属性存入propertyMap中,这个BeanReference就相当于一个占位的类,把这个属性的位置先占住,证明这里是对另一个bean的引用,等到真正给bean注入属性的时候,发现这个占位类,再去注入对应的bean。

7、定义一个BeanFactory接口

BeanFactory.java

 1 package com.yize.ioc.factory;
 2 
 3 import com.yize.ioc.BeanDefinition;
 4 
 5 /**
 6  * @author: yize.jjx
 7  * @date: 2018-12-08 20:45
 8  * @desc:
 9  */
10 public interface BeanFactory {
11 
12     Object getBean(String name);
13 
14     void registerBeanDefinition(String name, BeanDefinition beanDefinition);
15 }

以及接口的实现:

AutowireCapableBeanFactory.java

 1 package com.yize.ioc.factory;
 2 
 3 import com.yize.ioc.BeanDefinition;
 4 import com.yize.ioc.BeanReference;
 5 
 6 import java.lang.reflect.Field;
 7 import java.util.Map;
 8 import java.util.concurrent.ConcurrentHashMap;
 9 
10 /**
11  * @author: yize.jjx
12  * @date: 2018-12-08 21:04
13  * @desc:
14  */
15 public class AutowireCapableBeanFactory implements BeanFactory{
16 
17     private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();
18 
19     @Override
20     public Object getBean(String name) {
21         BeanDefinition beanDefinition = beanDefinitionMap.get(name);
22         Object bean = beanDefinition.getBean();
23         if(null == bean){
24             bean = doCreateBean(beanDefinition);
25         }
26         return bean;
27     }
28 
29     @Override
30     public void registerBeanDefinition(String name, BeanDefinition beanDefinition) {
31         beanDefinitionMap.put(name, beanDefinition);
32     }
33 
34     private Object doCreateBean(BeanDefinition beanDefinition) {
35         Object bean=null;
36         try{
37             bean = beanDefinition.getBeanClass().newInstance();
38         }catch (Exception e){
39             e.printStackTrace();
40         }
41         //先设置bean,再注入属性,否则会产生循环依赖
42         beanDefinition.setBean(bean);
43         applyPropertyValues(bean, beanDefinition.getPropertyMap());
44         return bean;
45     }
46 
47     private void applyPropertyValues(Object bean, Map<String, Object> propertyMap) {
48 
49         for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
50             try {
51                 Field field = bean.getClass().getDeclaredField(entry.getKey());
52                 field.setAccessible(true);
53                 Object value = entry.getValue();
54                 if (value instanceof BeanReference) {
55                     field.set(bean, this.getBean(((BeanReference) value).getRef()));
56                 } else {
57                     String type = field.getType().getName();
58                     if (type.equals("java.lang.String")) {
59                         field.set(bean, entry.getValue());
60                     } else if (type.equals("java.lang.Integer") || type.equals("int")) {
61                         field.set(bean, Integer.valueOf((String) entry.getValue()));
62                     }
63                 }
64             } catch (Exception e) {
65                 e.printStackTrace();
66             }
67         }
68     }
69 }

beanDefinitionMap里面存的就是name和BeanDefinition的对应关系了,这里采用先创建bean,再给bean注入属性的方式来解决循环依赖的情况。在applyPropertyValues函数中注意要判断一下属性值是否为BeanReference的实例,如果是,说明这个属性是另一个bean的引用,要先拿到这个被引用的bean然后给它注入进去;如果不是,还要判断一下这个值的类型,到底是String类型还是Integer类型,以免注入的时候由于类型不匹配而抛异常。另外值得一提的是,这里采用了lazy机制来创建bean和注入属性,即在你调用getBean函数企图通过name获取一个bean之前,这个name对应的BeanDefinition里面的bean字段其实是为null的,也就是说,只有当你调用了getBean函数,才会开始根据beanClass实例化一个bean,然后给这个bean注入相应的属性,然后把这个bean返回给你使用。

8、定义ApplicationContext接口

ApplicationContext.java

 1 package com.yize.ioc.context;
 2 
 3 /**
 4  * @author: yize.jjx
 5  * @date: 2018-12-08 20:41
 6  * @desc:
 7  */
 8 public interface ApplicationContext {
 9 
10     Object getBean(String name);
11 }

以及相应的实现:

ClassPathXmlApplicationContext.java

 1 package com.yize.ioc.context;
 2 
 3 import com.yize.ioc.BeanDefinition;
 4 import com.yize.ioc.factory.AutowireCapableBeanFactory;
 5 import com.yize.ioc.factory.BeanFactory;
 6 import com.yize.ioc.io.URLResource;
 7 import com.yize.ioc.xml.XmlBeanDefinitionReader;
 8 
 9 import java.util.Map;
10 
11 /**
12  * @author: yize.jjx
13  * @date: 2018-12-08 21:10
14  * @desc:
15  */
16 public class ClassPathXmlApplicationContext implements ApplicationContext{
17 
18     private String configLocation;
19 
20     private BeanFactory beanFactory;
21 
22     public ClassPathXmlApplicationContext(String configLocation){
23         this(configLocation, new AutowireCapableBeanFactory());
24     }
25 
26     public ClassPathXmlApplicationContext(String configLocation, BeanFactory beanFactory){
27         this.configLocation = configLocation;
28         this.beanFactory = beanFactory;
29         refresh();
30     }
31 
32     private void refresh(){
33         XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(new URLResource(configLocation));
34         for(Map.Entry<String, BeanDefinition> entry : xmlBeanDefinitionReader.getBeanDefinitionMap().entrySet()) {
35             this.beanFactory.registerBeanDefinition(entry.getKey(), entry.getValue());
36         }
37     }
38 
39     @Override
40     public Object getBean(String name) {
41         return beanFactory.getBean(name);
42     }
43 }

可以看到我们在refresh这个函数中实现了将XmlBeanDefinitionReader中读取到的bean的定义向BeanFactory中转移的过程。

9、最后,大功告成,我们可以执行文章最开始的那段代码来测试一下:

 1 package com.yize.ioc;
 2 
 3 import com.yize.ioc.context.ApplicationContext;
 4 import com.yize.ioc.context.ClassPathXmlApplicationContext;
 5 import org.junit.Test;
 6 
 7 /**
 8  * @author: yize.jjx
 9  * @date: 2018-12-08 20:40
10  * @desc:
11  */
12 public class TestIOC {
13 
14     @Test
15     public void testMyIOC(){
16         ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
17 
18         HelloWorldService helloWorldService = (HelloWorldService) context.getBean("helloWorldService");
19         helloWorldService.showHelloWorld();
20 
21         User user = (User) context.getBean("user");
22         user.showUser();
23 
24     }
25 }

执行结果:

jeysin:13
yize

Process finished with exit code 0

  可以看到,我们的IOC容器确实实现了从xml文件中读取配置然后注入到bean中,为了测试我们的IOC是否能解决循环依赖这个问题,我们可以在测试用例中加几行测试代码:

 1 package com.yize.ioc;
 2 
 3 import com.yize.ioc.context.ApplicationContext;
 4 import com.yize.ioc.context.ClassPathXmlApplicationContext;
 5 import org.junit.Test;
 6 
 7 /**
 8  * @author: yize.jjx
 9  * @date: 2018-12-08 20:40
10  * @desc:
11  */
12 public class TestIOC {
13 
14     @Test
15     public void testMyIOC(){
16         ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
17 
18         HelloWorldService helloWorldService = (HelloWorldService) context.getBean("helloWorldService");
19         helloWorldService.showHelloWorld();
20 
21         User user = (User) context.getBean("user");
22         user.showUser();
23 
24         System.out.println(helloWorldService);
25         System.out.println(helloWorldService.user);
26         System.out.println(helloWorldService.user.helloWorldService);
27         System.out.println(helloWorldService.user.helloWorldService.user);
28     }
29 }

执行结果:

1 jeysin:13
2 yize
3 com.yize.ioc.HelloWorldService@6aa8ceb6
4 com.yize.ioc.User@2530c12
5 com.yize.ioc.HelloWorldService@6aa8ceb6
6 com.yize.ioc.User@2530c12
7 
8 Process finished with exit code 0

最后输出的结果我就不多说了,我们的IOC容器确实解决了bean与bean之间相互依赖的问题。从最后的结果中也可以看出来,我们的IOC容器中所产生的bean都是单例。

参考文章:

https://github.com/code4craft/tiny-spring

猜你喜欢

转载自www.cnblogs.com/jeysin/p/10091269.html