前言

之前在 上篇 提到过会实现一个简易版的 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 的名称、所属类名称、是否单例、作用域等,如下所示:

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

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

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

总结以上分析结果,得出初步类图设计如下:

最终要实现 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,结构如下:![](https://s4.51cto.com/images/blog/202101/26/5acb471e0c63964dd686cd932fbf8979.png?x-oss-process=image/watermark,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) 查看完整代码。
©著作权归作者所有:来自51CTO博客作者mb5ff2f19eb6087的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
  2. android部分控件应用解析
  3. Android(安卓)解析json对象,存放到List中
  4. Android(安卓)init.rc文件解析过程详解
  5. Android(安卓)手机Root 原理解析
  6. 转载-runOnUIThread解析
  7. Dagger2 基础入门与使用
  8. Android(安卓)源码解析之Adapter和AdapterView与适配器模式
  9. Android中铃声总结【安卓源码解析一】

随机推荐

  1. 自学编程的八大误区!克服它!
  2. webpack4配置详解之慢嚼细咽
  3. “狗屁不通文章生成器”项目登顶GitHub热
  4. 005. 最长回文子串 | Leetcode题解
  5. 大家好 这就是2018年的我~
  6. Linux环境都没有,怎么学编程?憋说了,肝!(保姆
  7. 006. Z 字形变换 | Leetcode题解
  8. eruda 一个被人遗忘的调试神器
  9. Linux性能优化(五)——性能监控工具
  10. 小姐姐对不喜欢的男孩到底有多残忍?