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

前言

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

数据结构设计

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

<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 类如下:

/**
 * @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<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-constructor-injection.png

解析 XML 配置文件

有了 上篇文章 的基础,解析 XML 也比较简单,这里我们解析的是 constructor-arg 节点,组装数据添加到 BeanDefinitionConstructorArgument 属性中,修改 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 的过程整体上分为两步,第一步在遍历每个 <bean> 节点时判断 <constructor-arg> 节点是否存在,存在则解析 <constructor-arg> 节点;第二步将解析拼装好的 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 构造器注入的配置如下:

<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 - 实现字段注解方式注入」

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容