如何實現一個簡易版的 Spring - 如何實現 Setter 注入

mghio發表於2020-11-29

前言

之前在 上篇 提到過會實現一個簡易版的 IoC 和 AOP,今天它終於來了。。。相信對於使用 Java 開發語言的朋友們都使用過或者聽說過 Spring 這個開發框架,絕大部分的企業級開發中都離不開它,通過 官網 可以瞭解到其生態非常龐大,針對不同方面的開發提供了一些解決方案,可以說 Spring 框架的誕生是對 Java 開發人員的一大福利,自 2004 年釋出以來,Spring 為了解決一些企業開發中的痛點先後引入了很多的特性和功能,其中最重要的就是我們經常聽到的 IoC 和 AOP 特性,由於涉及到的知識和細節比較多,會分為幾篇文章來介紹,今天這篇(也是第一篇)我們來看看如何實現基於 XML 配置方式的 Setter 注入

預備知識

既然是通過 XML 配置檔案的方式,首先第一件事就是要讀取 XML 檔案然後轉換為我們需要的資料結構,解析 XML 檔案有但不限於這些方式(JDOMXOMdom4j),這裡使用的是簡單易上手的 dom4j,所你得對其基礎知識有一些簡單瞭解,其實都是一些很簡單的方法基礎使用而已,第二個就是你要有一些 Spring 框架的使用經驗,這裡實現的簡易版本質上是對 Spring 的一個精簡後的核心部分的簡單實現,是的,沒錯,你只需要有了這些基礎預備知識就可以了。

基礎資料結構抽象

在開始編碼實現前先要做一些簡單的構思和設計,首先在 Spring 中把一個被其管理的物件稱之為 Bean,然後其它的操作都是圍繞這個 Bean 來展開設計的,所以為了能在程式中統一併且規範的表示一個 Bean 的定義,於是第一個介面 BeanDefinition 就出來了,本次需要的一些基本資訊包含 Bean 的名稱、所屬類名稱、是否單例、作用域等,如下所示:

spring-injection-beandefinition-1.png

現在 BeanDefinition 有了,接下來就是要根據這個 BeanDefinition 去建立出對應的 Bean 例項了,很顯然這需要一個 Factory 工廠介面去完成這個建立的工作,這個建立 Bean 的介面命名為 BeanFactory,其提供根據不同條件去建立相對應的 Bean 例項功能(比如 beanId),但是建立的前提是需要先註冊這個 BeanDefinition,然後根據一定條件再從中去獲取 BeanDefinition,根據 單一職責 原則,這個功能應該由一個新的介面去完成,主要是做註冊和獲取 BeanDefinition 的工作,故將其命名為 BeanDefinitionRegistry,我們需要的 BeanDefinition 要從哪裡獲取呢?很顯然我們是基於 XML 配置的方式,當然是從 XML 配置檔案中獲取到的,同樣根據單一職責原則,也需要一個類去完成這個事情,將其命名為 XMLBeanDefinitionReader,這部分的整體結構如下所示:

spring-injection-beanfactory-2.png

接下來面臨的一個問題就是,像 XML 這種配置檔案資源要如何表示呢,這些配置對於程式來說是一種資源,可以統一抽象為 Resource,然後提供一個返回資源對應流(InputStream)物件介面,這種資源可以從專案中獲取、本地檔案獲取甚至是從遠端獲取,它們都是一種 Resource,結構如下:

spring-injection-resource-3.png

最後就是要一個提供去組合呼叫上面的那些類去完成 XML 配置檔案解析為 BeanDefinition 並注入到容器中了的功能,擔任這程式上下文的職責,將其命名為 ApplicationContext,這裡同樣也可以根據 Resource 的型別分為多種不同的類,比如:FileSystmXmlApplicationContext、ClassPathXmlApplicationContext 等,這些內部都有一個將配置檔案轉換為 Resource 的過程,可以使用 模板方法 抽象出一個公共父類抽象類,如下所示:

spring-injection-applicationcontext.png

總結以上分析結果,得出初步類圖設計如下:

spring-injection-all-4.png

最終要實現 Setter 注入這個目標,可以將其分解為以下兩個步驟:

  1. 將 XML 配置檔案中的 標籤解析為 BeanDefinition 並注入到容器中
  2. 實現 Setter 注入

下面我們分為這兩個部分來分別講述如何實現。

配置檔案解析

假設有如下內容的配置檔案 applicationcontext-config1.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.e3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="orderService" class="cn.mghio.service.version1.OrderService" />

</beans>

最終需要解析出一個 id 為 orderService 型別為 cn.mghio.service.version1.OrderService 的 BeanDefinition,翻譯成測試類的話也就是需要讓如下測試類可以執行通過:

/**
 * @author mghio
 */
public class BeanFactoryTest {

    private Resource resource;
    private DefaultBeanFactory beanFactory;
    private XmlBeanDefinitionReader reader;

    @BeforeEach
    public void beforeEach() {
        resource = new ClassPathResource("applicationcontext-config1.xml");
        beanFactory = new DefaultBeanFactory();
        reader = new XmlBeanDefinitionReader(beanFactory);
    }

    @Test
    public void testGetBeanFromXmlFile() {
        reader.loadBeanDefinition(resource);
        BeanDefinition bd = beanFactory.getBeanDefinition("orderService");

        assertEquals("cn.mghio.service.version1.OrderService", bd.getBeanClassNam());
        OrderService orderService = (OrderService) beanFactory.getBean("orderService");
        assertNotNull(orderService);
    }

    @Test
    public void testGetBeanFromXmlFileWithInvalidBeanId() {
        assertThrows(BeanCreationException.class, () -> beanFactory.getBean("notExistsBeanId"));
    }

    @Test
    public void testGetFromXmlFilWithFileNotExists() {
        resource = new ClassPathResource("notExists.xml");
        assertThrows(BeanDefinitionException.class, () -> reader.loadBeanDefinition(resource));
    }

}

可以看到這裡面的關鍵就是如何去實現 XmlBeanDefinitionReader 類的 loadBeanDefinition 從配置中載入和注入 BeanDefinition,思考分析後不然發現這裡主要是兩步,第一步是解析 XML 配置轉換為 BeanDefinition,這就需要上文提到的 dom4j 提供的能力了,第二步將解析出來的 BeanDefinition 注入到容器中,通過組合使用 BeanDefinitionRegistry 介面提供註冊 BeanDefinition 的能力來完成。讀取 XML 配置的類 XmlBeanDefinitionReader 的程式碼實現很快就可以寫出來了,該類部分程式碼如下所示:

/**
 * @author mghio
 */
public class XmlBeanDefinitionReader {

    private static final String BEAN_ID_ATTRIBUTE = "id";
    private static final String BEAN_CLASS_ATTRIBUTE = "class";

    private BeanDefinitionRegistry registry;

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        this.registry = registry;
    }

    @SuppressWarnings("unchecked")
    public void loadBeanDefinition(Resource resource) {
        try (InputStream is = resource.getInputStream()) {
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(is);
            Element root = document.getRootElement();  // <beans>
            Iterator<Element> iterator = root.elementIterator();
            while (iterator.hasNext()) {
                Element element = iterator.next();
                String beanId = element.attributeValue(BEAN_ID_ATTRIBUTE);
                String beanClassName = element.attributeValue(BEAN_CLASS_ATTRIBUTE);
                BeanDefinition bd = new GenericBeanDefinition(beanId, beanClassName);
                this.registry.registerBeanDefinition(beanId, bd);
            }
        } catch (DocumentException | IOException e) {
            throw new BeanDefinitionException("IOException parsing XML document:" + configurationFile, e);
        }
    }
}

然後當呼叫 BeanFactory 的 getBean 方法時就可以根據 Bean 的全限定名建立一個例項出來了(PS:暫時不考慮例項快取),方法實現主要程式碼如下:
```Java
public Object getBean(String beanId) {
    BeanDefinition bd = getBeanDefinition(beanId);
    if (null == bd) {
        throw new BeanCreationException("BeanDefinition does not exists, beanId:" + beanId);
    }
    ClassLoader classLoader = this.getClassLoader();
    String beanClassName = bd.getBeanClassNam();
    try {
        Class<?> clazz = classLoader.loadClass(beanClassName);
        return clazz.newInstance();
    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
        throw new BeanCreationException("Created bean for " + beanClassName + " fail.", e);
    }
}

到這裡配置檔案解析方面的工作已完成,接下來看看要如何實現 Setter 注入。

如何實現 Setter 注入

首先實現基於 XML 配置檔案的 Setter 注入本質上也是解析 XML 配置檔案,然後再呼叫物件屬性的 setXXX 方法將配置的值設定進去,配置檔案 applicationcontext-config2.xml 如下所示:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.e3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="stockDao" class="cn.mghio.dao.version2.StockDao"/>

    <bean id="tradeDao" class="cn.mghio.dao.version2.TradeDao"/>

    <bean id="orderService" class="cn.mghio.service.version2.OrderService">
        <property name="stockDao" ref="stockDao"/>
        <property name="tradeDao" ref="tradeDao"/>
        <property name="num" value="2"/>
        <property name="owner" value="mghio"/>
        <property name="orderTime" value="2020-11-24 18:42:32"/>
    </bean>

</beans>

我們之前使用了 BeanDefinition 去抽象了 標籤,這裡面臨的第一個問題就是要如何去表達配置檔案中的 標籤,其中 ref 屬性表示一個 beanId、value 屬性表示一個值(值型別為:Integer、String、Date 等)。觀察後可以發現, 標籤本質上是一個 K-V 格式的資料(name 作為 Key,ref 和 value 作為 Value),將這個類命名為 PropertyValue,很明顯一個 BeanDefinition 會有多個 PropertyValue,結構如下:

spring-injection-setter-property-1.png

這裡的 value 有兩種不同的型別,一種是表示 Bean 的 id 值,執行時會解析為一個 Bean 的引用,將其命名為 RuntimeBeanReference,還有一種是 String 型別,執行時會解析為不同的型別,將其命名為 TypeStringValue。第二個問題就是要如何將一個型別轉換為另一個型別呢?比如將上面配置中的字串 2 轉換為整型的 2、字串 2020-11-24 18:42:32 轉換為日期,這類通用的問題前輩們已經開發好了類庫處理了,這裡我們使用 commons-beanutils 庫提供的 BeanUtils.copyProperty(final Object bean, final String name, final Object value) 方法即可。然後只需在之前 XmlBeanDefinitionReader 類的 loadBeanDefinition 方法解析 XML 配置檔案的時解析 標籤下的 標籤並設定到 BeanDefinition 的 propertyValues 屬性中;DefaultBeanFactory 中的 getBean 方法分為例項化 Bean 和讀取向例項化完成的 Bean 使用 Setter 注入配置檔案中配置屬性對應的值。XmlBeanDefinitionReader 的 loadBeanDefinition() 方法程式碼修改為:

public void loadBeanDefinition(Resource resource) {
    try (InputStream is = resource.getInputStream()) {
        SAXReader saxReader = new SAXReader();
        Document document = saxReader.read(is);
        Element root = document.getRootElement();  // <beans>
        Iterator<Element> iterator = root.elementIterator();
        while (iterator.hasNext()) {
            Element element = iterator.next();
            String beanId = element.attributeValue(BEAN_ID_ATTRIBUTE);
            String beanClassName = element.attributeValue(BEAN_CLASS_ATTRIBUTE);
            BeanDefinition bd = new GenericBeanDefinition(beanId, beanClassName);
            parsePropertyElementValue(element, bd);  // parse <property>
            this.registry.registerBeanDefinition(beanId, bd);
        }
    } catch (DocumentException | IOException e) {
        throw new BeanDefinitionException("IOException parsing XML document:" + resource, e);
    }
}

private void parsePropertyElementValue(Element element, BeanDefinition bd) {
    Iterator<Element> iterator = element.elementIterator(PROPERTY_ATTRIBUTE);
    while (iterator.hasNext()) {
        Element propertyElement = iterator.next();
        String propertyName = propertyElement.attributeValue(NAME_ATTRIBUTE);
        if (!StringUtils.hasText(propertyName)) {
            return;
        }

        Object value = parsePropertyElementValue(propertyElement, propertyName);
        PropertyValue propertyValue = new PropertyValue(propertyName, value);
        bd.getPropertyValues().add(propertyValue);
    }

}

private Object parsePropertyElementValue(Element propertyElement, String propertyName) {
    String elementName = (propertyName != null) ?
            "<property> element for property '" + propertyName + "' " : "<constructor-arg> element";

    boolean hasRefAttribute = propertyElement.attribute(REF_ATTRIBUTE) != null;
    boolean hasValueAttribute = propertyElement.attribute(VALUE_ATTRIBUTE) != null;

    if (hasRefAttribute) {
        String refName = propertyElement.attributeValue(REF_ATTRIBUTE);
        RuntimeBeanReference ref = new RuntimeBeanReference(refName);
        return ref;
    } else if (hasValueAttribute) {
        String value = propertyElement.attributeValue(VALUE_ATTRIBUTE);
        TypedStringValue valueHolder = new TypedStringValue(value);
        return valueHolder;
    } else {
        throw new RuntimeException(elementName + " must specify a ref or value");
    }
}

DefaultBeanFactory 的 getBean 方法也增加 Bean 屬性注入操作,部分程式碼如下:

public Object getBean(String beanId) {
    BeanDefinition bd = getBeanDefinition(beanId);
    // 1. instantiate bean
    Object bean = instantiateBean(bd);
    // 2. populate bean
    populateBean(bd, bean);
    return bean;
}

private Object instantiateBean(BeanDefinition bd) {
    ClassLoader classLoader = this.getClassLoader();
    String beanClassName = bd.getBeanClassName();
    try {
        Class<?> clazz = classLoader.loadClass(beanClassName);
        return clazz.newInstance();
    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
        throw new BeanCreationException("Created bean for " + beanClassName + " fail.", e);
    }
}

private void populateBean(BeanDefinition bd, Object bean) {
    List<PropertyValue> propertyValues = bd.getPropertyValues();
    if (propertyValues == null || propertyValues.isEmpty()) {
        return;
    }

    BeanDefinitionResolver resolver = new BeanDefinitionResolver(this);
    SimpleTypeConverted converter = new SimpleTypeConverted();
    try {
        for (PropertyValue propertyValue : propertyValues) {
            String propertyName = propertyValue.getName();
            Object originalValue = propertyValue.getValue();
            Object resolvedValue = resolver.resolveValueIfNecessary(originalValue);

            BeanUtils.copyProperty(bean, propertyName, resolvedValue);
        }
    } catch (Exception e) {
        throw new BeanCreationException("Failed to obtain BeanInfo for class [" + bd.getBeanClassName() + "]");
    }
}

至此,簡單的 Setter 注入功能已完成。

總結

本文簡單概述了基於 XML 配置檔案方式的 Setter 注入簡單實現過程,整體實現 Setter 注入的思路就是先設計一個資料結構去表達 XML 配置檔案中的標籤資料(比如上面的 PropertyValue),然後再解析配置檔案填充資料並利用這個資料結構完成一些功能(比如 Setter 注入等)。感興趣的朋友可以到這裡 mghio-spring 檢視完整程式碼。

相關文章