In addition to spring-beans-x. sd, you can use custom XML tags to extend Spring’s XML Schema. For example, Mybatis, Redis, MQ components, etc. It also applies to Spring’s built-in features, such as transaction < TX: XXX />, AOP, and so on.

This article will look at the Spring source code to find out why XML custom tags work. In fact, in the source code, you can find that Spring divides XML tags into two categories: one is Spring custom tags; The other category is extended custom tags, such as Spring transaction tags tx: XXX, etc., as well as the user-defined tags introduced in the previous article are extended custom tags.

The source code interpretation

To interpret the source code, let’s talk about the startup classes in Spring’s XML Schema extension mechanism:

public class App {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-service.xml");
        DistributedIdComponent bean = context.getBean(DistributedIdComponent.class);

        String id = bean.generateId();

        System.out.println("id:"+ id); }}Copy the code

ClassPathXmlApplicationContext is the BeanFactory implementation class, as a container context, the initialization process includes parsing XML, load, beans, etc., This is in the ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext (” spring – service. The XML “); .

Loading sequence

Entrance ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext (” spring – service. The XML “); , the ClassPathXmlApplicationContext two construction method is as follows, including the refresh () method as the main entrance:

public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
    this(new String[] {configLocation}, true.null);
  }

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
      throws BeansException {

    super(parent);
    setConfigLocations(configLocations);
    if (refresh) {
      // Main entrancerefresh(); }}Copy the code

Since the whole process involves a deep source code level and many classes, the following sequence diagram is roughly made for method back and forth viewing (focusing on the process of parsing custom tags, the subsequent process has been ignored) :

Through this figure can be convenient to view the source code, switching back and forth to locate. Actually to DefaultBeanDefinitionDocumentReader class involves specific back from the XML tag parsed into BeanDefinition, here will be back from parseCustomElement process in detail.

The class diagram relationships involved in this process are as follows:

Parse custom tags

DefaultBeanDefinitionDocumentReader

As a class diagram shown in BeanDefinitionDocumentReader DefaultBeanDefinitionDocumentReader is interface implementation class, used in from the XML parsing is configured to bean definition.

As shown in the following source code, pay attention to the two places in the doRegisterBeanDefinitions methods, one is to create a delegate class BeanDefinitionParserDelegate, is actually to parse the XML tag is configured to bean definition of class; One is parseBeanDefinitions (root, enclosing the delegate), for a specific analytical method of BeanDefinition BeanDefinitionParserDelegate last call for processing. (Delegate mode is used here.)

  // DefaultBeanDefinitionDocumentReader.java
  
  /**
   * Register each bean definition within the given root {@code <beans/>} element.
   */
  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);
        if(! getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {if (logger.isInfoEnabled()) {
            logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                "] not matching: " + getReaderContext().getResource());
          }
          return;
        }
      }
    }

    preProcessXml(root);
    // Parse BeanDefinition in XML
    parseBeanDefinitions(root, this.delegate);
    postProcessXml(root);

    this.delegate = parent;
  }

  /**
   * Parse the elements at the root level in the document:
   * "import", "alias", "bean".
   * @param root the DOM root element of the document
   */
  protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    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) {
          Element ele = (Element) node;
          if (delegate.isDefaultNamespace(ele)) {
            // Parse the default tags
            parseDefaultElement(ele, delegate);
          }
          else {
            // Parse custom (extended) tagsdelegate.parseCustomElement(ele); }}}}else {
      // Parse custom (extended) tagsdelegate.parseCustomElement(root); }}Copy the code

As you can see from the logic in the parseBeanDefinitions method, you end up with two branches: parsing default tags; One is analytical custom (extension) tag, here see parseCustomElement directly, and then enter the delegate class BeanDefinitionParserDelegate:

  // BeanDefinitionParserDelegate.java  

 public BeanDefinition parseCustomElement(Element ele) {
    return parseCustomElement(ele, null);
  }

  public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    String namespaceUri = getNamespaceURI(ele);
    / / key point
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    if (handler == null) {
      error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
      return null;
    }
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
  }

Copy the code

One of the key lines of code for the NamespaceHandler handler = this. ReaderContext. GetNamespaceHandlerResolver (). The resolve (namespaceUri), Get the corresponding NamespaceHandler class.

Where this.readerContext is instantiated in the registerBeanDefinitions method of XmlBeanDefinitionReader What is then passed down (see the first sequence diagram to see where this method fits in the process) is as follows:

// XmlBeanDefinitionReader.java

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    int countBefore = getRegistry().getBeanDefinitionCount();
    // createReaderContext see the method belowCreateReaderContext documentReader. RegisterBeanDefinitions (doc, see the following method (resource));return getRegistry().getBeanDefinitionCount() - countBefore;
  }

// Instantiate XmlReaderContext
public XmlReaderContext createReaderContext(Resource resource) {
    return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
        this.sourceExtractor, this, getNamespaceHandlerResolver());
}

/ / create DefaultNamespaceHandlerResolver
public NamespaceHandlerResolver getNamespaceHandlerResolver(a) {
    if (this.namespaceHandlerResolver == null) {
      this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
    }
    return this.namespaceHandlerResolver;
  }

protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver(a) {
    return new DefaultNamespaceHandlerResolver(getResourceLoader().getClassLoader());
  }

Copy the code

In order to draw this. ReaderContext. GetNamespaceHandlerResolver () returns is DefaultNamespaceHandlerResolver instance, And then check DefaultNamespaceHandlerResolver resolve (namespaceUri) method, the following view DefaultNamespaceHandlerResolver class code:

 // DefaultNamespaceHandlerResolver.java 
public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {

	/** * The location to look for the mapping files. Can be present in multiple JAR files. */
	public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";
    
    /** Save the mapping between the namespace URI and the NamespaceHandler class name */
    @Nullable
	private volatile Map<String, Object> handlerMappings;

      @Override
      public NamespaceHandler resolve(String namespaceUri) {
        Map<String, Object> handlerMappings = getHandlerMappings();
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        if (handlerOrClassName == null) {
          return null;
        }
        else if (handlerOrClassName instanceof NamespaceHandler) {
          return (NamespaceHandler) handlerOrClassName;
        }
        else {
          String className = (String) handlerOrClassName;
          try{ Class<? > handlerClass = ClassUtils.forName(className,this.classLoader);
            if(! NamespaceHandler.class.isAssignableFrom(handlerClass)) {throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                  "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
            }
            NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
            namespaceHandler.init();
            handlerMappings.put(namespaceUri, namespaceHandler);
            return namespaceHandler;
          }
          catch (ClassNotFoundException ex) {
            throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
                namespaceUri + "] not found", ex);
          }
          catch (LinkageError err) {
            throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
                namespaceUri + "]: problem with handler class file or dependent class", err); }}}/** * Load the specified NamespaceHandler mappings lazily. */
      private Map<String, Object> getHandlerMappings(a) {
        if (this.handlerMappings == null) {
          synchronized (this) {
            if (this.handlerMappings == null) {
              try {
                Properties mappings =
                    PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                if (logger.isDebugEnabled()) {
                  logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                }
                Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
                CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                this.handlerMappings = handlerMappings;
              }
              catch (IOException ex) {
                throw new IllegalStateException(
                    "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex); }}}}return this.handlerMappings; }}Copy the code

HandlerMappingsLocation defaults to meta-INF /spring.handlers, Handlers store information configured in meta-INF/Spring.Handlers in the handlerMappings attribute as a map using the getHandlerMappings() method above. For example, the Spring-Handlers file is configured as follows:

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler
Copy the code

Here will make www.springframework.org/schema/aop… The map key, org. Springframework. Aop. Config. The AopNamespaceHandler for corresponding value, in the handlerMappings.

Therefore, return to BeanDefinitionParserDelegate parseCustomElement method,

// BeanDefinitionParserDelegate.java  

 public BeanDefinition parseCustomElement(Element ele) {
    return parseCustomElement(ele, null);
  }

  public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    String namespaceUri = getNamespaceURI(ele);
    / / this. ReaderContext. GetNamespaceHandlerResolver corresponding to DefaultNamespaceHandlerResolver ()
    / / by DefaultNamespaceHandlerResolver resolve method, from the meta-inf/spring. The handlers of the allocation of access to the corresponding NamespaceHandler class
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    if (handler == null) {
      error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
      return null;
    }
    // Use the custom NamespaceHandler implementation class for parsing
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
  }

Copy the code

NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); You can obtain the corresponding NamespaceHandler class according to the Namespace URI, and finally call the Parse method of NamespaceHandler to parse the XML to bean Definition.

Next look at the NamespaceHandler#parse method, Usually by inherited abstract class NamespaceHandlerSupport (NamespaceHandlerSupport inherits from the interface NamespaceHandler) by registerBeanDefinitionParser to register a custom BeanDefinitionParser,

// abstract class NamespaceHandlerSupport

  @Override
  public BeanDefinition parse(Element element, ParserContext parserContext) {
    // Get BeanDefinitionParser and call its parse method
    return findParserForElement(element, parserContext).parse(element, parserContext);
  }

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // Get a name such as Aspectj-autoProxy
    String localName = parserContext.getDelegate().getLocalName(element);
   / / to get to the corresponding BeanDefinitionParser AspectJAutoProxyBeanDefinitionParser
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
      parserContext.getReaderContext().fatal(
          "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
  }

Copy the code

If BeanDefinitionParser is obtained with localName, then where is the relationship between BeanDefinitionParser and localName? To do this, override init() in the implementation class of NamespaceHandler.

// 1. Register BeanDefinitionParser

public class AopNamespaceHandler extends NamespaceHandlerSupport {
    public AopNamespaceHandler(a) {}public void init(a) {
        / / call the BeanDefinitionParser NamespaceHandlerSupport registerBeanDefinitionParser for registration
        this.registerBeanDefinitionParser("config".new ConfigBeanDefinitionParser());
        this.registerBeanDefinitionParser("aspectj-autoproxy".new AspectJAutoProxyBeanDefinitionParser());
        this.registerBeanDefinitionDecorator("scoped-proxy".new ScopedProxyBeanDefinitionDecorator());
        this.registerBeanDefinitionParser("spring-configured".newSpringConfiguredBeanDefinitionParser()); }}// abstract class NamespaceHandlerSupport
// The parsers attribute holds the mapping
private final Map<String, BeanDefinitionParser> parsers =
      new HashMap<String, BeanDefinitionParser>();

// the above init method is called in the property parsers
protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    this.parsers.put(elementName, parser);
  }

Copy the code

From the top, in the init () method calls this. RegisterBeanDefinitionParser method is realized in NamespaceHandlerSupport, finally saved in the properties of the Map type parsers.

Finally, find the corresponding BeanDefinitionParser with the corresponding localName. The final process for parsing Bean Definitions in XML is BeanDefinitionParser. Such as a chat Spring’s XML Schema extension mechanism shown in DistributedIdParser, as well as the AOP above the corresponding AspectJAutoProxyBeanDefinitionParser, etc.

This completes the basic analysis of how the Spring container loads and parses custom tags.

conclusion

By reading and analyzing the source code process of how the Spring container loads and parses custom tags, you can understand why the configuration of custom tags works and how Spring parses the configuration of XML into bean Definition. The whole process is completed with the spring source code call hierarchy, without a specific understanding of why the source code is designed in this way, such as useful in the process of design pattern delegation pattern, the subsequent can be further learning on this basis.