简易版的 Spring 之如何实现 Setter 注入

前言

之前在 上篇 提到过会实现一个简易版的 IoC 和 AOP,今天它终于来了图片。相信对于使用 Java 开发语言的朋友们都使用过或者听说过 Spring 这个开发框架,绝大部分的企业级开发中都离不开它,通过官网可以了解到其生态非常庞大,针对不同方面的开发提供了一些解决方案,可以说 Spring 框架的诞生是对 Java 开发人员的一大福利,自 2004 年发布以来,Spring 为了解决一些企业开发中的痛点先后引入了很多的特性和功能,其中最重要的就是我们经常听到的 IoC 和 AOP 特性,由于涉及到的知识和细节比较多,会分为几篇文章来介绍,今天这篇(也是第一篇)我们来看看如何实现基于 XML 配置方式的 Setter 注入。

预备知识

既然是通过 XML 配置文件的方式,首先第一件事就是要读取 XML 文件然后转换为我们需要的数据结构,解析 XML 文件有但不限于这些方式(JDOM、XOM、DOM4J),这里使用的是简单易上手的 dom4j,所你得对其基础知识有一些简单了解,其实都是一些很简单的方法基础使用而已,第二个就是你要有一些 Spring 框架的使用经验,这里实现的简易版本质上是对 Spring 的一个精简后的核心部分的简单实现,是的,没错,你只需要有了这些基础预备知识就可以了。

基础数据结构抽象

在开始编码实现前先要做一些简单的构思和设计,首先在 Spring 中把一个被其管理的对象称之为 Bean,然后其它的操作都是围绕这个 Bean 来展开设计的,所以为了能在程序中统一并且规范的表示一个 Bean 的定义,于是第一个接口 BeanDefinition 就出来了,本次需要的一些基本信息包含 Bean 的名称、所属类名称、是否单例、作用域等,如下所示:

简易版的 Spring 之如何实现 Setter 注入

现在 BeanDefinition 有了,接下来就是要根据这个 BeanDefinition 去创建出对应的 Bean 实例了,很显然这需要一个 Factory 工厂接口去完成这个创建的工作,这个创建 Bean 的接口命名为 BeanFactory,其提供根据不同条件去创建相对应的 Bean 实例功能(比如 beanId),但是创建的前提是需要先注册这个 BeanDefinition,然后根据一定条件再从中去获取 BeanDefinition,根据 单一职责 原则,这个功能应该由一个新的接口去完成,主要是做注册和获取 BeanDefinition 的工作,故将其命名为 BeanDefinitionRegistry,我们需要的 BeanDefinition 要从哪里获取呢?很显然我们是基于 XML 配置的方式,当然是从 XML 配置文件中获取到的,同样根据单一职责原则,也需要一个类去完成这个事情,将其命名为 XMLBeanDefinitionReader,这部分的整体结构如下所示:

简易版的 Spring 之如何实现 Setter 注入

接下来面临的一个问题就是,像 XML 这种配置文件资源要如何表示呢,这些配置对于程序来说是一种资源,可以统一抽象为 Resource,然后提供一个返回资源对应流(InputStream)对象接口,这种资源可以从项目中获取、本地文件获取甚至是从远程获取,它们都是一种 Resource,结构如下:

简易版的 Spring 之如何实现 Setter 注入

最后就是要一个提供去组合调用上面的那些类去完成 XML 配置文件解析为 BeanDefinition 并注入到容器中了的功能,担任这程序上下文的职责,将其命名为 ApplicationContext,这里同样也可以根据 Resource 的类型分为多种不同的类,比如:FileSystmXmlApplicationContext、ClassPathXmlApplicationContext 等,这些内部都有一个将配置文件转换为 Resource 的过程,可以使用 模板方法 抽象出一个公共父类抽象类,如下所示:
简易版的 Spring 之如何实现 Setter 注入
总结以上分析结果,得出初步类图设计如下:
简易版的 Spring 之如何实现 Setter 注入

最终要实现 Setter 注入这个目标,可以将其分解为以下两个步骤:

将 XML 配置文件中的标签解析为 BeanDefinition 并注入到容器中

实现 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:暂时不考虑实例缓存),方法实现主要代码如下:

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,结构如下:

![](http://www.icode9.com/i/li/?n=4&i=images/blog/202101/26/5acb471e0c63964dd686cd932fbf8979.png?,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

这里的 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 属性注入操作,部分代码如下:
Java
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 (https://github.com/mghio/mghio-spring) 查看完整代码。
上一篇:Spring源码系列(三)BeanDefinition(二)


下一篇:Spring 程序入口和xml解析