前言
之前在 上篇 提到過會實現一個簡易版的 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:暫時不考慮例項快取),方法實現主要程式碼如下:
```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 去抽象了
這裡的 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 配置檔案的時解析
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 檢視完整程式碼。