如何實現一個簡易版的 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 節點,組裝數據添加到 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 類有如下三個構造函數:

/**
 * @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 – 實現字段註解方式注入」