[Spring source code analysis] XmlBeanFactory file resource loading (including timing diagram)

1 Introduction

When using Spring there is always confusion about how Spring manages Spring bean s.
There is also a lot of mixed information on the Internet. Here I record some of the content that I have separated from the book after reading "Spring Source Code In-depth Analysis" recently.

Speaking of Spring, I have to say that in ancient times, when Spring was in its prime, XML was still used, and the annotation method was only available for subsequent optimization.
Here is also from the Spring Xml way.

2. Examples

Create a simple maven project first.

  • add maven dependencies

The Spring version used here is 5.1.7. In fact, only a few core packages of Spring are needed in the initial stage. For the convenience of the follow-up, they are added here at one time.

Note: The scope of spring-test and Junit here is the default test, so the test code is in test.

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <spring-version>5.1.7.RELEASE</spring-version>
</properties>
<dependencies>
    <!--log-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.11.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.11.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.11.2</version>
    </dependency>

    <!--ioc-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <!--test-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring-version}</version>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.7.2</version>
    </dependency>
    <!--aop-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring-version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>
  • Java Bean
public class User {
    private int sex;
    private String username;
    private int age;
    // omit set/get
}
  • Configuration XML

Here is a brief explanation:

  1. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance represents an XMLschema instance
  2. http://www.springframework.org/schema/beans represents the storage location of the bean namespace and can be accessed directly.
  3. http://www.springframework.org/schema/beans/spring-beans-3.2.xsd stands for XSD xml parsing 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">

    <bean id="user" class="com.fans.entity.User"/>
</beans>
  • test code

The test code is written under the project src/test/java:

public class SpringTest {

    @Test
    void test(){
        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("user.xml"));
        User user = (User) beanFactory.getBean("user");
        System.out.println(user);
    }
}

Test result: com.fans.entity.User@2f7c2f4f

  • code structure

3. Analysis

Note: Only the case of resource loading is parsed here, and will continue to be added later

Starting from the sentence BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("user.xml"));:

3.1,Resource

new ClassPathResouce("user.xml") is the loading of the resource.

In Java, resources from different sources are abstracted into URLs, and the reading logic of resources from different sources is handled by registering different handlers (URLStreamHandler). Generally, the handler type is identified by using different prefixes (Protocol), such as "file:","jar:","http:", etc. However, the URL does not define the handler of resources such as relative classpath or ServletContext by default, Although you can register your own URLStreamHandler to parse URL prefixes (protocols), such as classpath:, it requires understanding the implementation mechanism of the protocol, and the URL does not provide basic URL methods, such as checking whether the current Resource exists and whether the current Resource is readable. Therefore, Spring implements its own abstract structure for the resources used internally: the Resource interface encapsulates the underlying resources

The above quotation is from the book. We can know that new ClassPathResource(“user.xml”) in the demo test code is the resource loading of the configuration information in user.xml, which is convenient for us to parse the content later.

View the source code:

  • new ClassPathResouce("user.xml")
public ClassPathResource(String path) {
   this(path, (ClassLoader) null);
}

public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
   Assert.notNull(path, "Path must not be null");
   String pathToUse = StringUtils.cleanPath(path);
   if (pathToUse.startsWith("/")) {
      pathToUse = pathToUse.substring(1);
   }
   this.path = pathToUse;
   this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}

This is mainly to deal with the path problem in "user.xml", StringUtils.cleanPath, pathToUse.substring, etc.

3.2, XmlBeanFactory

  • new XmlBeanFactory(new ClassPathResource("user.xml"))
public XmlBeanFactory(Resource resource) throws BeansException {
   this(resource, null);
}

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
   super(parentBeanFactory);
   this.reader.loadBeanDefinitions(resource);
}

XmlBeanFactory(Resource resource) points to XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) for parsing, where the builder is used to create objects.

  • super(parentBeanFactory)

Here XmlBeanFactory points to the parent class DefaultListableBeanFactory, and DefaultListableBeanFactory points to the parent class AbstractAutowireCapableBeanFactory.

public AbstractAutowireCapableBeanFactory() {
   super();
   ignoreDependencyInterface(BeanNameAware.class);
   ignoreDependencyInterface(BeanFactoryAware.class);
   ignoreDependencyInterface(BeanClassLoaderAware.class);
}

public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
   this();
   setParentBeanFactory(parentBeanFactory);
}

Here ignoreDependencyInterface(Class<?> clazz) The above is to ignore the incoming class during autowiring.

super() points to the parent class AbstractBeanFactory by AbstractAutowireCapableBeanFactory.
setParentBeanFactory(parentBeanFactory) is also implemented in AbstractBeanFactory, mainly injecting parent BeanFactory.

3.3,loadBeanDefinitions

this.reader.loadBeanDefinitions(resource) Here is the processing of resources, in which the parsing and loading of resources are completed by this sentence. This is not the loading done by XmlBeanFactory itself, but using its own property XmlBeanDefinitionReader to complete, click in to see:

@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
   return loadBeanDefinitions(new EncodedResource(resource));
}

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
   Assert.notNull(encodedResource, "EncodedResource must not be null");
   if (logger.isTraceEnabled()) {
      logger.trace("Loading XML bean definitions from " + encodedResource);
   }

   Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();

   if (!currentResources.add(encodedResource)) {
      throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
   }

   try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
      InputSource inputSource = new InputSource(inputStream);
      if (encodedResource.getEncoding() != null) {
         inputSource.setEncoding(encodedResource.getEncoding());
      }
      return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException(
            "IOException parsing XML document from " + encodedResource.getResource(), ex);
   }
   finally {
      currentResources.remove(encodedResource);
      if (currentResources.isEmpty()) {
         this.resourcesCurrentlyBeingLoaded.remove();
      }
   }
}

EncodedResource encodes the resource to satisfy the encoding of the input stream below.
encodedResource.getResource().getInputStream() to get the resource's data stream.
new InputSource(inputStream) then encapsulates the resource stream object obtained.

doLoadBeanDefinitions(inputSource, encodedResource.getResource()) This is where the actual content is loaded.

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

   try {
      Document doc = doLoadDocument(inputSource, resource);
      int count = registerBeanDefinitions(doc, resource);
      if (logger.isDebugEnabled()) {
         logger.debug("Loaded " + count + " bean definitions from " + resource);
      }
      return count;
   }
   catch (BeanDefinitionStoreException ex) {
        // The catch content is omitted, it is the processing of parsing exceptions, and only the main process is concerned here.
   }
}

The parsing and loading here is divided into two steps:

(1) Parse and convert into Document object

Document doc = doLoadDocument(inputSource, resource);

(2) Execute registration and count the number of registered bean s

int count = registerBeanDefinitions(doc, resource);

  • doLoadDocument
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
   return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
         getValidationModeForResource(resource), isNamespaceAware());
}

Before converting the object, we actually do the process of obtaining the parser and verifying:

getEntityResolver() Gets the resolver, here the project provides a way to find and parse the XML DTD file.
getValidationModeForResource is used to determine whether the XML is parsed by custom, DTD or XSD.

  • loadDocument

loadDocument actually completes the conversion of XML files to Document through the default implementation class of DocumentLoader, DafaultDocument.

@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
      ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

   DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
   if (logger.isTraceEnabled()) {
      logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
   }
   DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
   return builder.parse(inputSource);
}

The main function of createDocumentBuilderFactory here is to create a factory class:

protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
      throws ParserConfigurationException {

   DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
   factory.setNamespaceAware(namespaceAware);

   if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
      factory.setValidating(true);
      if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
         // Enforce namespace aware for XSD...
         factory.setNamespaceAware(true);
         try {
            factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
         }
         catch (IllegalArgumentException ex) {
            ParserConfigurationException pcex = new ParserConfigurationException(
                  "Unable to validate using XSD: Your JAXP provider [" + factory +
                  "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
                  "Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
            pcex.initCause(ex);
            throw pcex;
         }
      }
   }

   return factory;
}

Then create a Builder through the factory class, set the parser, and handle exceptions:

protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
      @Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
      throws ParserConfigurationException {

   DocumentBuilder docBuilder = factory.newDocumentBuilder();
   if (entityResolver != null) {
      docBuilder.setEntityResolver(entityResolver);
   }
   if (errorHandler != null) {
      docBuilder.setErrorHandler(errorHandler);
   }
   return docBuilder;
}

The above two parts are to create parsing objects. The real parsing is through parse method, builder Parse (inputsource), in the DocumentBuilder implementation class DocumentBuilderImpl, here is to parse XML files through DomParser. DomParser is not the focus of our discussion. Here, I am interested in can be deeply understood

public Document parse(InputSource is) throws SAXException, IOException {
    if (is == null) {
        throw new IllegalArgumentException(
            DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
            "jaxp-null-input-source", null));
    }
    if (fSchemaValidator != null) {
        if (fSchemaValidationManager != null) {
            fSchemaValidationManager.reset();
            fUnparsedEntityHandler.reset();
        }
        resetSchemaValidator();
    }
    domParser.parse(is);
    Document doc = domParser.getDocument();
    domParser.dropDocumentReferences();
    return doc;
}
  • registerBeanDefinitions

After the parsing of the Document is completed, it is the processing of the Document. Now what is actually done is to convert the XML configuration file into a Document. The following is the content of continuing to parse:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
   int countBefore = getRegistry().getBeanDefinitionCount();
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
   return getRegistry().getBeanDefinitionCount() - countBefore;
}
  1. First obtain the statistics of the number of beans registered in the registry
  2. Complete the registration through BeanDefinitionDocumentReader, this is the main content
  3. Count the number of registered bean s
  • registerBeanDefinitions

Check out the default implementation class DefaultBeanDefinitionDocumentReader of BeanDefinitionDocumentReader :

@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
   this.readerContext = readerContext;
   doRegisterBeanDefinitions(doc.getDocumentElement());
}

protected void doRegisterBeanDefinitions(Element root) {
   // Any nested <beans> elements will cause recursion in this method. In
   // order to propagate and preserve <beans> default-* attributes correctly,
   // keep track of the current (parent) delegate, which may be null. Create
   // the new (child) delegate with a reference to the parent for fallback purposes,
   // then ultimately reset this.delegate back to its original (parent) reference.
   // this behavior emulates a stack of delegates without actually necessitating one.
   BeanDefinitionParserDelegate parent = this.delegate;
   this.delegate = createDelegate(getReaderContext(), root, parent);

   if (this.delegate.isDefaultNamespace(root)) {
      String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
      if (StringUtils.hasText(profileSpec)) {
         String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
               profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
         // We cannot use Profiles.of(...) since profile expressions are not supported
         // in XML config. See SPR-12458 for details.
         if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
            if (logger.isDebugEnabled()) {
               logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                     "] not matching: " + getReaderContext().getResource());
            }
            return;
         }
      }
   }

   preProcessXml(root);
   parseBeanDefinitions(root, this.delegate);
   postProcessXml(root);

   this.delegate = parent;
}

Create the bean's holding object through createDelegate, and then through parseBeanDefinitions to finally complete the parsing and registration of the bean. The parsing and registration of all tags are included, and the next part will explain the details.

4. Timing diagram

When I checked it before, it took a lot of time to make a sequence diagram (if you can't see clearly, you can click to enlarge):

5. Summary

When writing the above, there are also some details that I did not pay attention to, but the main process is complete. If you want to be very detailed, it is recommended to read the original book.
In addition, the internal implementation of Spring is very complex, and we are also struggling to look at it, but there is always something to be gained by slowly combing it.

Come on, let's work together!

Tags: Java Spring Junit

Posted by daddymac on Thu, 21 Jul 2022 01:58:20 +0930