前言
之前在 上篇 提到过会实现一个简易版的 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
配置文件中的<bean>
标签解析为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
去抽象了 <bean>
标签,这里面临的第一个问题就是要如何去表达配置文件中的 <property>
标签,其中 ref
属性表示一个 beanId
、value
属性表示一个值(值类型为:Integer
、String
、Date
等)。观察后可以发现,<property>
标签本质上是一个 K-V
格式的数据(name
作为 Key
,ref
和 value
作为 Value
),将这个类命名为 PropertyValue
,很明显一个 BeanDefinition
会有多个 PropertyValue
,结构如下:
这里的 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
配置文件的时解析 <bean>
标签下的 <property>
标签并设置到 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 查看完整代码。