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

前言

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

<!--more-->git

預備知識

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

基礎數據結構抽象

在開始編碼實現前先要作一些簡單的構思和設計,首先在 Spring 中把一個被其管理的對象稱之爲 Bean,而後其它的操做都是圍繞這個 Bean 來展開設計的,因此爲了能在程序中統一而且規範的表示一個 Bean 的定義,因而第一個接口 BeanDefinition 就出來了,本次須要的一些基本信息包含 Bean 的名稱、所屬類名稱、是否單例、做用域等,以下所示:spring

spring-injection-beandefinition-1.png

如今 BeanDefinition 有了,接下來就是要根據這個 BeanDefinition 去建立出對應的 Bean 實例了,很顯然這須要一個 Factory 工廠接口去完成這個建立的工做,這個建立 Bean 的接口命名爲 BeanFactory,其提供根據不一樣條件去建立相對應的 Bean 實例功能(好比 beanId),可是建立的前提是須要先註冊這個 BeanDefinition,而後根據必定條件再從中去獲取 BeanDefinition,根據 單一職責 原則,這個功能應該由一個新的接口去完成,主要是作註冊和獲取 BeanDefinition 的工做,故將其命名爲 BeanDefinitionRegistry,咱們須要的 BeanDefinition 要從哪裏獲取呢?很顯然咱們是基於 XML 配置的方式,固然是從 XML 配置文件中獲取到的,一樣根據單一職責原則,也須要一個類去完成這個事情,將其命名爲 XMLBeanDefinitionReader,這部分的總體結構以下所示:apache

spring-injection-beanfactory-2.png

接下來面臨的一個問題就是,像 XML 這種配置文件資源要如何表示呢,這些配置對於程序來講是一種資源,能夠統一抽象爲 Resource,而後提供一個返回資源對應流(InputStream)對象接口,這種資源能夠從項目中獲取、本地文件獲取甚至是從遠程獲取,它們都是一種 Resource,結構以下:緩存

spring-injection-resource-3.png

最後就是要一個提供去組合調用上面的那些類去完成 XML 配置文件解析爲 BeanDefinition 並注入到容器中了的功能,擔任這程序上下文的職責,將其命名爲 ApplicationContext,這裏一樣也能夠根據 Resource 的類型分爲多種不一樣的類,好比:FileSystmXmlApplicationContextClassPathXmlApplicationContext 等,這些內部都有一個將配置文件轉換爲 Resource 的過程,可使用 模板方法 抽象出一個公共父類抽象類,以下所示:數據結構

spring-injection-applicationcontext.png

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

spring-injection-all-4.png

最終要實現 Setter 注入這個目標,能夠將其分解爲如下兩個步驟:框架

  1. XML 配置文件中的 <bean> 標籤解析爲 BeanDefinition 並注入到容器中
  2. 實現 Setter 注入

下面咱們分爲這兩個部分來分別講述如何實現。dom

配置文件解析

假設有以下內容的配置文件 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>

最終須要解析出一個 idorderService 類型爲 cn.mghio.service.version1.OrderServiceBeanDefinition,翻譯成測試類的話也就是須要讓以下測試類能夠運行經過:

/**
 * @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);
        }
    }
}

而後當調用 BeanFactorygetBean 方法時就能夠根據 Bean 的全限定名建立一個實例出來了(PS:暫時不考慮實例緩存),方法實現主要代碼以下:

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 去抽象了 <bean> 標籤,這裏面臨的第一個問題就是要如何去表達配置文件中的 <property> 標籤,其中 ref 屬性表示一個 beanIdvalue 屬性表示一個值(值類型爲:IntegerStringDate 等)。觀察後能夠發現,<property> 標籤本質上是一個 K-V 格式的數據(name 做爲 Keyrefvalue 做爲 Value),將這個類命名爲 PropertyValue,很明顯一個 BeanDefinition 會有多個 PropertyValue,結構以下:

spring-injection-setter-property-1.png

這裏的 value 有兩種不一樣的類型,一種是表示 Beanid 值,運行時會解析爲一個 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 配置文件的時解析 <bean> 標籤下的 <property> 標籤並設置到 BeanDefinitionpropertyValues 屬性中;DefaultBeanFactory 中的 getBean 方法分爲實例化 Bean 和讀取向實例化完成的 Bean 使用 Setter 注入配置文件中配置屬性對應的值。XmlBeanDefinitionReaderloadBeanDefinition() 方法代碼修改成:

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");
    }
}

DefaultBeanFactorygetBean 方法也增長 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 查看完整代碼。

相關文章
相關標籤/搜索