如何实现一个简易版的 Spring - 如何实现 Constructor 注入

前言

本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML 的简单 Setter 注入,这篇来看看要如何去实现一个简单的 Constructor 注入功能,实现步骤和 Setter 注入是同样的“套路”,先设计一个数据结构去解析表达 XML 配置文件里的信息,而后再使用这些解析好的数据结构作一些事情,好比这里的 Constructor 注入。话很少说,下面咱们直接进入正题。html

数据结构设计

使用 Constructor 注入方式的 XML 的一种配置以下所示:java

<bean id="orderService" class="cn.mghio.service.version3.OrderService">
    <constructor-arg ref="stockService"/>
    <constructor-arg ref="tradeService"/>
    <constructor-arg type="java.lang.String" value="mghio"/>
</bean>

以上 OrderService 类以下:node

/**
 * @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,以下所示:git

/**
 * @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 做为内部类,以下所示:github

/**
 * @author mghio
 * @since 2021-01-16
 */
public class ConstructorArgument {

    private final List<ValueHolder> argumentsValues = new LinkedList<>();

    public void addArgumentValue(Object value) {
        this.argumentsValues.add(new ValueHolder(value));
    }

    public List<ValueHolder> 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 方法。结构以下图所示:spring

spring-constructor-injection.png

解析 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();  // <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);
                if (null != element.attributeValue(BEAN_SCOPE_ATTRIBUTE)) {
                    bd.setScope(element.attributeValue(BEAN_SCOPE_ATTRIBUTE));
                }
                // parse <constructor-arg> 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<Element> 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 类有以下三个构造函数:post

/**
 * @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 构造器注入的配置以下:this

<bean id="orderService" class="cn.mghio.service.version3.OrderService">
    <constructor-arg ref="stockService"/>
    <constructor-arg ref="tradeService"/>
    <constructor-arg type="java.lang.String" value="mghio"/>
</bean>

这时该如何选择最适合的构造器进行注入呢?这里使用的匹配方法是 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 - 实现字段注解方式注入」

相关文章
相关标签/搜索