自己動手實現一個簡單的 IOC

莫那·魯道發表於2019-03-03

再上一篇文章中,樓主和大家一起分析spring的 IOC 實現,剖析了Spring的原始碼,看的出來,原始碼異常複雜,這是因為Spring的設計者需要考慮到框架的擴充套件性,健壯性,效能等待元素,因此設計的很複雜。樓主在最後也說要實現一個簡單的 IOC,讓我們更加深刻的理解IOC,因此,有了這篇文章。

當然我們是仿照Spring 的 IOC,因此程式碼命名和設計基本是仿照spring的。

我們將分為幾步來編寫簡易 IOC,首先設計元件,再設計介面,然後關注實現。

1. 設計元件。

我們還記得Spring中最重要的有哪些元件嗎?BeanFactory 容器,BeanDefinition Bean的基本資料結構,當然還需要載入Bean的資源載入器。大概最後最重要的就是這幾個元件。容器用來存放初始化好的Bean,BeanDefinition 就是Bean的基本資料結構,比如Bean的名稱,Bean的屬性 PropertyValue,Bean的方法,是否延遲載入,依賴關係等。資源載入器就簡單了,就是一個讀取XML配置檔案的類,讀取每個標籤並解析。

2. 設計介面

首先肯定需要一個BeanFactory,就是Bean容器,容器介面至少有2個最簡單的方法,一個是獲取Bean,一個註冊Bean.

/**
 * 需要一個beanFactory 定義ioc 容器的一些行為 比如根據名稱獲取bean, 比如註冊bean,引數為bean的名稱,bean的定義
 *
 * @author stateis0
 * @version 1.0.0
 * @Date 2017/11/30
 */
public interface BeanFactory {

  /**
   * 根據bean的名稱從容器中獲取bean物件
   *
   * @param name bean 名稱
   * @return bean例項
   * @throws Exception 異常
   */
  Object getBean(String name) throws Exception;

  /**
   * 將bean註冊到容器中
   *
   * @param name bean 名稱
   * @param bean bean例項
   * @throws Exception 異常
   */
  void registerBeanDefinition(String name, BeanDefinition bean) throws Exception;
}

複製程式碼

根據Bean的名字獲取Bean物件,註冊引數有2個,一個是Bean的名字,一個是 BeanDefinition 物件。

定義完了Bean最基本的容器,還需要一個最簡單 BeanDefinition 介面,我們為了方便,但因為我們這個不必考慮擴充套件,因此可以直接設計為類,BeanDefinition 需要哪些元素和方法呢? 需要一個 Bean 物件,一個Class物件,一個ClassName字串,還需要一個元素集合 PropertyValues。這些就能組成一個最基本的 BeanDefinition 類了。那麼需要哪些方法呢?其實就是這些屬性的get set 方法。 我們看看該類的詳細:

package cn.thinkinjava.myspring;

/**
 * bean 的定義
 *
 * @author stateis0
 */
public class BeanDefinition {

  /**
   * bean
   */
  private Object bean;

  /**
   * bean 的 CLass 物件
   */
  private Class beanClass;

  /**
   * bean 的類全限定名稱
   */
  private String ClassName;

  /**
   * 類的屬性集合
   */
  private PropertyValues propertyValues = new PropertyValues();

  /**
   * 獲取bean物件
   */
  public Object getBean() {
    return this.bean;
  }

  /**
   * 設定bean的物件
   */
  public void setBean(Object bean) {
    this.bean = bean;
  }

  /**
   * 獲取bean的Class物件
   */
  public Class getBeanclass() {
    return this.beanClass;
  }

  /**
   * 通過設定類名稱反射生成Class物件
   */
  public void setClassname(String name) {
    this.ClassName = name;
    try {
      this.beanClass = Class.forName(name);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }

  /**
   * 獲取bean的屬性集合
   */
  public PropertyValues getPropertyValues() {
    return this.propertyValues;
  }

  /**
   * 設定bean的屬性
   */
  public void setPropertyValues(PropertyValues pv) {
    this.propertyValues = pv;
  }

}

複製程式碼

有了基本的 BeanDefinition 資料結構,還需要一個從XML中讀取並解析為 BeanDefinition 的操作類,首先我們定義一個 BeanDefinitionReader 介面,該介面只是一個標識,具體由抽象類去實現一個基本方法和定義一些基本屬性,比如一個讀取時需要存放的註冊容器,還需要一個委託一個資源載入器 ResourceLoader, 用於載入XML檔案,並且我們需要設定該構造器必須含有資源載入器,當然還有一些get set 方法。

package cn.thinkinjava.myspring;

import cn.thinkinjava.myspring.io.ResourceLoader;
import java.util.HashMap;
import java.util.Map;

/**
 * 抽象的bean定義讀取類
 *
 * @author stateis0
 */
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

  /**
   * 註冊bean容器
   */
  private Map<String, BeanDefinition> registry;

  /**
   * 資源載入器
   */
  private ResourceLoader resourceLoader;

  /**
   * 構造器器必須有一個資源載入器, 預設外掛建立一個map容器
   *
   * @param resourceLoader 資源載入器
   */
  protected AbstractBeanDefinitionReader(ResourceLoader resourceLoader) {
    this.registry = new HashMap<>();
    this.resourceLoader = resourceLoader;
  }

  /**
   * 獲取容器
   */
  public Map<String, BeanDefinition> getRegistry() {
    return registry;
  }

  /**
   * 獲取資源載入器
   */
  public ResourceLoader getResourceLoader() {
    return resourceLoader;
  }

}

複製程式碼

有了這幾個抽象類和介面,我們基本能形成一個雛形,BeanDefinitionReader 用於從XML中讀取配置檔案,生成 BeanDefinition 例項,存放在 BeanFactory 容器中,初始化之後,就可以呼叫 getBean 方法獲取初始化成功的Bean。形成一個完美的閉環。

3. 如何實現

剛剛我們說了具體的流程:從XML中讀取配置檔案, 解析成 BeanDefinition,最終放進容器。說白了就3步。那麼我們就先來設計第一步。

1. 從XML中讀取配置檔案, 解析成 BeanDefinition

我們剛剛設計了一個讀取BeanDefinition 的介面 BeanDefinitionReader 和一個實現它的抽象類 AbstractBeanDefinitionReader,抽象了定義了一些簡單的方法,其中由一個委託類—–ResourceLoader, 我們還沒有建立, 該類是資源載入器,根據給定的路徑來載入資源。我們可以使用Java 預設的類庫 java.net.URL 來實現,定義兩個類,一個是包裝了URL的類 ResourceUrl, 一個是依賴 ResourceUrl 的資源載入類。

ResourceUrl 程式碼實現

/**
 * 資源URL
 */
public class ResourceUrl implements Resource {

  /**
   * 類庫URL
   */
  private final URL url;

  /**
   * 需要一個類庫URL
   */
  public ResourceUrl(URL url) {
    this.url = url;
  }

  /**
   * 從URL中獲取輸入流
   */
  @Override
  public InputStream getInputstream() throws Exception {
    URLConnection urlConnection = url.openConnection();
    urlConnection.connect();
    return urlConnection.getInputStream();

  }

}

複製程式碼

ResourceLoader 實現

/**
 * 資源URL
 */
public class ResourceUrl implements Resource {

  /**
   * 類庫URL
   */
  private final URL url;

  /**
   * 需要一個類庫URL
   */
  public ResourceUrl(URL url) {
    this.url = url;
  }

  /**
   * 從URL中獲取輸入流
   */
  @Override
  public InputStream getInputstream() throws Exception {
    URLConnection urlConnection = url.openConnection();
    urlConnection.connect();
    return urlConnection.getInputStream();
  }
}
複製程式碼

當然還需要一個介面,只定義了一個抽象方法

package cn.thinkinjava.myspring.io;

import java.io.InputStream;

/**
 * 資源定義
 *
 * @author stateis0
 */
public interface Resource {

  /**
   * 獲取輸入流
   */
  InputStream getInputstream() throws Exception;
}

複製程式碼

好了, AbstractBeanDefinitionReader 需要的元素已經有了, 但是,很明顯該方法不能實現讀取 BeanDefinition 的任務。那麼我們需要一個類去繼承抽象類,去實現具體的方法, 既然我們是XML 配置檔案讀取,那麼我們就定義一個 XmlBeanDefinitionReader 繼承 AbstractBeanDefinitionReader ,實現一些我們需要的方法, 比如讀取XML 的readrXML, 比如將解析出來的元素註冊到 registry 的 Map 中, 一些解析的細節。我們還是看程式碼吧。

XmlBeanDefinitionReader 實現讀取配置檔案並解析成Bean

package cn.thinkinjava.myspring.xml;

import cn.thinkinjava.myspring.AbstractBeanDefinitionReader;
import cn.thinkinjava.myspring.BeanDefinition;
import cn.thinkinjava.myspring.BeanReference;
import cn.thinkinjava.myspring.PropertyValue;
import cn.thinkinjava.myspring.io.ResourceLoader;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * 解析XML檔案
 *
 * @author stateis0
 */
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

  /**
   * 構造器,必須包含一個資源載入器
   *
   * @param resourceLoader 資源載入器
   */
  public XmlBeanDefinitionReader(ResourceLoader resourceLoader) {
    super(resourceLoader);
  }

  public void readerXML(String location) throws Exception {
    // 建立一個資源載入器
    ResourceLoader resourceloader = new ResourceLoader();
    // 從資源載入器中獲取輸入流
    InputStream inputstream = resourceloader.getResource(location).getInputstream();
    // 獲取文件建造者工廠例項
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    // 工廠建立文件建造者
    DocumentBuilder docBuilder = factory.newDocumentBuilder();
    // 文件建造者解析流 返回文件物件
    Document doc = docBuilder.parse(inputstream);
    // 根據給定的文件物件進行解析,並註冊到bean容器中
    registerBeanDefinitions(doc);
    // 關閉流
    inputstream.close();
  }

  /**
   * 根據給定的文件物件進行解析,並註冊到bean容器中
   *
   * @param doc 文件物件
   */
  private void registerBeanDefinitions(Document doc) {
    // 讀取文件的根元素
    Element root = doc.getDocumentElement();
    // 解析元素的根節點及根節點下的所有子節點並新增進註冊容器
    parseBeanDefinitions(root);
  }

  /**
   * 解析元素的根節點及根節點下的所有子節點並新增進註冊容器
   *
   * @param root XML 檔案根節點
   */
  private void parseBeanDefinitions(Element root) {
    // 讀取根元素的所有子元素
    NodeList nl = root.getChildNodes();
    // 遍歷子元素
    for (int i = 0; i < nl.getLength(); i++) {
      // 獲取根元素的給定位置的節點
      Node node = nl.item(i);
      // 型別判斷
      if (node instanceof Element) {
        // 強轉為父型別元素
        Element ele = (Element) node;
        // 解析給給定的節點,包括name,class,property, name, value,ref
        processBeanDefinition(ele);
      }
    }
  }

  /**
   * 解析給給定的節點,包括name,class,property, name, value,ref
   *
   * @param ele XML 解析元素
   */
  private void processBeanDefinition(Element ele) {
    // 獲取給定元素的 name 屬性
    String name = ele.getAttribute("name");
    // 獲取給定元素的 class 屬性
    String className = ele.getAttribute("class");
    // 建立一個bean定義物件
    BeanDefinition beanDefinition = new BeanDefinition();
    // 設定bean 定義物件的 全限定類名
    beanDefinition.setClassname(className);
    // 向 bean 注入配置檔案中的成員變數
    addPropertyValues(ele, beanDefinition);
    // 向註冊容器 新增bean名稱和bean定義
    getRegistry().put(name, beanDefinition);
  }

  /**
   * 新增配置檔案中的屬性元素到bean定義例項中
   *
   * @param ele 元素
   * @param beandefinition bean定義 物件
   */
  private void addPropertyValues(Element ele, BeanDefinition beandefinition) {
    // 獲取給定元素的 property 屬性集合
    NodeList propertyNode = ele.getElementsByTagName("property");
    // 迴圈集合
    for (int i = 0; i < propertyNode.getLength(); i++) {
      // 獲取集合中某個給定位置的節點
      Node node = propertyNode.item(i);
      // 型別判斷
      if (node instanceof Element) {
        // 將節點向下強轉為子元素
        Element propertyEle = (Element) node;
        // 元素物件獲取 name 屬性
        String name = propertyEle.getAttribute("name");
        // 元素物件獲取 value 屬性值
        String value = propertyEle.getAttribute("value");
        // 判斷value不為空
        if (value != null && value.length() > 0) {
          // 向給定的 “bean定義” 例項中新增該成員變數
          beandefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, value));
        } else {
          // 如果為空,則獲取屬性ref
          String ref = propertyEle.getAttribute("ref");
          if (ref == null || ref.length() == 0) {
            // 如果屬性ref為空,則丟擲異常
            throw new IllegalArgumentException(
                "Configuration problem: <property> element for property `"
                    + name + "` must specify a ref or value");
          }
          // 如果不為空,測建立一個 “bean的引用” 例項,構造引數為名稱,例項暫時為空
          BeanReference beanRef = new BeanReference(name);
          // 向給定的 “bean定義” 中新增成員變數
          beandefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, beanRef));
        }
      }
    }
  }

}

複製程式碼

可以說程式碼註釋寫的非常詳細,該類方法如下:

  1. public void readerXML(String location) 公開的解析XML的方法,給定一個位置的字串引數即可。
  2. private void registerBeanDefinitions(Document doc) 給定一個文件物件,並進行解析。
  3. private void parseBeanDefinitions(Element root) 給定一個根元素,迴圈解析根元素下所有子元素。
  4. private void processBeanDefinition(Element ele) 給定一個子元素,並對元素進行解析,然後拿著解析出來的資料建立一個 BeanDefinition 物件。並註冊到BeanDefinitionReader 的 Map 容器(該容器存放著解析時的所有Bean)中。
  5. private void addPropertyValues(Element ele, BeanDefinition beandefinition) 給定一個元素,一個 BeanDefinition 物件,解析元素中的 property 元素, 並注入到 BeanDefinition 例項中。

一共5步,完成了解析XML檔案的所有操作。 最終的目的是將解析出來的檔案放入到 BeanDefinitionReader 的 Map 容器中。

好了,到這裡,我們已經完成了從XML檔案讀取並解析的步驟,那麼什麼時候放進BeanFactory的容器呢? 剛剛我們只是放進了 AbstractBeanDefinitionReader 的註冊容器中。因此我們要根據BeanFactory 的設計來實現如何構建成一個真正能用的Bean呢?因為剛才的哪些Bean只是一些Bean的資訊。沒有我們真正業務需要的Bean。

2. 初始化我們需要的Bean(不是Bean定義)並且實現依賴注入

我們知道Bean定義是不能幹活的,只是一些Bean的資訊,就好比一個人,BeanDefinition 就相當你在公安局的檔案,但是你人不在公安局,可只要公安局拿著你的檔案就能找到你。就是這樣一個關係。

那我們就根據BeanFactory的設計來設計一個抽象類 AbstractBeanFactory。

package cn.thinkinjava.myspring.factory;

import cn.thinkinjava.myspring.BeanDefinition;
import java.util.HashMap;

/**
 * 一個抽象類, 實現了 bean 的方法,包含一個map,用於儲存bean 的名字和bean的定義
 *
 * @author stateis0
 */
public abstract class AbstractBeanFactory implements BeanFactory {

  /**
   * 容器
   */
  private HashMap<String, BeanDefinition> map = new HashMap<>();

  /**
   * 根據bean的名稱獲取bean, 如果沒有,則丟擲異常 如果有, 則從bean定義物件獲取bean例項
   */
  @Override
  public Object getBean(String name) throws Exception {
    BeanDefinition beandefinition = map.get(name);
    if (beandefinition == null) {
      throw new IllegalArgumentException("No bean named " + name + " is defined");
    }
    Object bean = beandefinition.getBean();
    if (bean == null) {
      bean = doCreate(beandefinition);
    }
    return bean;
  }

  /**
   * 註冊 bean定義 的抽象方法實現,這是一個模板方法, 呼叫子類方法doCreate,
   */
  @Override
  public void registerBeanDefinition(String name, BeanDefinition beandefinition) throws Exception {
    Object bean = doCreate(beandefinition);
    beandefinition.setBean(bean);
    map.put(name, beandefinition);
  }

  /**
   * 減少一個bean
   */
  abstract Object doCreate(BeanDefinition beandefinition) throws Exception;
}

複製程式碼

該類實現了介面的2個基本方法,一個是getBean,一個是 registerBeanDefinition, 我們也設計了一個抽象方法供這兩個方法呼叫,將具體邏輯建立邏輯延遲到子類。這是什麼設計模式呢?模板模式。主要還是看 doCreate 方法,就是建立bean 具體方法,所以我們還是需要一個子類, 叫什麼呢? AutowireBeanFactory, 自動注入Bean,這是我們這個標準Bean工廠的工作。看看程式碼吧?

package cn.thinkinjava.myspring.factory;

import cn.thinkinjava.myspring.BeanDefinition;
import cn.thinkinjava.myspring.PropertyValue;
import cn.thinkinjava.myspring.BeanReference;
import java.lang.reflect.Field;


/**
 * 實現自動注入和遞迴注入(spring 的標準實現類 DefaultListableBeanFactory 有 1810 行)
 *
 * @author stateis0
 */
public class AutowireBeanFactory extends AbstractBeanFactory {


  /**
   * 根據bean 定義建立例項, 並將例項作為key, bean定義作為value存放,並呼叫 addPropertyValue 方法 為給定的bean的屬性進行注入
   */
  @Override
  protected Object doCreate(BeanDefinition beandefinition) throws Exception {
    Object bean = beandefinition.getBeanclass().newInstance();
    addPropertyValue(bean, beandefinition);
    return bean;
  }

  /**
   * 給定一個bean定義和一個bean例項,為給定的bean中的屬性注入例項。
   */
  protected void addPropertyValue(Object bean, BeanDefinition beandefinition) throws Exception {
    // 迴圈給定 bean 的屬性集合
    for (PropertyValue pv : beandefinition.getPropertyValues().getPropertyValues()) {
      // 根據給定屬性名稱獲取 給定的bean中的屬性物件
      Field declaredField = bean.getClass().getDeclaredField(pv.getname());
      // 設定屬性的訪問許可權
      declaredField.setAccessible(true);
      // 獲取定義的屬性中的物件
      Object value = pv.getvalue();
      // 判斷這個物件是否是 BeanReference 物件
      if (value instanceof BeanReference) {
        // 將屬性物件轉為 BeanReference 物件
        BeanReference beanReference = (BeanReference) value;
        // 呼叫父類的 AbstractBeanFactory 的 getBean 方法,根據bean引用的名稱獲取例項,此處即是遞迴
        value = getBean(beanReference.getName());
      }
      // 反射注入bean的屬性
      declaredField.set(bean, value);
    }

  }


}

複製程式碼

可以看到 doCreate 方法使用了反射建立了一個物件,並且還需要對該物件進行屬性注入,如果屬性是 ref 型別,那麼既是依賴關係,則需要呼叫 getBean 方法遞迴的去尋找那個Bean(因為最後一個Bean 的屬性肯定是基本型別)。這樣就完成了一次獲取例項化Bean操作,並且也實現類依賴注入。

4. 總結

我們通過這些程式碼實現了一個簡單的 IOC 依賴注入的功能,也更加了解了 IOC, 以後遇到Spring初始化的問題再也不會手足無措了。直接看原始碼就能解決。哈哈

具體程式碼樓主放在了github上,地址:自己實現的一個簡單IOC,包括依賴注入

good luck !!!

相關文章