簡易版的 Spring 之如何實現 Constructor 注入

玻璃窗起霧了發表於2021-01-27

前言

本文是「如何實現一個簡易版的 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 方法。結構如下圖所示:

解析 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 之實現欄位註解方式注入」。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69992957/viewspace-2753922/,如需轉載,請註明出處,否則將追究法律責任。

相關文章