头图

opening

This article is mainly based on the SpringFramework5.2.0.RELEASE version. The download steps of the source code have been described in other articles, so I will not repeat them here.

Basic usage of containers

Let's first create a simple example to see the basic usage of containers.

Create a simple Java Bean.

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2022/3/15
 * @Description 简单的bean实例
 */
public class MyTestBean {

   private String testStr = "testStr";

   public String getTestStr() {
      return testStr;
   }

   public void setTestStr(String testStr) {
      this.testStr = testStr;
   }
}

Create a simple Spring 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"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
   <bean id="myTestBean" class="cn.jack.MyTestBean"/>
</beans>

ok, write a test class to test.

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2022/3/15
 * @Description 测试类
 */
@SuppressWarnings("deprecation")
public class BeanFactoryTest {

   @Test
   public void testSimpleLoad(){
      final BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
      final MyTestBean myTestBean = (MyTestBean) beanFactory.getBean("myTestBean");
      assertEquals("testStr",myTestBean.getTestStr());
   }

}

After running, you can see that the execution is successful, the example is as simple as that.

运行结果

Usually, we don't use BeanFactory directly, but use ApplicationContext, just to better analyze Spring principles later.

Functional Analysis

This part of the code completes the following functions:

  • Read the configuration file beanFactoryTest.xml
  • Find the configuration of the corresponding class according to the configuration in the configuration file, and instantiate it
  • call the instantiated instance

From these descriptions, we can understand that at least three classes are required to implement.

  • ConfigReader : used to read and verify the configuration file, then put into memory
  • ReflectionUtil : Reflect instantiation according to the content of the configuration file, which is <bean id="myTestBean" class="cn.jack.MyTestBean"/> in our configuration file
  • App : used to complete the concatenation of the entire logic

最简单的Spring架构

According to the original way of thinking, the whole process is like this, but what is the structure of such an excellent Spring framework?

Spring's Hierarchical Architecture

We first try to sort out the framework structure of Spring, and understand the composition of Spring from a global perspective.

Beans package hierarchy

To implement the example just now, we mainly use org.springframework.beans.jar.

Before analyzing the source code, we first understand the two core classes.

1.DefaultListableBeanFactory

The XmlBeanFactory we just used inherits from DefaultListableBeanFactory , and the DefaultListableBeanFactory is the core part of the whole loading, is the default implementation of Spring's bean registration and loading , and the custom XML reader XmlBeanDefinitionReader is used in the XmlBeanFactory to achieve personalized BeanDefinitionReader reads. DefaultListableBeanFactory inherits AbstractAutoWireCapableBeanFactory and implements ConfigurableListableBeanFactory and BeanDefinitionRegistry interfaces.

容器加载相关类图

From this class diagram, we can clearly understand the context of DefaultListableBeanFactory, let's briefly understand the role of each class.

  • AliasRegistry : Define simple additions, deletions and modifications to aliases
  • SimpleAliasRegistry : Mainly use map as alias cache and implement the interface AliasRegistry
  • SingletonBeanRegistry : Define the registration and acquisition of singletons
  • BeanFactory : Defines various properties for getting beans and beans
  • DefaultSingletonBeanRegistry : Implementation of each function of the interface SingletonBeanRegistry
  • HierarchicalBeanFactory : Inherit BeanFactory, add support for parentFactory based on the functions defined by BeanFactory
  • BeanDefinitionRegistry : Define various additions, deletions and modifications to BeanDefinition
  • FactoryBeanRegistrySupport : Added special processing function for FactoryBean based on DefaultSingletonBeanRegistry
  • ConfigurableBeanFactory : Provides various ways to configure the Factory
  • ListableBeanFactory : Get the bean's configuration manifest based on various conditions
  • AbstractBeanFactory : Combines the functions of FactoryBeanRegistrySupport and ConfigurableBeanFactory
  • AutowireCapableBeanFactory : Provides post-processors that create beans, auto-inject, initialize, and apply beans
  • AbstractAutowireCapableBeanFactory : Integrate the functions of AbstractBeanFactory and implement the interface AutowireCapableBeanFactory
  • ConfigurableListableBeanFactory : BeanFactory configuration list, specifying ignore types and interfaces, etc.
  • DefaultListableBeanFactory : Combining all the above functions, mainly the processing after bean registration
XmlBeanFactory extends DefaultListableBeanFactory and is mainly used to read BeanDefinition from XML. For registration and acquisition of beans, the methods inherited from the parent class DefaultListableBeanFactory are used. Different from the parent class, XmlBeanFactory adds the reader attribute of XmlBeanDefinitionReader type. Read and register resource files.

2.XmlBeanDefinitionReader

The reading of XML configuration files is an important function in Spring. We can sort out the general context of resource file reading, parsing and registration from XmlBeanDefinitionReader.

配置文件读取相关类图

Let's first look at the functions of each class:

  • ResourceLoader : Define a resource loader, mainly used to return the corresponding Resource according to the given resource file address
  • BeanDefinitionReader : Mainly defines the functions of reading and converting resource files into BeanDefinition
  • EnvironmentCapable : Define get Environment method
  • DocumentLoader : Define the function to load from resource file to convert to Document
  • AbstractBeanDefinitionReader : Implement the methods defined by the EnvironmentCapable and BeanDefinitionReader classes
  • BeanDefinitionDocumentReader : Define the function of reading Document and registering BeanDefinition
  • BeanDefinitionParserDelegate : Define various methods for parsing Element

After the above analysis, we can sort out the general process of XML configuration file reading.

  1. Use ResourLoader to convert the resource file path to the corresponding Resource file by inheriting the method in AbstractBeanDefinitionReader
  2. Convert Resource files to Document files through DocumentLoader
  3. The Document is parsed by the DefaultBeanDefinitionDocumentReader class that implements the interface BeanDefinitionDocumentReader, and the Element is parsed using the BeanDefinitionParserDelegate

The base XmlBeanFactory for the container

After having a general understanding of the Spring container, let's analyze the implementation of the following code.

 final BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));

XmlBeanFactory初始化时序图

From the sequence diagram, we can see that the constructor of ClassPathResource is called first to construct the instance object of the Resource resource file, and then the resource processing can be operated with the services provided by the Resource, and then the XmlBeanFactory can be initialized. Next Let's take a look at how Resource resources are encapsulated.

Configuration file package

First, the read file is encapsulated by ClassPathResource, such as new ClassPathResource("beanFactoryTest.xml") , so what function does ClassPathResource accomplish?

In Java, resources from different sources are abstracted into URLs, and then different handlers (URLStreamHandler) are registered to handle the reading logic of different resources, but URLs do not define handlers relative to resources such as Classpath or ServletContext by default, although you can register your own URLStreamHandler to parse a specific URL prefix protocol, but this requires understanding of the implementation mechanism of the URL, and the URL does not provide basic methods (such as whether the resource is readable, whether it exists, etc.). Therefore, Spring implements its own abstract structure for the resources used internally, and the Resource interface encapsulates the underlying resources.

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}
public interface Resource extends InputStreamSource {

   boolean exists();


   default boolean isReadable() {
      return exists();
   }


   default boolean isOpen() {
      return false;
   }

   default boolean isFile() {
      return false;
   }


   URL getURL() throws IOException;

   URI getURI() throws IOException;

   File getFile() throws IOException;

   default ReadableByteChannel readableChannel() throws IOException {
      return Channels.newChannel(getInputStream());
   }

   long contentLength() throws IOException;

   long lastModified() throws IOException;

   Resource createRelative(String relativePath) throws IOException;

   @Nullable
   String getFilename();

   String getDescription();

}

There is only one method in InputStreamSource, getInputStream(), which returns a new InputStream object.

The Resource interface abstracts the underlying resources used internally by Spring, such as File, URL, Classpath, etc. Four methods are defined for judging the resource status: exists(), isReadable(), isOpen(), isFile(). In addition, the Resource interface also provides the conversion of different resources to URL, URI, and File types.

The createRelative() method can create a relative resource method based on the current resource.

The getDescription() method is used to print information during error handling.

After having a general understanding of the instance method of encapsulating the configuration file as a Resource type in Spring, we continue to look at the initialization process of XmlBeanFactory. Here we use the Resource instance as a constructor parameter method.

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

In the above method, this.reader.loadBeanDefinitions(resource); is the real implementation of resource loading, and is also one of the key points of the analysis, .

Our timing diagram 3.1 above is done here. Before calling this method, we also need to call the parent class constructor for initialization.

We trace directly to the constructor of the parent class AbstractAutowireCapableBeanFactory:

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

The main function of ignoreDependencyInterface is that ignores the autowiring function for a given interface. Why do this?

For example: when A has attribute B, if B has not been initialized when Spring obtains A's bean, then Spring will automatically initialize B, but in some cases, B will not be initialized, one of which is that B implements BeanNameAware interface.

As introduced in Spring: The given dependency interface is ignored during autowiring, and the typical application is to resolve the Application context registration dependencies in other ways, similar to BeanFactory injection through BeanFactoryAware or ApplicationContext injection through ApplicationContextAware.

load beans

Back to just now, we called this.reader.loadBeanDefinitions(resource); in the XmlBeanFactory constructor. This code is the entry point of the entire resource loading. Let's take a look at the sequence diagram of this method.

loadBeanDefinitions加载时序图

Let's try to sort out the process:

  1. Use the EncodedResource class to encapsulate the parameter Resource resource file
  2. Get the corresponding InputStream from the Resource, and then construct the InputSource
  3. Then proceed to call the doLoadBeanDefinitions method with the constructed InputSource instance and Resource instance

Next, let's take a look at the specific implementation process of the loadBeanDefinitions method.

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

First, let's take a look at what the function of EncodedResource is. It can be seen from the name that it encodes the resource file. The main logic is in the getReader() method. If the encoding attribute is set, Spring will use the corresponding encoding as The encoding of the input stream.

public Reader getReader() throws IOException {
   if (this.charset != null) {
      return new InputStreamReader(this.resource.getInputStream(), this.charset);
   }
   else if (this.encoding != null) {
      return new InputStreamReader(this.resource.getInputStream(), this.encoding);
   }
   else {
      return new InputStreamReader(this.resource.getInputStream());
   }
}

This method constructs an InputStreamReader. When the EncodedResource is constructed, the loadBeanDefinitions overloaded method is called.

Inside this method is the real data preparation phase.

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);
   }
        //resourcesCurrentlyBeingLoaded是一个ThreadLocal,里面存放着Resource类的set集合
   Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
   if (currentResources == null) {
      currentResources = new HashSet<>(4);
      this.resourcesCurrentlyBeingLoaded.set(currentResources);
   }
   //如果set中已有这个元素则返回false并抛出异常
   if (!currentResources.add(encodedResource)) {
      throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
   }
   try {
      //从encodedResource中获取已经封装的Resource对象并再次从Resource中获取InputStream
      InputStream inputStream = encodedResource.getResource().getInputStream();
      try {
         //准备解析xml文件,全路径为org.xml.sax.InputSource
         InputSource inputSource = new InputSource(inputStream);
         //设置编码集
         if (encodedResource.getEncoding() != null) {
            inputSource.setEncoding(encodedResource.getEncoding());
         }
         return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
      }
      finally {
         inputStream.close();
      }
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException(
            "IOException parsing XML document from " + encodedResource.getResource(), ex);
   }
   finally {
       //资源加载完毕,移除该Resource
      currentResources.remove(encodedResource);
      //如果没有其他资源了,则remove
      if (currentResources.isEmpty()) {
         this.resourcesCurrentlyBeingLoaded.remove();
      }
   }
}

This method first encapsulates the incoming Resource parameter, in order to take into account that the Resource may have coding requirements, secondly, create the InputSource object by reading the XML file through SAX, and finally pass the parameters to the core processing part of doLoadBeanDefinitions(inputSource,encodedResource.getResource())

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) {
      throw ex;
   }
   // ...省略catch
}

This method mainly does three things:

  • Get the validation schema for an XML file
  • Load the XML file to get the corresponding Document
  • Register Bean information according to the returned Document

Let's look at it step by step, starting with the XML validation schema.

Get the validation schema for XML

There are two commonly used methods to verify the correctness of XML: DTD and XSD.

DTD (Document Type Definition) is a document type definition, which is an XML constraint schema language and a verification mechanism XML files.

DTD stands for document type definition, which is an XML constraint schema language and a verification mechanism for XML files, which is a part of XML files.
DTD is an effective method to ensure the correct format of XML documents. You can compare XML documents and DTD files to see whether the document conforms to the specification and whether the elements and tags are used correctly. A DTD document contains: rules for defining elements, rules for defining relationships between elements, attributes that elements can use, and rules for entities or symbols that can be used.

DTD vs. XSD: DTDs are written using non-XML syntax.
DTDs are not extensible, do not support namespaces, and only provide very limited data types.

XSD (XML Schemas Definition), the XML Schema language, was launched in 2001 by W3C for the defects of DTD. XML Schema itself is an XML document, using XML syntax, so XSD documents can be easily parsed.

Compared with DTD, XSD has the following advantages:

  • XML Schema is based on XML and has no special syntax
  • XML Schema can be parsed and processed like any other XML file
  • XML Schema provides richer data types than DTD
  • XML Schema provides an extensible data model
  • XML Schema supports comprehensive namespaces
  • XML Schema supports attribute groups
In the Spring source code, the authentication mode of the Bean is configured based on the XML file, which is usually the XSD mode.

XSD模式

Verify Mode Read

protected int getValidationModeForResource(Resource resource) {
   int validationModeToUse = getValidationMode();
    //如果手动指定了验证模式则使用指定的验证模式
   if (validationModeToUse != VALIDATION_AUTO) {
      return validationModeToUse;
   }
   // 如果未指定则使用自动检测
   int detectedMode = detectValidationMode(resource);
   if (detectedMode != VALIDATION_AUTO) {
      return detectedMode;
   }
   return VALIDATION_XSD;
}

Spring detects the verification mode by judging whether it contains DOCTYPE, which is DTD, otherwise it is XSD.

getDocument

After the verification mode is prepared, the Document can be loaded. The documentLoader here is an interface, and the real implementation is the following DefaultDocumentLoader.

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
   return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
         getValidationModeForResource(resource), isNamespaceAware());
}
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
      ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
      //创建文档构建器工厂对象,并初始化一些属性
    //如果验证模式为XSD,那么强制支持XML名称空间,并加上schema属性
   DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
   if (logger.isTraceEnabled()) {
      logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
   }
   DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
   //按照XML文档解析给定inputSource的内容,然后返回一个新的DOM对象
   return builder.parse(inputSource);
}

This code mainly creates an instance of DocumentBuilderFactory, then creates a DocumentBuilder through DocumentBuilderFactory, and finally parses the inputSource and returns the Document object.

Parse and register BeanDefinitions

We go back to the doLoadBeanDefinitions method. After getting the Document object, we can register the bean object and call the registerBeanDefinitions method.

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
   //实例化BeanDefinitionDocumentReader
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
   //记录统计前BeanDefinition的加载个数
   int countBefore = getRegistry().getBeanDefinitionCount();
   //加载及注册bean(关键)
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
   //记录本次加载个数
   return getRegistry().getBeanDefinitionCount() - countBefore;
}

Call the registerBeanDefinitions method and select the implementation class as DeDefaultBeanDefinitionDocumentReader .

@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
   this.readerContext = readerContext;
   //拿到了xml文档对象的根元素 并调用该方法
   doRegisterBeanDefinitions(doc.getDocumentElement());
}

doRegisterBeanDefinitions starts the actual parsing.

protected void doRegisterBeanDefinitions(Element root) {
    // 任何被嵌套的<beans>元素都会导致此方法的递归。为了正确的传播和保存<beans>的默认属性、
    // 保持当前(父)代理的跟踪,它可能为null
    // 为了能够回退,新的(子)代理具有父的引用,最终会重置this.delegate回到它的初始(父)引用。
    // 这个行为模拟了一堆代理,但实际上并不需要一个代理
   BeanDefinitionParserDelegate parent = this.delegate;
   //代码(1)
   this.delegate = createDelegate(getReaderContext(), root, parent);
      //默认名称空间是"http://www.springframework.org/schema/beans"
   if (this.delegate.isDefaultNamespace(root)) {
      String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
     //在xml配置文件中对profile的设置 区分是生产环境还是线上环境
      if (StringUtils.hasText(profileSpec)) {
         String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
               profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
         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);
   //生成BeanDefinition,并注册在工厂中,代码(2)
   parseBeanDefinitions(root, this.delegate);
   //解析后处理,留给子类实现
   postProcessXml(root);

   this.delegate = parent;
}

Let's start by looking at the method of code (1), which creates a BeanDefinitionParserDelegate object that delegates parsing of attribute values in XML.

protected BeanDefinitionParserDelegate createDelegate(
      XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) {

   BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
   delegate.initDefaults(root, parentDelegate);
   return delegate;
}

Let's look at the constants of the BeanDefinitionParserDelegate class.

public class BeanDefinitionParserDelegate {

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

   public static final String MULTI_VALUE_ATTRIBUTE_DELIMITERS = ",; ";

   /**
    * Value of a T/F attribute that represents true.
    * Anything else represents false. Case seNsItive.
    */
   public static final String TRUE_VALUE = "true";

   public static final String FALSE_VALUE = "false";

   public static final String DEFAULT_VALUE = "default";

   public static final String DESCRIPTION_ELEMENT = "description";

   public static final String AUTOWIRE_NO_VALUE = "no";

   public static final String AUTOWIRE_BY_NAME_VALUE = "byName";

   public static final String AUTOWIRE_BY_TYPE_VALUE = "byType";

   public static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor";

   public static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect";

   public static final String NAME_ATTRIBUTE = "name";

   public static final String BEAN_ELEMENT = "bean";

   public static final String META_ELEMENT = "meta";

   public static final String ID_ATTRIBUTE = "id";

   public static final String PARENT_ATTRIBUTE = "parent";

   //...
}

We found that the properties of the Spring configuration file are all here. Now we know that the BeanDefinitionParserDelegate object is indeed to parse the XML configuration file, and continue to return to the createDelegate method. After the BeanDefinitionParserDelegate object is created, the initDefaults method is also executed to initialize some default values.

Parse and register BeanDefinition

Now we go back to code (2) and enter the parseBeanDefinitions method.

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
   //对beans的处理,默认名称空间是"http://www.springframework.org/schema/beans"
   if (delegate.isDefaultNamespace(root)) {
     //获取根元素下的子Node
      NodeList nl = root.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++) {
         Node node = nl.item(i);
         if (node instanceof Element) {
           //拿到了<beans>下的子标签
            Element ele = (Element) node;
            if (delegate.isDefaultNamespace(ele)) {
               //如果该标签属于beans的名称空间,则进入这个方法
               //xmlns="http://www.springframework.org/schema/beans"
               parseDefaultElement(ele, delegate);
            }
            else {
               //如果该标签属于其他的名称空间比如:context,aop等
               //xmlns:aop="http://www.springframework.org/schema/aop"
               //xmlns:context="http://www.springframework.org/schema/context"
               delegate.parseCustomElement(ele);
            }
         }
      }
   }
   else {
      delegate.parseCustomElement(root);
   }
}

XML in Spring is divided into two categories, one is the default as follows:

<bean id="test" class="cn.jack.Test"/>

The other is custom:

<tx:annotation-driven/>

If it is a custom implementation, the user needs to implement a custom configuration. If the root node or node is named by default, use parseDefaultElement to parse, otherwise use the delegate.parseCustomElement method to parse the custom namespace.

命名空间对比

To judge whether it is a default naming or a custom namespace, use node.getNamespaceURI() in the isDefaultNamespace method to obtain the namespace, and then compare it with the fixed namespace http://www.springframework.org/schema/beans . If it is inconsistent, it is a custom namespace.

For default tag parsing and custom tag parsing, it will be in the next article.


神秘杰克
765 声望382 粉丝

Be a good developer.