Hand roll Spring framework, design and implement resource loader, from Spring XML parsing and registering Bean objects

Posted by yddib on Mon, 31 Jan 2022 14:49:24 +0100


Author: Xiao Fu Ge
Blog: https://bugstack.cn

Precipitate, share and grow, so that you and others can gain something! 😄

1, Foreword

Love shopping https://m.cqfenfa.com/

Can the code you write meet the product and demand?

Yes, it can be connected. It's OK to connect several times, even if it's just a class and a piece of if Else is OK! However, it is not certain what kind of work will be completed, and whether there will be an accident is not controllable.

When the accident happened, you said because I wrote if Too many else leads to bad code, but you have to move first: you have to add the demand you said, the boss asked you to go online, and the contracts you said have been signed. I have no way to move the brick code farmers, so I can level the demand with the pile of code. If there are too many needs, it is difficult to do, so I can level the demand with the move of bricks! If the princes refuse to obey, I will subdue the princes with soldiers. If you refuse, I will fight you!

But sometimes the code is rotten not because of the fast demand or the rush to go online. Because often, the design of a functional logic will not be too complex or urgent several times before undertaking the product requirements, and you will even set aside time for design, review and development. If you still can't assess what may happen in the future into the requirements at this time, the confusion of the code has been buried from the beginning, In the future, it can only become more and more chaotic!

It is the result of comprehensive factors such as the understanding of requirements, the experience of product scenario development and the ability to control the implementation of code practice. Just like the development you are doing now, what are your codes that change frequently, which are fixed and general, which are responsible for logical assembly and which are for core implementation. Now, if your core common layer makes frequent changes in business layer packaging, it is certain that your code will become more and more chaotic, and may even bury the risk of accidents!

In the Spring framework we implemented, each chapter will continue to expand functions in combination with the previous chapter, just like adding requirements to each product. In the process of learning, you can make a comparison and reference between the preceding and the following to see what logic and technical details are used to add each module. The study of these contents will be very conducive to your specific development in the design and implementation and undertaking product requirements in the future, and the quality of the code will be higher and higher, more and more extensible and maintainable.

2, Target

After completing the prototype of Spring framework, now we can manually operate the definition, registration and property filling of Bean objects through unit testing, and finally obtain the object and call methods. But there will be a problem here, that is, if you actually use this 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 registration of beans in the existing Spring framework.

3, Design

According to the requirements background of this chapter, we need to add a resource parser to the prototype of the existing Spring framework, 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 into 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 be combined with the DefaultListableBeanFactory core class, because all your parsed registration actions will put the Bean 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.

4, Realize

1. Engineering structure

small-spring-step-05
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.springframework  
    │           ├── beans
    │           │   ├── factory
    │           │   │   ├── factory
    │           │   │   │   ├── AutowireCapableBeanFactory.java
    │           │   │   │   ├── BeanDefinition.java
    │           │   │   │   ├── BeanReference.java
    │           │   │   │   ├── ConfigurableBeanFactory.java
    │           │   │   │   └── SingletonBeanRegistry.java
    │           │   │   ├── support
    │           │   │   │   ├── AbstractAutowireCapableBeanFactory.java
    │           │   │   │   ├── AbstractBeanDefinitionReader.java
    │           │   │   │   ├── AbstractBeanFactory.java
    │           │   │   │   ├── BeanDefinitionReader.java
    │           │   │   │   ├── BeanDefinitionRegistry.java
    │           │   │   │   ├── CglibSubclassingInstantiationStrategy.java
    │           │   │   │   ├── DefaultListableBeanFactory.java
    │           │   │   │   ├── DefaultSingletonBeanRegistry.java
    │           │   │   │   ├── InstantiationStrategy.java
    │           │   │   │   └── SimpleInstantiationStrategy.java  
    │           │   │   ├── support
    │           │   │   │   └── XmlBeanDefinitionReader.java
    │           │   │   ├── BeanFactory.java
    │           │   │   ├── ConfigurableListableBeanFactory.java
    │           │   │   ├── HierarchicalBeanFactory.java
    │           │   │   └── ListableBeanFactory.java
    │           │   ├── BeansException.java
    │           │   ├── PropertyValue.java
    │           │   └── PropertyValues.java 
    │           ├── core.io
    │           │   ├── ClassPathResource.java 
    │           │   ├── DefaultResourceLoader.java 
    │           │   ├── FileSystemResource.java 
    │           │   ├── Resource.java 
    │           │   ├── ResourceLoader.java 
    │           │   └── UrlResource.java
    │           └── utils
    │               └── ClassUtils.java
    └── test
        └── java
            └── cn.bugstack.springframework.test
                ├── bean
                │   ├── UserDao.java
                │   └── UserService.java
                └── ApiTest.java

Project source: official account "bugstack wormhole stack", reply: Spring column, get the complete source code.

Spring Bean container resource loading and usage class relationship, as shown in Figure 6-3

  • 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 operation of registering Bean container 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 the 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. As shown in Figure 6-4

  • 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 > t getBean (string name, class < T > 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. Sub-interface implemented by bean factories that can be part of a hierarchy.
  • AutowireCapableBeanFactory is an interface for automatic processing of 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.

2. Definition and implementation of resource loading interface

cn.bugstack.springframework.core.io.Resource

public interface Resource {

    InputStream getInputStream() throws IOException;

}
  • Create core. Net under the Spring framework IO core package, which is mainly used to process the resource loading stream.
  • 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

ClassPath: cn.bugstack.springframework.core.io.ClassPathResource

public class ClassPathResource implements Resource {

    private final String path;

    private ClassLoader classLoader;

    public ClassPathResource(String path) {
        this(path, (ClassLoader) 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 {
        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)

FileSystem: cn.bugstack.springframework.core.io.FileSystemResource

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.

Url: cn.bugstack.springframework.core.io.UrlResource

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){
            if (con instanceof HttpURLConnection){
                ((HttpURLConnection) con).disconnect();
            }
            throw ex;
        }
    }

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

3. Packaging resource loader

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

Define interface: CN bugstack. springframework. core. io. ResourceLoader

public interface ResourceLoader {

    /**
     * Pseudo URL prefix for loading from the class path: "classpath:"
     */
    String CLASSPATH_URL_PREFIX = "classpath:";

    Resource getResource(String location);

}
  • Define the interface for obtaining resources, where you can pass the location address.

Implementation interface: CN bugstack. springframework. core. io. DefaultResourceLoader

public class DefaultResourceLoader implements ResourceLoader {

    @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
        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.

4. Bean defines the read interface

cn.bugstack.springframework.beans.factory.support.BeanDefinitionReader

public interface BeanDefinitionReader {

    BeanDefinitionRegistry getRegistry();

    ResourceLoader getResourceLoader();

    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 to load 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.

5. Bean definition and abstract class implementation

cn.bugstack.springframework.beans.factory.support.AbstractBeanDefinitionReader

public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader {

    private final BeanDefinitionRegistry registry;

    private 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 to allow the external caller 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

6. Parsing XML and processing Bean registration

cn.bugstack.springframework.beans.factory.xml.XmlBeanDefinitionReader

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 {
            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);
    }

    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, which is convenient 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;
                // Parse 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 and value object
                Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue;
                // Create attribute information
                PropertyValue propertyValue = new PropertyValue(attrName, value);
                beanDefinition.getPropertyValues().addPropertyValue(propertyValue);
            }
            if (getRegistry().containsBeanDefinition(beanName)) {
                throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed");
            }
            // Register BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }

}

The core content of 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.

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

5, Testing

1. Preparation in advance

cn.bugstack.springframework.test.bean.UserDao

public class UserDao {

    private static Map<String, String> hashMap = new HashMap<>();

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

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

}

cn.bugstack.springframework.test.bean.UserService

public class UserService {

    private String uId;

    private UserDao userDao;

    public void queryUserInfo() {
        return userDao.queryUserName(uId);
    }

    // ...get/set
}
  • Dao and Service are often used in our daily development. Inject UserDao into UserService to reflect the dependency of Bean attribute.

2. Configuration file

important.properties

# Config File
system.key=OLpj9823dZ

spring.xml

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

    <bean id="userDao" class="cn.bugstack.springframework.test.bean.UserDao"/>

    <bean id="userService" class="cn.bugstack.springframework.test.bean.UserService">
        <property name="uId" value="10001"/>
        <property name="userDao" ref="userDao"/>
    </bean>

</beans>
  • Here are two configuration files, one for testing the resource loader and the other for spring XML is used to test the overall Bean registration function.

3. Unit test (resource loading)

case

private DefaultResourceLoader resourceLoader;      

@Before
public void init() {
    resourceLoader = new DefaultResourceLoader();
}   

@Test
public void test_classpath() throws IOException {
    Resource resource = resourceLoader.getResource("classpath:important.properties");
    InputStream inputStream = resource.getInputStream();
    String content = IoUtil.readUtf8(inputStream);
    System.out.println(content);
}   

@Test
public void test_file() throws IOException {
    Resource resource = resourceLoader.getResource("src/test/resources/important.properties");
    InputStream inputStream = resource.getInputStream();
    String content = IoUtil.readUtf8(inputStream);
    System.out.println(content);
}    

@Test
public void test_url() throws IOException {
    Resource resource = resourceLoader.getResource("https://github.com/fuzhengwei/small-spring/important.properties"
    InputStream inputStream = resource.getInputStream();
    String content = IoUtil.readUtf8(inputStream);
    System.out.println(content);
}

test result

# Config File
system.key=OLpj9823dZ

Process finished with exit code 0
  • These three methods: test_classpath,test_file,test_url, which is used to test and load ClassPath, FileSystem and URL files respectively. The URL file is in Github and may be slow to load

4. Unit test (configuration file registration Bean)

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("classpath:spring.xml");

    // 3. Get Bean object and call method
    UserService userService = beanFactory.getBean("userService", UserService.class);
    String result = userService.queryUserInfo();
    System.out.println("Test results:" + result);
}

test result

Test result: little brother Fu

Process finished with exit code 0
  • In the above test case, we can see that we handed over the contents of manually registering beans and configuring attribute information to the new XmlBeanDefinitionReader(beanFactory) class to read spring XML, and passed the test verification.

6, Summary

  • At this time, the engineering structure has become more and more like the Spring framework. Take the configuration file as the entry to parse and register the Bean information, and finally obtain the Bean through the Bean factory and do the corresponding call operation.
  • As for the implementation of each step in the case, little brother Fu will make corresponding simplification by referring to the interface definition, abstract class implementation, name specification, code structure, etc. of Spring source code as far as possible. In this way, in the process of learning, you can also learn the Spring source code through the class name or interface and the whole structure, which is much easier to learn.
  • Reading is definitely not equal to meeting. You can really master the knowledge only when you start from a small engineering frame structure to becoming larger, more and stronger now and in the future. In addition, the function realization of each chapter will involve a lot of code design ideas, which should be carefully understood. Of course, practice is the best way to understand!

7, Series recommendation

  • Give you a server. Can you deploy your code online?
  • I wrote 200000 lines of code before graduation, which made me a bully in the eyes of my classmates!
  • How close is mathematics to a programmer?
  • A code review, almost can't pass the probation period!
  • Start with mathematical knowledge points and explain the core technology of Java in depth. 400 pages of Java classics