Interviewer: tell me about Spring loading configuration

Interviewer: tell me about Spring loading configuration

Scene review

Happy new year, everyone. I'm Xiao Zhang. Today is the first day of resumption of work. When I return to my bedroom, Xiao Ye is depressed again. There are the following scenes.

Interviewer: Hello, Xiaoye. I see that my resume says that I am proficient in Spring. Then I want to ask that in Spring, we will certainly write many configuration files for Spring to load. So how do you do it?

Xiaoye: Mmm, we will involve many configuration files in the project, such as database, Redis and other configuration files. When Spring used xml in the early days, we usually defined one The xml file contains various configuration information, which is provided to the container for loading.

Interviewer: Hmm, the configuration file is in XML format, so many tags used in it are provided to us by Spring. Can I include my custom tags in the XML configuration?

Xiaoye: it seems that you can't customize the label? (can XML configuration files also customize labels? It shouldn't be.)

Interviewer: today's interview is here first.

Case preparation

As follows, in normal development, we usually define one in the Resource directory The Bean information is defined in the xml file.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

        <bean class="com.peter.person">
        </bean>
</beans>

Then write the following code in the Main method to start the container.

public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("peter.xml");
        context.getBean("person");
    }

Through the above lines of code, we can get the person object. Then the code for parsing the configuration file must be completed in the context of instantiating the Spring Context. We can also locate where to call the configuration parsing through DEBUG, find the specific place to parse the configuration file, and we can know whether the framework provides us with the ability of extension.

Source code DEBUG

Through debugging all the way, we will find that the loaded XML configuration file is stored in this method, AbstractRefreshableApplicationContext#refreshBeanFactory.

@Override
    protected final void refreshBeanFactory() throws BeansException {
        ...
        try {
            ...
            // Load BeanDefinition
            loadBeanDefinitions(beanFactory);
            ...
        }
        catch (IOException ex) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
        }
    }

We will see that loadBeanDefinitions is called in this method. We don't know what operations are done in this class for the time being, but we can roughly guess from the name that it loads the configuration through the configuration file. This is also a point we need to learn at ordinary times. We should know the intention by seeing the name.

BeanDefinition

We can even boldly guess that BeanDefinition may be an interface, which contains the attributes that should be available after the configuration file is parsed. Then let's carefully verify whether there is this interface.

Sure enough, we found a large number of familiar attributes in the BeanDefinition interface: scope, beanClassName and other attributes we will define in the XML file. If necessary, we can easily extend our BeanDefinition through the interface.

Load BeanDefinition

@Override
    protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
        // Create Reader using adapter mode
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
        ...
        // Start loading beanDefinition
        loadBeanDefinitions(beanDefinitionReader);
    }

    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
        // Obtain the Resource location of the configuration file in the form of Resource
        Resource[] configResources = getConfigResources();
        if (configResources != null) {
            reader.loadBeanDefinitions(configResources);
        }
        // Get the location of the configuration file in the form of String
        String[] configLocations = getConfigLocations();
        if (configLocations != null) {
            reader.loadBeanDefinitions(configLocations);
        }
    }
    
    @Override
    public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
        Assert.notNull(locations, "Location array must not be null");
        int count = 0;
        for (String location : locations) {
            count += loadBeanDefinitions(location);
        }
        return count;
    }

We will see that the XmlBeanDefinitionReader object is directly constructed by passing the beanFactory object to the Reader constructor. Here is the familiar adapter pattern.

From the source code, we can see that the container supports the reading of multiple configuration files. The framework will cycle to obtain BeanDefinition from the configuration file. Continue to DEBUG, and we will go to the doLoadBeanDefinitions method. In the Spring source code, the functions starting with do are actually working functions, that is, specific implementations.

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

        try {
            // Get the document object of the xml file here
            Document doc = doLoadDocument(inputSource, resource);
            // Parse the document object and add the parsed BeanDefinition to the container
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }
        ...Omit exception capture...
    }

I believe that the little friends who understand XML parsing are familiar with Document, because this object represents that the XML file has been parsed into a ROOT node by us. We can traverse all nodes through this object to obtain XML information.

Next, when we enter the registerBeanDefinitions method, we will find that there are some unknown classes, which also reflects the flexibility of the Spring framework. The design of classes is a dedicated mode. Each class plays a related role and will not put irrelevant contents into an interface / class.

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        // Parse the beanDefinition of xml
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        // Get the existing BeanDefinition in the previous container
        int countBefore = getRegistry().getBeanDefinitionCount();
        // Complete the specific parsing process
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

The function of documentReader here is to parse XML files and add BeanDefinition to the container. The register here can be understood as adding.

Then the code will enter the doRegisterBeanDefinitions method and see the do method. You should be able to see the name and meaning.

protected void doRegisterBeanDefinitions(Element root) {
        ...
        // Parsing preprocessing
        preProcessXml(root);
        // Perform parsing
        parseBeanDefinitions(root, this.delegate);
        // Post parsing
        postProcessXml(root);
        ...
    }

protected void preProcessXml(Element root) {
    }
protected void postProcessXml(Element root) {
    }

You may find it strange to see the two methods of preProcessXml and postProcessXml. How can there be an empty implementation? In fact, the framework is provided to users for extension. As long as we subclass inherit and rewrite these two methods, we will extend the method.

When we turn our attention to the specific parsing node, we will see some namespace related content. If you have no foundation for xml, you can pass it xml namespace Learn what a namespace is.

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        // Determine whether the default namespace
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    // Start parsing node
                    Element ele = (Element) node;
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

public boolean isDefaultNamespace(@Nullable String namespaceUri) {
        return !StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri);
    }

public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";

Through the source code, we will find that Spring will take beans as the default namespace, but we have also seen < context: component scan base package = "XXX" / > this kind of configuration file in the configuration file. In the configuration file, we will find that the namespace of context is xmlns: context=“ http://www.springframework.org/schema/context ", is not the default namespace, so how does Spring implement it?

Through the source code, we can see that if the current node namespace is not the default node, it will enter the parseCustomElement method, so the resolution of the non default namespace must be here.

    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        // Get the corresponding namespace
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        // Find the corresponding NamespaceHandlerspring according to the namespace
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if (handler == null) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
            return null;
        }
        // Call the custom NamespaceHandler for parsing
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

Through the source code, we will find the NamespaceHandler through the non default namespace. The specific role will be shown below. Let's first look at how to find the actuator through the namespace.

@Override
    @Nullable
    public NamespaceHandler resolve(String namespaceUri) {
        // Get all the configured handler mappings
        Map<String, Object> handlerMappings = getHandlerMappings();
        // Find the corresponding information according to the namespace
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        if (handlerOrClassName == null) {
            return null;
        }
        else if (handlerOrClassName instanceof NamespaceHandler) {
            // If it has been parsed, read it directly from the cache
            return (NamespaceHandler) handlerOrClassName;
        }
        else {
            // If no parsing has been done, the classpath is returned
            String className = (String) handlerOrClassName;
            try {
                // Convert Classpaths to classes through reflection
                Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
                // Instantiation class
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
                // Call the initialization method of the custom namespaceHandler
                namespaceHandler.init();
                // The results are recorded in the cache
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
        }
    }

We will find that it stores a spring hadlers file in the META-INF directory, which stores the relationship between the namespace and the parser.

The following figure shows the mapping relationship stored in the spring beans project, so we can create a custom label parser according to the gourd drawing.

Through the above figure, we will find another one named spring The schema file stores the relationship between the schema URL and the schema file path. It is verified by the schema file when the XML file is parsed into a Document object.

Start expanding

Through the reading of the above source code, we can customize the label, which requires the following steps.

  1. Write schema file
  2. In meta-inf / spring Write the mapping relationship between schema url and schema file in schema file
  3. Create a custom label parser class
  4. In meta-inf / spring Write the mapping relationship between schema url and custom tag parser in handlers file

Schema file writing

We can refer to spring beans xsd writes its own xsd file.

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.xxx.com/schema/peter"
        xmlns:tns="http://www.xxx.com/schema/peter"
        elementFormDefault="qualified">
<element name="user">
    <complexType>
        <attribute name ="id" type = "string"/>
        <attribute name ="userName" type = "string"/>
    </complexType>
</element>
</schema>

Set schema mapping relationship

http\://www.xxx.com/schema/peter.xsd=peter.xsd

Create label parser class

public class User {
    private String username;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

public class UserHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }

    private static class UserBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser {

        @Override
        protected Class<?> getBeanClass(Element element) {
            return User.class;
        }

        @Override
        protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
            String username = element.getAttribute("userName");
            if (!StringUtils.hasText(username)) {
                builder.addPropertyValue("username", username);
            }

        }
    }
}

Create label parser mapping relationship

http\://www.xxx.com/schema/peter=com.peter.UserHandler

Write configuration file

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:peter="http://www.xxx.com/schema/peter"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.xxx.com/schema/peter http://www.xxx.com/schema/peter.xsd">
        <peter:user id="peter" userName="peter"/>
</beans>

Program start

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("peter.xml");
        User bean = context.getBean(User.class);
        System.out.println();
    }
}

Write at the end

By reading the source code of the configuration file through Spring, we can clearly understand the general meaning through the method name and class name, which is actually a very important point in normal development. If we know the meaning by name, the efficiency of others in reading your code will increase significantly. In normal development, we can also learn from this set of parsing ideas to realize flexible expansion. Finally, many people who have read the Spring source code will have a feeling that the classes are too complex. In fact, the Spring framework defines many interfaces for flexible expansion and develops around the design mode.

Keywords: Java

Added by reaper7861 on Wed, 09 Feb 2022 21:05:15 +0200