Java Web框架——自己动手实现一个简化版的Spring IOC框架

Posted night_joe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Web框架——自己动手实现一个简化版的Spring IOC框架相关的知识,希望对你有一定的参考价值。

 一、序

  只要是Java Web的开发者,相信没有不知道大名鼎鼎的Spring框架的。作为一个优秀的开源框架,Spring包含9大组件,如果想要通过源码去理解每一个组件,复杂度和所花费的时间都容易让人望而却步——笔者平时习惯阅读源码,但是在学习Spring这种级别的框架面前显然不是一个好选择。但是问题又来了,不阅读源码,如何深入理解Spring呢?笔者在搜索学习资料的时候在github找到了一个优秀的repo——https://github.com/code4craft/tiny-spring,通过参考其思路,自己动手实现一个简化版的Spring框架(这里只实现核心IOC部分,后续会有文章实现其他部分)。本文假设读者已经对Spring有了大致的理解,不会花太多篇幅介绍Spring,重点在实现上。

 

二、设计

  首先花几分钟简单看一下Spring3.x的架构,虽然现在Spring都5.x了,但是我们现在是理解核心思路,看以前版本的架构图反而更清晰,下图转自网络:

  Spring含有十几个组件,核心的只有3个——Core,Context和Bean,那么这次我们就实现相对来说比较重要的BeanCore,Context主要是一个起封装功能的作用,影响不大,放到以后完善。首先设计好我们的架构,首先我们需要一个Resource来存储我们需要的Bean信息来源和链接到我们需要的类,也需要一个BeanDefiinition对Bean进行描述和操作,还需要有一个BeanFactory来管理Bean。为了职责分明,再添加一个ResourceLoader对Resource存储的Bean信息进行读取,以及一个对BeanDefinition进行读取的BeanDefinitionReader,大致如下图:

 

三、实现

  

  1、首先写Resource接口,只规定了一个通过流获取资源getInputStream()方法,方便实现不同的获取资源的方式:

1 package com.joe.io;
2 
3 import java.io.InputStream;
4 
5 public interface Resource {
6     InputStream getInputStream() throws Exception;
7 }

  

  2、通过具体的类UrlResource实现Resource接口:

 1 package com.joe.io;
 2 
 3 import java.io.InputStream;
 4 import java.net.URL;
 5 import java.net.URLConnection;
 6 
 7 public class UrlResource implements Resource {
 8     
 9     private URL url;
10     
11     public UrlResource(URL url) {
12         this.url = url;
13     }
14     
15     public InputStream getInputStream() throws Exception {
16         URLConnection urlConnection = url.openConnection();
17         urlConnection.connect();
18         return urlConnection.getInputStream();
19     }
20 }

  这里底层用了java.net包的URL以及相关类去操作资源的获取,核心方法就一个getInputStream(),这里通过URLConnection去获取资源,读者想了解底层细节可以自行google。那么作为入参的URL是由谁传入的呢?自然是我们接下来要介绍的ResourceLoader。

  

  3、负责读取Resource的资源加载器ResourceLoader类:

 1 package com.joe.io;
 2 
 3 import java.net.URL;
 4 
 5 public class ResourceLoader {
 6     
 7     public Resource getResource(String location) {
 8         URL url = this.getClass().getClassLoader().getResource(location);
 9         return new UrlResource(url);
10     }
11 }

  这里很清晰地看到,getResource()方法把接收到的String作为参数,通过获取当前的类加载器,调用ClassLoader.getResource(String location)方法将返回的URL传递给我们的Resource(同上,ClassLoader相关api原理和本文关系不大,想学习的读者的自行google),至此资源加载器的功能就完成了。

  

  4、描述Bean的BeanDefinition类:

 1 package com.joe;
 2 
 3 public class BeanDefinition {
 4     private Object bean;// 对象实例
 5     
 6     private Class beanClass;//bean的类型
 7     
 8     private String beanClassName;//类名
 9     
10     private PropertyValues propertyValues; 
11 
12     public Object getBean() {
13         return bean;
14     }
15 
16     public void setBean(Object bean) {
17         this.bean = bean;
18     }
19 
20     public Class getBeanClass() {
21         return beanClass;
22     }
23 
24     public void setBeanClass(Class beanClass) {
25         this.beanClass = beanClass;
26     }
27 
28     public String getBeanClassName() {
29         return beanClassName;
30     }
31 
32     public void setBeanClassName(String beanClassName) {
33         this.beanClassName = beanClassName;
34         try {
35             this.beanClass = Class.forName(beanClassName);
36         } catch (ClassNotFoundException e) {
37             e.printStackTrace();
38         }
39     }
40 
41     public PropertyValues getPropertyValues() {
42         if (propertyValues == null) {
43             propertyValues = new PropertyValues();
44         }
45         return propertyValues;
46     }
47 
48     public void setPropertyValues(PropertyValues propertyValues) {
49         this.propertyValues = propertyValues;
50     }
51 }

  BeanDefinition为了对Bean进行描述和初始化,所以需要持有以下变量:

  1) Object bean,用来存储Bean,也就是具体的对象实例;

  2) Class beanClass,用来存储bean的Class类型;

  3) Class beanClassName,用来存储bean的具体类名;

  4) PropertyValues propertyValues,用来随着bean一起注入IOC容器的类变量。

  前三个变量没什么好说的,PropertyValues主要用来存储类变量,我们稍后对其进行实现。

  

  5、定义好了描述Bean,为了职责分明,我们还需要对一个其进行读取的BeanDefinitonReader接口:

  

1 package com.joe;
2 
3 public interface BeanDefinitionReader {
4     void loadBeanDefinitions(String location) throws Exception;
5 }

  定义一个方法供IOC容器在初始化的时候从location指定的位置进行资源以及Bean的加载,在这里我们只实现xml加载方式。

 

  6、AbstractBeanDefinitionReader,在我们实现具体的通过xml进行Bean加载器之前,为了通用性(实际场景,不可能只通过xml去解析Bean资源),我们实现一个抽象的Bean加载器类:

 

 1 package com.joe;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 import com.joe.io.ResourceLoader;
 7 
 8 public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {
 9     
10     private Map<String, BeanDefinition> registry;
11     
12     private ResourceLoader resourceLoader;
13     
14     public AbstractBeanDefinitionReader(ResourceLoader resourceLoader) {
15         this.registry = new HashMap<String, BeanDefinition>();
16         this.resourceLoader = resourceLoader;
17     }
18 
19     public Map<String, BeanDefinition> getRegistry() {
20         return registry;
21     }
22 
23     public ResourceLoader getResourceLoader() {
24         return resourceLoader;
25     }
26 }

  抽象类除了持有Map<String, BeanDefinition>以方便Bean的注册外,还持有一个我们之前实现的资源加载器ResourceLoader,稍后介绍资源加载器的作用。

  

  7、XmlBeanDefinitionReader,这部分内容涉及比较多的细节,比如dom解析文件节点等操作,可以选择性跳过,理解思路即可:

 1 package com.joe.xml;
 2 
 3 import java.io.InputStream;
 4 
 5 import javax.xml.parsers.DocumentBuilder;
 6 import javax.xml.parsers.DocumentBuilderFactory;
 7 
 8 import org.w3c.dom.Document;
 9 import org.w3c.dom.Element;
10 import org.w3c.dom.Node;
11 import org.w3c.dom.NodeList;
12 
13 import com.joe.AbstractBeanDefinitionReader;
14 import com.joe.BeanDefinition;
15 import com.joe.BeanReference;
16 import com.joe.PropertyValue;
17 import com.joe.io.ResourceLoader;
18 
19 public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
20 
21     public XmlBeanDefinitionReader(ResourceLoader resourceLoader) {
22         super(resourceLoader);
23     }
24 
25     public void loadBeanDefinitions(String location) throws Exception {
26         InputStream inputStream = getResourceLoader().getResource(location).getInputStream();
27         doLoadBeanDefinitions(inputStream);
28     }
29 
30     protected void doLoadBeanDefinitions(InputStream inputStream) throws Exception {
31         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
32         DocumentBuilder docBuilder = factory.newDocumentBuilder();
33         Document doc = docBuilder.parse(inputStream);
34         // 解析bean
35         registerBeanDefinitions(doc);
36         inputStream.close();
37     }
38 
39     public void registerBeanDefinitions(Document doc) {
40         Element root = doc.getDocumentElement();
41 
42         parseBeanDefinitions(root);
43     }
44 
45     protected void parseBeanDefinitions(Element root) {
46         NodeList nl = root.getChildNodes();
47         for (int i = 0; i < nl.getLength(); i++) {
48             Node node = nl.item(i);
49             if (node instanceof Element) {
50                 Element ele = (Element) node;
51                 processBeanDefinition(ele);
52             }
53         }
54     }
55 
56     protected void processBeanDefinition(Element ele) {
57         String name = ele.getAttribute("id");
58         String className = ele.getAttribute("class");
59         BeanDefinition beanDefinition = new BeanDefinition();
60         processProperty(ele, beanDefinition);
61         beanDefinition.setBeanClassName(className);
62         getRegistry().put(name, beanDefinition);
63     }
64 
65     private void processProperty(Element ele, BeanDefinition beanDefinition) {
66         NodeList propertyNode = ele.getElementsByTagName("property");
67         for (int i = 0; i < propertyNode.getLength(); i++) {
68             Node node = propertyNode.item(i);
69             if (node instanceof Element) {
70                 Element propertyEle = (Element) node;
71                 String name = propertyEle.getAttribute("name");
72                 String value = propertyEle.getAttribute("value");
73                 if (value != null && value.length() > 0) {
74                     beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, value));
75                 } else {
76                     String ref = propertyEle.getAttribute("ref");
77                     if (ref == null || ref.length() == 0) {
78                         throw new IllegalArgumentException("Configuration problem: <property> element for property \'"
79                                 + name + "\' must specify a ref or value");
80                     }
81                     BeanReference beanReference = new BeanReference(ref);
82                     beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, beanReference));
83                 }
84             }
85         }
86     }
87 }

  这个代码稍微多点,大致上就干了这么几件事:

  1)加载xml:初始化时,把指定的xml文件(这里是xml,也可以是其他资源)加载进IOC容器,转化成流inputStream后存储到org.w3c.dom.Document,这里用到的org.w3c包里的对象比较多,分别有Document,Element,Node等。谁负责加载呢?自然就是我们之前定义的资源加载器ResourceLoader了,这时候,之前提到的抽象资源加载器类AbstractBeanDefinitionReader持有的ResourceLoader就发挥作用了,正是通过这个ResourceLoader完成一系列上述操作,忘了的读者可以回去看其实现。

  2)获取xml中的各个节点:由1中获取到的Document对象中获得其Element元素,Element描述了xml文件的各个定义bean的节点,也可以说是一个Element代表了一个xml中bean相关的标签,形如<bean id="...", class="...">以及其包括的内容。

  3)注入bean:对各个Element元素遍历,获取标签中定义为id和class的内容,存储为name和className,name在5中讲解,className为完整描述了包路径的类名。

  4)注入类变量:针对每个Element创建一个BeanDefinition,遍历Element包含的所有<property>标签(此标签定义了类变量),获取到类变量名以及希望注入的值,添加到BeanDefinition持有的PropertyValues中去。如果类变量是引用类型,则进行另外的处理,这里就不展开讲了,有兴趣的读者自行阅读源码。

  5)初始化BeanDefiniton部分属性:完成变量的注入后,我们对BeanDefinition持有的其他类变量进行初始化,这里就用到3)中存储的类名name,通过Class.forName(name)获取到其类型,这样BeanDefiniton的beanClassName和beanClass也得到了初始化(还有bean的初始化呢?请带着疑问接着看下去),这里把name和BeanDefiniton注册到抽象资源加载器类AbstractBeanDefinitionReader持有的Map中。至此,资源加载器初始化完成,可以供bean工厂获取了。那么接下来要实现的自然是bean工厂。

 

  8、BeanFactory接口

 1 package com.joe.factory;
 2 
 3 import com.joe.BeanDefinition;
 4 
 5 public interface BeanFactory {
 6     
 7     Object getBean(String name);
 8     
 9     void registerBeanDefinition(String name, BeanDefinition beanDefinition) throws Exception;     
10 }

  一个获取Bean的getBean(),一个负责从资源加载器中获取并注册Bean的registerBeanDefinition()。

  

  9、同样的考虑了通用性,实现了其抽象类:

 1 package com.joe.factory;
 2 
 3 import java.util.concurrent.ConcurrentHashMap;
 4 
 5 import com.joe.BeanDefinition;
 6 
 7 public abstract class AbstractBeanFactory implements BeanFactory {
 8     
 9     private ConcurrentHashMap<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();
10 
11     public Object getBean(String name) {
12         return beanDefinitionMap.get(name).getBean();
13     }
14 
15     public void registerBeanDefinition(String name,
16             BeanDefinition beanDefinition) throws Exception {
17         // 何时设置beanDefinition的其他属性beanClass,beanClassName?——在BeanDefinitionReader加载xml文件的时候set(初始化的时候)
18         //测试用例指定要获取的beanClassName
19         Object bean = doCreateBean(beanDefinition);//beanDefinition.getBeanClass().newInstance()
20         beanDefinition.setBean(bean);
21         beanDefinitionMap.put(name, beanDefinition);
22     }
23 
24     abstract Object doCreateBean(BeanDefinition beanDefinition) throws Exception;
25     
26 }

  抽象类持有负责存储注册bean的ConcurrentHashMap,关于ConcurrentHashMap的内容也不展开了,读者自行google关于它的内容。这个map的键为String的类名,值为BeanDefinition对象。这里创建类实例(交给实现类实现doCreateBean()去做),并且设置到BeanDefiniton中,至此,BeanDefiniton的所有属性都初始化完毕。可以注册到BeanDefinition工厂中了,beanDefinitonMap.put()方法注册完成后,bean工厂的初始化也正式宣告完成,可以等待代码获取bean了(工厂可以正式开始量产“对象”)。

  

  10、AutowiredCapableBenafactory,第9中提到的如何创建类实例,我们通过实现doCreateBean()自行决定如何创建:

 1 package com.joe.factory;
 2 
 3 import java.lang.reflect.*;
 4 
 5 import com.joe.*;
 6 
 7 public class AutowiredCapableBeanFactory extends AbstractBeanFactory {
 8 
 9     @Override
10     Object doCreateBean(BeanDefinition beanDefinition) throws Exception {
11         Object bean = beanDefinition.getBeanClass().newInstance();
12         beanDefinition.setBean(bean);
13         applyPropertyValues(bean, beanDefinition);
14         return bean;
15     }
16     
17     void applyPropertyValues(Object bean, BeanDefinition mdb) throws Exception {
18         for (PropertyValue propertyValue : mdb.getPropertyValues().getPropertyValues()) {
19             Field field = bean.getClass().getDeclaredField(propertyValue.getName());
20             field.setAccessible(true);
21             field.set(bean, propertyValue.getValue());
22         }
23     }
24 
25 }

  doCreateBena通过反射相关API创建实例bean,并设置给BeanDefiniton的bean属性,然后通过遍历BeanDefiniton的变量ProperyValues,还是通过反射,为变量设置从xml文件注入的值(回忆下刚刚资源加载器从如何从xml解析各个节点元素的值)。

  

  11、之前提到用于描述类变量的PropertyValues和PropertyValue,注意两者的差别,两者配合使用,联系比较紧密,就两个类一起贴出来了。

package com.joe;

import java.util.ArrayList;
import java.util.List;

public class PropertyValues {
    
    private final List<PropertyValue> propertyValueList = new ArrayList<PropertyValue>();
    
    public PropertyValues() {}
    
    public void addPropertyValue(PropertyValue pv) {
        this.propertyValueList.add(pv);
    }
    
    public List<PropertyValue> getPropertyValues() {
        return this.propertyValueList;
    }
}


package com.joe;

public class PropertyValue {
    
    private final String name;
    
    public String getName() {
        return name;
    }

    public Object getValue() {
        return value;
    }

    private final Object value;
    
    public PropertyValue(String name, Object value) {
        this.name = name;
        this.value = value;
    }
}

 

  12、为了读者能跑通UnitTest,也给出相关的其他类以及xml文件。

  用于注入引用类型变量的BeanReference实现:

 1 package com.joe;
 2 
 3 public class BeanReference {
 4 
 5     private String name;
 6 
 7     private Object bean;
 8 
 9     public BeanReference(String name) {
10         this.name = name;
11     }
12 
13     public String getName() {
14         return name;
15     }
16 
17     public void setName(String name) {
18         this.name = name;
19     }
20 
21     public Object getBean() {
22         return bean;
23     }
24 
25     public void setBean(Object bean) {
26         this.bean = bean;
27     }
28 }

  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" xmlns:aop="http://www.springframework.org/schema/aop"
 4        xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
 5        xsi:schemaLocation="
 6     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 7     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
 8     http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
 9     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
10 
11     <bean id="helloWorldService" class="com.joe.HelloWorldServiceImpl">
12         <property name="text" value="Hello Joe"></property>
13     </bean>
14 
15 </beans>

 

  13:单元测试文件:

  测试类BeanFactoryTest:

 1 package com.joe;
 2 
 3 import static org.junit.Assert.*;
 4 
 5 import java.util.Map;
 6 
 7 import org.junit.Test;
 8 
 9 import com.joe.factory.AbstractBeanFactory;
10 import com.joe.factory.AutowiredCapableBeanFactory;
11 import com.joe.io.ResourceLoader;
12 import com.joe.xml.XmlBeanDefinitionReader;
13 
14 public class BeanFactoryTest {
15 
16     @Test
17     public void test() throws Exception {
18         //初始化......
19         XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(new ResourceLoader());
20         xmlBeanDefinitionReader.loadBeanDefinitions("tinyioc.xml");
21         
22         AbstractBeanFactory beanFactory = new AutowiredCapableBeanFactory();
23         for (Map.Entry<String, BeanDefinition> beanDefinitionEntry : xmlBeanDefinitionReader.getRegistry().entrySet()) {
24             beanFactory.registerBeanDefinition(beanDefinitionEntry.getKey(), beanDefinitionEntry.getValue());
25         }
26         //初始化完毕,获取想要的bean
27         HelloWorldService helloWorldService = (HelloWorldService)beanFactory.getBean("helloWorldService");
28         helloWorldService.helloWorld();
29     }
30 
31 }

  测试用服务类以及其接口(一起贴出来了):

package com.joe;
public class HelloWorldServiceImpl implements HelloWorldService {

    private String text;

    public void setText(String text) {
        this.text = text;
    }

    @Override
    public void helloWorld() {
        System.out.println(text);
    }

}


package com.joe;
public interface HelloWorldService {

    void helloWorld();
}

 

四、总结

  Spring作为一个通用的Web框架,其代码组织方式非常值得我们借鉴。接口、抽象类、实现类之间都很好的体现了开闭原则,各个类职责清晰,可以说是工厂模式,模板方法等等设计模式的一个很好的实践。这个简化版Spring,代码量虽然不大,但是也正因为这样,更适合新手理解和实践。

以上是关于Java Web框架——自己动手实现一个简化版的Spring IOC框架的主要内容,如果未能解决你的问题,请参考以下文章

自己实现一个简化版Mybatis框架

动手写一个简单的Web框架(Werkzeug路由问题)

动手写一个简单的Web框架(Werkzeug路由问题)

自己动手模仿 springmvc 写一个 mvc框架

自己动手模仿 springmvc 写一个 mvc框架

自己动手写一个简单的MVC框架(第二版)