How to implement Constructor injection in a simple version of Spring

Preface

This article is the second article in the "How to implement a simple version of Spring" series. The first article introduces how to implement a simple XML-based Setter injection. This article will take a look at how to implement a simple Constructor injection function. The steps are the same as Setter injection. First, design a data structure to parse and express the information in the XML configuration file, and then use these parsed data structures to do some things, such as Constructor injection here. Not much to say, let's go directly to the topic.

Data structure design

A configuration of XML that uses Constructor injection is as follows:


<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>

The above OrderService class is as follows:

/**
 * @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;
    }
}

From the configuration structure of XML, it is similar to Setter injection. It is in the format of Key-Value. Each constructor-arg node can be abstracted as ValueHolder, including the actual parsed value type value, type type and parameter name name, as follows Shown:


/**
 * @author mghio
 * @since 2021-01-16
 */
public class ValueHolder {
    private Object value;
    private String type;
    private String name;

    // omit setter and getter 
}

The same Bean can contain multiple ValueHolders. In order to encapsulate the implementation and provide some judgment methods (such as whether it is configured with constructor injection, etc.), it will be further encapsulated as ConstructorArgument and provide some CRUD interfaces, and ValueHolder as an internal class, as shown below :

/**
 * @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;
    }
}

Then add the Get ConstructorArgument method and determine whether to configure ConstructorArgument method in the BeanDefinition interface. The structure is shown in the figure below:

How to implement Constructor injection in a simple version of Spring

Parse the XML configuration file

With the basis of the previous article, parsing XML is relatively simple. Here we are parsing the constructor-arg node, adding the assembly data to the ConstructorArgument property of BeanDefinition, and modifying the loadBeanDefinition(Resource resource) method of the XmlBeanDefinitionReader class as follows:


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

}

The process of parsing XML is divided into two steps as a whole. The first step is to determine whether the node exists when traversing each node, and the node is parsed if it exists; the second step is to add the parsed and assembled ValueHolder to the BeanDefinition, so that we configure the XML The Constructor injection parsed into BeanDefinition, let’s see how to use this data structure for constructor injection in the process of creating Bean.

How to choose Constructor

Obviously, the use of constructor injection needs to be placed in the stage of instantiating the Bean, by judging whether the currently to be instantiated Bean has configuration constructor injection, and if it is, use the constructor to instantiate. To determine whether there is a configuration constructor injection in XML, you can directly use the hasConstructorArguments() method provided by BeanDefinition. In fact, it is ultimately determined by determining whether the ConstructorArgument.ValueHolder collection has a value. There is also a question how to choose when there are multiple constructors. For example, the OrderService class has the following three constructors:


/**
 * @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;
    }
}

The configuration injected by the XML constructor is as follows:


<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>

How to choose the most suitable constructor for injection? The matching method used here is 1. First judge the number of constructor parameters, if it does not match, skip directly and proceed to the next cycle; 2. When the number of constructor parameters match, then judge the parameter type, if it is consistent with the current parameter type or If it is the super type of the current parameter type, use this constructor for instantiation. The judgment method used is relatively simple and straightforward. In fact, Spring's judgment method considers the situation more comprehensively and the code implementation is also more complicated. If you are interested, please check org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(.. .) Method. It should be noted here that when parsing the constructor injection parameters of the XML configuration, the type must be converted to the target type, and the class must be named ConstructorResolver. There are many implementation codes and I will not post it here. You can check the complete code on GitHub. Then you only need to determine whether there is a constructor injection configuration when instantiating the Bean. If it exists, use the constructor injection. Modify the instantiation method of DefaultBeanFactory as follows:


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

}

So far, a simple version of Constructor injection based on XML configuration has been implemented.

to sum up

This article briefly introduces Spring's Constructor injection based on XML configuration. In fact, with the foundation of Setter injection in the first article, it is much less difficult to implement Constructor injection. The implementation here is relatively simple, but its thought and general process It is similar. If you want to know more about the specific details of Spring implementation, you can check the source code. The complete code has been uploaded to GitHub. If you are interested, you can go to mghio-spring to view the complete code. The next preview: "Simplified version of Spring implements field annotation method injection".

Guess you like

Origin blog.51cto.com/15075507/2607606