前言
本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML
的简单 Setter
注入,这篇来看看要如何去实现一个简单的 Constructor
注入功能,实现步骤和 Setter
注入是一样的“套路”,先设计一个数据结构去解析表达 XML
配置文件里的信息,然后再使用这些解析好的数据结构做一些事情,比如这里的 Constructor
注入。话不多说,下面我们直接进入正题。
数据结构设计
使用 Constructor
注入方式的 XML
的一种配置如下所示:
以上 OrderService
类如下:
/**
* @author mghio
* @since 2021-01-16
*/
public class OrderService {
private StockDao stockDao;
private TradeDao tradeDao;
private String owner;
public OrderService(StockDao stockDao, TradeDao tradeDao, String owner) {
this.stockDao = stockDao;
this.tradeDao = tradeDao;
this.owner = owner;
}
}
从 XML
的配置结构上看和 Setter
注入类似,都是 Key-Value
类的格式,可以将每个 constructor-arg
节点抽象为 ValueHolder
,包含实际解析后的值类型 value
、类型 type
以及参数名称 name
,如下所示:
/**
* @author mghio
* @since 2021-01-16
*/
public class ValueHolder {
private Object value;
private String type;
private String name;
// omit setter and getter
}
同样一个 Bean
可以包含多个 ValueHolder
,为了封装实现以及方便提供一些判断方法(比如是否配置有构造器注入等),将进一步封装为 ConstructorArgument
,并提供一些 CRUD
接口,而 ValueHolder
作为内部类,如下所示:
/**
* @author mghio
* @since 2021-01-16
*/
public class ConstructorArgument {
private final List argumentsValues = new LinkedList<>();
public void addArgumentValue(Object value) {
this.argumentsValues.add(new ValueHolder(value));
}
public List getArgumentsValues() {
return this.argumentsValues;
}
public int getArgumentCount() {
return this.argumentsValues.size();
}
public boolean isEmpty() {
return this.argumentsValues.isEmpty();
}
public void clear() {
this.argumentsValues.clear();
}
// some other methods...
public static class ValueHolder {
private Object value;
private String type;
private String name;
}
}
然后在 BeanDefinition
接口中增加获取 ConstructorArgument
方法和判断是否配置 ConstructorArgument
方法。结构如下图所示:
解析 XML 配置文件
有了 上篇文章 的基础,解析 XML
也比较简单,这里我们解析的是 constructor-arg
节点,组装数据添加到 BeanDefinition
的 ConstructorArgument
属性中,修改 XmlBeanDefinitionReader
类的 loadBeanDefinition(Resource resource)
方法如下:
/**
* @author mghio
* @since 2021-01-16
*/
public class XmlBeanDefinitionReader {
private static final String CONSTRUCTOR_ARG_ELEMENT = "constructor-arg";
private static final String NAME_ATTRIBUTE = "name";
private static final String TYPE_ATTRIBUTE = "type";
// other fields and methods ...
public void loadBeanDefinition(Resource resource) {
try (InputStream is = resource.getInputStream()) {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(is);
Element root = document.getRootElement(); //
Iterator 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);
if (null != element.attributeValue(BEAN_SCOPE_ATTRIBUTE)) {
bd.setScope(element.attributeValue(BEAN_SCOPE_ATTRIBUTE));
}
// parse node
parseConstructorArgElements(element, bd);
parsePropertyElementValues(element, bd);
this.registry.registerBeanDefinition(beanId, bd);
}
} catch (DocumentException | IOException e) {
throw new BeanDefinitionException("IOException parsing XML document:" + resource, e);
}
}
private void parseConstructorArgElements(Element rootEle, BeanDefinition bd) {
Iterator iterator = rootEle.elementIterator(CONSTRUCTOR_ARG_ELEMENT);
while (iterator.hasNext()) {
Element element = iterator.next();
parseConstructorArgElement(element, bd);
}
}
private void parseConstructorArgElement(Element element, BeanDefinition bd) {
String typeAttr = element.attributeValue(TYPE_ATTRIBUTE);
String nameAttr = element.attributeValue(NAME_ATTRIBUTE);
Object value = parsePropertyElementValue(element, null);
ConstructorArgument.ValueHolder valueHolder = new ConstructorArgument.ValueHolder(value);
if (StringUtils.hasLength(typeAttr)) {
valueHolder.setType(typeAttr);
}
if (StringUtils.hasLength(nameAttr)) {
valueHolder.setName(nameAttr);
}
bd.getConstructorArgument().addArgumentValue(valueHolder);
}
// other fields and methods ...
}
解析 XML
的过程整体上分为两步,第一步在遍历每个
节点时判断
节点是否存在,存在则解析
节点;第二步将解析拼装好的 ValueHolder
添加到 BeanDefinition
中,这样我们就把 XML
配置的 Constructor
注入解析到 BeanDefinition
中了,下面看看如何在创建 Bean
的过程中如何使用该数据结构进行构造器注入。
如何选择 Constructor
很明显,使用构造器注入需要放在实例化 Bean
的阶段,通过判断当前待实例化的 Bean
是否有配置构造器注入,有则使用构造器实例化。判断 XML
是否有配置构造器注入可以直接使用 BeanDefinition
提供的 hasConstructorArguments()
方法即可,实际上最终是通过判断 ConstructorArgument.ValueHolder
集合是否有值来判断的。这里还有个问题 当存在多个构造器时如何选择
,比如 OrderService
类有如下三个构造函数:
/**
* @author mghio
* @since 2021-01-16
*/
public class OrderService {
private StockDao stockDao;
private TradeDao tradeDao;
private String owner;
public OrderService(StockDao stockDao, TradeDao tradeDao) {
this.stockDao = stockDao;
this.tradeDao = tradeDao;
this.owner = "nobody";
}
public OrderService(StockDao stockDao, String owner) {
this.stockDao = stockDao;
this.owner = owner;
}
public OrderService(StockDao stockDao, TradeDao tradeDao, String owner) {
this.stockDao = stockDao;
this.tradeDao = tradeDao;
this.owner = owner;
}
}
其 XML
构造器注入的配置如下:
这时该如何选择最适合的构造器进行注入呢?这里使用的匹配方法是 1. 先判断构造函数参数个数,如果不匹配直接跳过,进行下一次循环;2. 当构造器参数个数匹配时再判断参数类型,如果和当前参数类型一致或者是当前参数类型的父类型则使用该构造器进行实例化
。这个使用的判断方法比较简单直接,实际上 Spring
的判断方式考虑到的情况比较全面同时代码实现也更加复杂,感兴趣的朋友可以查看 org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(...)
方法。这里需要注意的是,在解析 XML
配置的构造器注入参数时要进行类型转换为目标类型,将该类命名为 ConstructorResolver
,实现代码比较多这里就不贴出来了,可以到 GitHub 查看完整代码。然后只需要在实例化 Bean
的时候判断是否存在构造器注入配置,存在则使用构造器注入即可,修改 DefaultBeanFactory
的实例化方法如下:
/**
* @author mghio
* @since 2021-01-16
*/
public class DefaultBeanFactory extends DefaultSingletonBeanRegistry implements ConfigurableBeanFactory,
BeanDefinitionRegistry {
// other fields and methods ...
private Object doCreateBean(BeanDefinition bd) {
// 1. instantiate bean
Object bean = instantiateBean(bd);
// 2. populate bean
populateBean(bd, bean);
return bean;
}
private Object instantiateBean(BeanDefinition bd) {
// 判断当前 Bean 的 `XML` 配置是否配置为构造器注入方式
if (bd.hasConstructorArguments()) {
ConstructorResolver constructorResolver = new ConstructorResolver(this);
return constructorResolver.autowireConstructor(bd);
} else {
ClassLoader classLoader = this.getClassLoader();
String beanClassName = bd.getBeanClassName();
try {
Class> beanClass = null;
Class> cacheBeanClass = bd.getBeanClass();
if (cacheBeanClass == null) {
beanClass = classLoader.loadClass(beanClassName);
bd.setBeanClass(beanClass);
} else {
beanClass = cacheBeanClass;
}
return beanClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new BeanCreationException("Created bean for " + beanClassName + " fail.", e);
}
}
}
// other fields and methods ...
}
到这里就已经实现了一个简易版的基于 XML
配置的 Constructor
注入了。
总结
本文简要介绍了 Spring
基于 XML
配置的 Constructor
注入,其实有了第一篇的 Setter
注入的基础,实现 Constructor
注入相对来说难度要小很多,这里的实现相对来说比较简单,但是其思想和大体流程是类似的,想要深入了解 Spring
实现的具体细节可以查看源码。完整代码已上传至 GitHub
,感兴趣的朋友可以到这里 mghio-spring 查看完整代码,下篇预告:「如何实现一个简易版的 Spring - 实现字段注解方式注入」。