• 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,代码量虽然不大,但是也正因为这样,更适合新手理解和实践。

  • 相关阅读:
    针对专业人员的 TensorFlow 2.0 入门
    学习深度学习过程中的一些问题
    Leetcode_06_ZigZag Conversion (easy)
    leetcode_07_Reverse Integer (easy)
    独立游戏人:像素风格游戏制作分享(转)
    关于iphone开发中的@property和@synthesize的一些见解(转)
    iphone开发cocoa中nil,NSNull,Nil的使用区别
    Xcode6.1创建仅xib文件无storyboard的hello world应用(转)
    iOS 学习资料整理(转)
    hdoj1042ac
  • 原文地址:https://www.cnblogs.com/junqiao/p/9502255.html
Copyright © 2020-2023  润新知