Handwritten simple version of Spring framework: design and implementation of resource loader, from Spring XML parsing and registering Bean objects

Posted by prcollin on Tue, 21 Dec 2021 00:17:17 +0100

target

After completing the prototype of Spring framework, we can now manually operate the definition, registration and property filling of Bean objects through unit testing, and finally obtain the object and call methods. However, there will be a problem here, that is, if you actually use the Spring framework, it is unlikely to let users create it manually, but it is best to simplify the creation process through configuration files. The following operations need to be completed:

As shown in the figure, we need to integrate steps 2, 3 and 4 into the Spring framework and instantiate the Bean object through the Spring configuration file. Next, we need to add operations that can solve the reading, parsing and registering beans of Spring configuration in the existing Spring framework.

Design

According to the requirements background of this chapter, we need to add a resource parser to the existing Spring framework prototype, that is, it can read the configuration contents of classpath, local files and cloud files. These configuration contents are just like the Spring configuration when using Spring Like XML, it will include the description and attribute information of the Bean object. After reading the configuration file information, the next step is to parse the Bean description information in the configuration file and register the Bean object in the Spring container. The overall design structure is shown in the figure below:

  • The resource loader is a relatively independent part. It is located in the IO implementation content under the core package of the Spring framework. It is mainly used to process file information in Class, local and cloud environments.
  • When the resource can be loaded, the next step is to parse and register the Bean into Spring. This part of the implementation needs to
    DefaultListableBeanFactory is combined with the core class, because all your registered actions after parsing will integrate the Bean
    Put the definition information into this class.
  • Then, when implementing, we should design the implementation hierarchy of the interface, including defining the reading interface BeanDefinitionReader defined by the Bean and the corresponding implementation class, and complete the parsing and registration of the Bean object in the implementation class.

realization

Structure diagram of classes involved in this chapter

  • This chapter is to give the definition, registration and initialization of beans to Spring For xml configuration processing, you need to implement two parts: Resource loader and xml Resource processing class. The implementation process is mainly based on the implementation of the interfaces Resource and ResourceLoader. In addition, the BeanDefinitionReader interface is the specific use of resources and registers the configuration information in the Spring container.
  • The implementation of Resource loader includes ClassPath, system file and cloud configuration file. These three parts are consistent with the design and implementation in Spring source code. Finally, specific calls are made in DefaultResourceLoader.
  • Interface: BeanDefinitionReader, abstract class: AbstractBeanDefinitionReader, implementation class: XmlBeanDefinitionReader. These three parts mainly deal with the registration Bean container operation after resource reading reasonably and clearly. Interface management definition, the abstract class handles the filling of registered Bean components outside the non interface functions, and the final implementation class only cares about the specific business implementation.

In addition, this chapter also refers to the Spring source code and makes the relationship between the integration and implementation of corresponding interfaces. Although these interfaces do not play a great role at present, they will also play a role with the gradual improvement of the framework:

  • BeanFactory, the existing Bean factory interface is used to obtain Bean objects. This time, a new method to obtain beans by type is added: T getBean(String name, Class requiredType)
  • ListableBeanFactory is an interface that extends the Bean factory interface. getBeansOfType and getBeanDefinitionNames() methods are added. There are other extension methods in the Spring source code.
  • Hierarchical BeanFactory, in the Spring source code, provides a method that can obtain the parent class BeanFactory. It is a layer sub interface that extends the factory.
  • AutowireCapableBeanFactory is an interface for automatically processing Bean factory configuration. At present, the corresponding implementation has not been made in the case project, and it will be gradually improved in the future.
  • ConfigurableBeanFactory, which can obtain a configured interface of BeanPostProcessor, BeanClassLoader, etc.
  • ConfigurableListableBeanFactory provides an operation interface for analyzing and modifying beans and pre instantiation, but there is only one getBeanDefinition method at present.

Definition and implementation of resource loading interface

package com.qingyun.springframework.core.io;

import java.io.IOException;
import java.io.InputStream;

/**
 * @description:  It has Bean defined resources and related operations
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:06
 **/
public interface Resource {
    /**
     * Get input stream from resource
     * @return Input stream
     * @throws IOException IO abnormal
     */
    InputStream getInputStream() throws IOException;
}

Create core. Net under the Spring framework IO core package, which is mainly used to process Resource loading streams. Define the Resource interface and provide the method to obtain the InputStream stream stream. Next, implement three different stream file operations: classPath, FileSystem and URL.

package com.qingyun.springframework.core.io;

import cn.hutool.core.lang.Assert;
import com.qingyun.springframework.util.ClassUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * @description:  Resources are stored in the classpath
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:08
 **/
public class ClassPathResource implements Resource {

    private final String path;

    private final ClassLoader classLoader;

    public ClassPathResource(String path) {
        this(path, null);
    }

    public ClassPathResource(String path, ClassLoader classLoader) {
        Assert.notNull(path, "Path must not be null");
        this.path = path;
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }

    @Override
    public InputStream getInputStream() throws IOException {
        //  Get input stream using class loader
        InputStream is = classLoader.getResourceAsStream(path);
        if (is == null) {
            throw new FileNotFoundException(
                    this.path + " cannot be opened because it does not exist");
        }
        return is;
    }
}

The implementation of this part is used to read the file information under ClassPath through ClassLoader. The specific reading process is mainly: ClassLoader getResourceAsStream(path).

package com.qingyun.springframework.core.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @description:  Resources are stored in files
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:14
 **/
public class FileSystemResource implements Resource {

    private final File file;

    private final String path;

    public FileSystemResource(File file) {
        this.file = file;
        this.path = file.getPath();
    }

    public FileSystemResource(String path) {
        this.file = new File(path);
        this.path = path;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new FileInputStream(this.file);
    }

    public final String getPath() {
        return this.path;
    }

}

You must be very familiar with reading file information by specifying the file path. You often read some txt and excel files and output them to the console.

package com.qingyun.springframework.core.io;

import cn.hutool.core.lang.Assert;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

/**
 * @description:  Resources are stored on the network
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:15
 **/
public class UrlResource implements Resource{

    private final URL url;

    public UrlResource(URL url) {
        Assert.notNull(url,"URL must not be null");
        this.url = url;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection con = this.url.openConnection();
        try {
            return con.getInputStream();
        } catch (IOException ex){
            throw ex;
        } finally {
            if (con instanceof HttpURLConnection){
                ((HttpURLConnection) con).disconnect();
            }
        }
    }

}

Read the cloud service file through HTTP, and we can also put the configuration file on GitHub or Gitee.

Packaging resource loader

According to different resource loading methods, the resource loader can concentrate these methods under a unified class service for processing. External users only need to pass the resource address to simplify use.

package com.qingyun.springframework.core.io;

/**
 * @description:  The appearance mode is used to mask the details of the Resource interface implementation system, so that the client does not need to know which Resource implementation classes are available
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:18
 **/
public interface ResourceLoader {

    //  Prefix of classpath
    String CLASSPATH_URL_PREFIX = "classpath:";

    /**
     * Obtain a corresponding Resource according to the Resource address location entered by the user
     * @param location Resource address
     * @return Resource
     */
    Resource getResource(String location);
}

Define the resource acquisition interface, where you can pass the location address.

package com.qingyun.springframework.core.io;

import cn.hutool.core.lang.Assert;

import java.net.MalformedURLException;
import java.net.URL;

/**
 * @description: 
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:26
 **/
public class DefaultResourceLoader implements ResourceLoader {

    @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
        //  The classpath resource parser is returned when location starts with the classpath prefix
        if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()));
        } else {
            try {
                URL url = new URL(location);
                return new UrlResource(url);
            } catch (MalformedURLException e) {
                return new FileSystemResource(location);
            }
        }
    }

}

In the implementation of obtaining resources, three different types of resource processing methods are packaged, which are divided into: judging whether it is ClassPath, URL and file. Although the implementation of the DefaultResourceLoader class is simple, it is also the specific result of the design pattern convention. For example, the external call will not know too many details, but only care about the specific call result.

Bean definition read interface

package com.qingyun.springframework.beans.factory.support;

import com.qingyun.springframework.beans.BeansException;
import com.qingyun.springframework.core.io.Resource;
import com.qingyun.springframework.core.io.ResourceLoader;

/**
 * @description:  Parse the bean definition from the bean defined resource and register it
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:29
 **/
public interface BeanDefinitionReader {

    /**
     * Get instance registry
     */
    BeanDefinitionRegistry getRegistry();

    /**
     * Get resource loader
     */
    ResourceLoader getResourceLoader();

    /**
     * Parse and create the bean definition information from the resource and register it in the registry
     */
    void loadBeanDefinitions(Resource resource) throws BeansException;

    void loadBeanDefinitions(Resource... resources) throws BeansException;

    void loadBeanDefinitions(String location) throws BeansException;
}

This is a Simple interface for bean definition readers In fact, there are only several methods defined, including getRegistry(), getResourceLoader(), and three methods for loading Bean definitions. It should be noted here that getRegistry() and getResourceLoader() are tools provided for the following three methods to load and register. The implementation of these two methods will be wrapped in abstract classes to avoid polluting the specific interface implementation methods.

Bean definition abstract class implementation

package com.qingyun.springframework.beans.factory.support;

import com.qingyun.springframework.core.io.DefaultResourceLoader;
import com.qingyun.springframework.core.io.ResourceLoader;

/**
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:38
 **/
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private final BeanDefinitionRegistry registry;

    private final ResourceLoader resourceLoader;

    protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        this(registry, new DefaultResourceLoader());
    }

    public AbstractBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        this.registry = registry;
        this.resourceLoader = resourceLoader;
    }

    @Override
    public BeanDefinitionRegistry getRegistry() {
        return registry;
    }

    @Override
    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }

}

The abstract class implements the first two methods of the BeanDefinitionReader interface, and provides a constructor for external callers to inject the Bean definition into the class and pass it in. In this way, the Bean information in the parsed XML file can be registered in the Spring container in the specific implementation class of the interface BeanDefinitionReader. In the past, we used to call BeanDefinitionRegistry to complete the registration of beans through unit tests. Now we can put them into XML for operation

Parsing XML processing Bean registration

package com.qingyun.springframework.beans.factory.xml;

import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import com.qingyun.springframework.beans.BeansException;
import com.qingyun.springframework.beans.factory.PropertyValue;
import com.qingyun.springframework.beans.factory.config.BeanDefinition;
import com.qingyun.springframework.beans.factory.config.BeanReference;
import com.qingyun.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import com.qingyun.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.qingyun.springframework.core.io.Resource;
import com.qingyun.springframework.core.io.ResourceLoader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.io.InputStream;

/**
 * @description:  When the resource resource defining the bean is an Xml file, parse the Xml file, generate the bean definition information, and then register it
 * @author: Zhang Qingyun
 * @create: 2021-08-20 10:46
 **/
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }

    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry, ResourceLoader resourceLoader) {
        super(registry, resourceLoader);
    }

    @Override
    public void loadBeanDefinitions(Resource resource) throws BeansException {
        try (InputStream inputStream = resource.getInputStream()) {
            doLoadBeanDefinitions(inputStream);
        } catch (IOException | ClassNotFoundException e) {
            throw new BeansException("IOException parsing XML document from " + resource, e);
        }
    }

    @Override
    public void loadBeanDefinitions(Resource... resources) throws BeansException {
        for (Resource resource : resources) {
            loadBeanDefinitions(resource);
        }
    }

    @Override
    public void loadBeanDefinitions(String location) throws BeansException {
        ResourceLoader resourceLoader = getResourceLoader();
        Resource resource = resourceLoader.getResource(location);
        loadBeanDefinitions(resource);
    }

    /**
     * Parse the XML file and complete the registration of Bean definition information
     */
    protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException {
        Document doc = XmlUtil.readXML(inputStream);
        Element root = doc.getDocumentElement();
        NodeList childNodes = root.getChildNodes();

        for (int i = 0; i < childNodes.getLength(); i++) {
            // Judgment element
            if (!(childNodes.item(i) instanceof Element)) continue;
            // Judgment object
            if (!"bean".equals(childNodes.item(i).getNodeName())) continue;

            // Parse label
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String className = bean.getAttribute("class");
            // Get Class to get the name in the Class
            Class<?> clazz = Class.forName(className);
            // Priority ID > name
            String beanName = StrUtil.isNotEmpty(id) ? id : name;
            if (StrUtil.isEmpty(beanName)) {
                beanName = StrUtil.lowerFirst(clazz.getSimpleName());
            }

            // Define Bean
            BeanDefinition beanDefinition = new BeanDefinition(clazz);
            // Read properties and populate
            for (int j = 0; j < bean.getChildNodes().getLength(); j++) {
                if (!(bean.getChildNodes().item(j) instanceof Element)) continue;
                if (!"property".equals(bean.getChildNodes().item(j).getNodeName())) continue;
                // Parsing tag: property
                Element property = (Element) bean.getChildNodes().item(j);
                String attrName = property.getAttribute("name");
                String attrValue = property.getAttribute("value");
                String attrRef = property.getAttribute("ref");
                // Get attribute value: import object, value object
                Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
                // Create attribute information
                PropertyValue propertyValue = new PropertyValue(attrName, value);
                beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
            }
            //  Judge whether the Bean has been defined
            if (getRegistry().containsBeanDefinition(beanName)) {
                throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // Register BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }

}

The core content of the XmlBeanDefinitionReader class is the parsing of XML files, which puts our original operations in the code into the way of automatic registration by parsing XML.

The loadBeanDefinitions method handles resource loading. Here, an internal method is added: doLoadBeanDefinitions, which is mainly responsible for parsing xml. In the doLoadBeanDefinitions method, it is mainly used to read xml xmlutil Readxml (InputStream) and Element parsing. In the process of parsing, the Bean configuration and the id, name, class, value and ref information in the configuration are obtained through circular operation. Finally, create the read configuration information into BeanDefinition and PropertyValue, and finally register the complete Bean definition content to the Bean container: getregistry() registerBeanDefinition(beanName, BeanDefinition)

test

Prepare in advance

package com.qingyun.springframework.beansTest;

import java.util.HashMap;
import java.util.Map;

/**
 * @description: 
 * @author: Zhang Qingyun
 * @create: 2021-08-19 00:13
 **/
public class UserDao {
    private static Map<String, String> hashMap = new HashMap<>();

    static {
        hashMap.put("10001", "Mr. Qingyun");
        hashMap.put("10002", "Eight cups of water");
        hashMap.put("10003", "A Mao");
    }

    public String queryUserName(String uId) {
        return hashMap.get(uId);
    }
}

package com.qingyun.springframework.beansTest;

/**
 * @description: 
 * @author: Zhang Qingyun
 * @create: 2021-08-18 22:54
 **/
public class UserService {
    private String uId;

    private UserDao userDao;

    public UserService() {
    }

    public void queryUserInfo() {
        System.out.println("Query user information:" + userDao.queryUserName(uId));
    }
}

test case

    @Test
    public void test_xml() {
        // 1. Initialize BeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 2. Read configuration file & register Bean
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions(new DefaultResourceLoader().getResource("classpath:spring.xml"));

        // 3. Get Bean object and call method
        UserService userService = beanFactory.getBean("userService", UserService.class);
        userService.queryUserInfo();
    }

XML file:

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userDao" class="com.qingyun.springframework.beansTest.UserDao"/>

    <bean id="userService" class="com.qingyun.springframework.beansTest.UserService">
        <property name="uId" value="10001"/>
        <property name="userDao" ref="userDao"/>
    </bean>

</beans>

result

Project code Github address: https://github.com/Zhang-Qing-Yun/mini-spring , the commit ID corresponding to the code in this section is ff6db8d

Welcome to Biao Xing

Topics: Java Spring