Spring source code analysis - IOC custom tag analysis

Overview

We have previously introduced the parsing of default tags in spring. After parsing, we will analyze the parsing of custom tags. Let us first review the methods used in custom tag parsing, as shown in the following figure:

We see that the parsing of custom tags is performed through BeanDefinitionParserDelegate.parseCustomElement(ele). Let us analyze it in detail.

Use of custom labels

Extending Spring custom tag configuration generally requires the following steps:

  1. Create a component that needs to be extended
  2. Define an XSD file to describe component content
  3. Create a class that implements the AbstractSingleBeanDefinitionParser interface to parse definitions and component definitions in XSD files
  4. Create a Handler that inherits NamespaceHandlerSupport and is used to register components to the Spring container
  5. Writing Spring.handlers and Spring.schemas files

Next, follow the steps above to implement a custom label component.

Create component

This component is just an ordinary JavaBean, nothing special. Here I created two components, why two, we will use them later

User.java

package dabin.spring01;

public class User {
    
    

    private String id;

    private String userName;

    private String email;public void setId(String id) {
    
    
        this.id = id;
    }public void setUserName(String userName) {
    
    
        this.userName = userName;
    }public void setEmail(String email) {
    
    
        this.email = email;
    }

    @Override
    public String toString() {
    
    
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"id\":\"")
                .append(id).append('\"');
        sb.append(",\"userName\":\"")
                .append(userName).append('\"');
        sb.append(",\"email\":\"")
                .append(email).append('\"');
        sb.append('}');
        return sb.toString();
    }
}

Phone.java

package dabin.spring01;

public class Phone {
    
    

    private String color;

    private int size;

    private String remark;


    public void setColor(String color) {
    
    
        this.color = color;
    }

    public void setSize(int size) {
    
    
        this.size = size;
    }

    public void setRemark(String remark) {
    
    
        this.remark = remark;
    }

    @Override
    public String toString() {
    
    
        final StringBuilder sb = new StringBuilder("{");
        sb.append("\"color\":\"")
                .append(color).append('\"');
        sb.append(",\"size\":")
                .append(size);
        sb.append(",\"remark\":\"")
                .append(remark).append('\"');
        sb.append('}');
        return sb.toString();
    }
}

Define XSD file

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

    <xsd:element name="phone">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string" />
            <xsd:attribute name="color" type="xsd:string" />
            <xsd:attribute name="size" type="xsd:int" />
            <xsd:attribute name="remark" type="xsd:string" />
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

A new targetNamespace is described in the above XSD file, and an element named user and phone is defined in this space . There are three attributes in user. Mainly to verify custom formats in Spring configuration files. To explain further, in the user-defined tag used in the Spring location file, the attributes can only be the three above. If there are other attributes, an error will be reported.

Parser class

Define a Parser class, which inherits AbstractSingleBeanDefinitionParser and implements getBeanClass()and doParse()two methods. Mainly used to parse definitions and component definitions in XSD files. Two Parser classes are defined here, one is used to parse the User class, and the other is used to parse the Phone class.

UserBeanDefinitionParser.java

package dabin.spring01;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    
    
    @Override
    protected Class getBeanClass(Element ele){
    
    
        return User.class;
    }

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

    }
}

PhoneBeanDefinitionParser.java

package dabin.spring01;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    
    
    @Override
    protected Class getBeanClass(Element ele){
    
    
        return Phone.class;
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
    
    
        String color = element.getAttribute("color");
        int size=Integer.parseInt(element.getAttribute("size"));
        String remark=element.getAttribute("remark");
        if(StringUtils.hasText(color)){
    
    
            builder.addPropertyValue("color",color);
        }
        if(StringUtils.hasText(String.valueOf(size))){
    
    
            builder.addPropertyValue("size", size);
        }
        if(StringUtils.hasText(remark)){
    
    
            builder.addPropertyValue("remark", remark);
        }

    }
}

Handler class

Define the Handler class and inherit NamespaceHandlerSupport. The main purpose is to register the parser Parser class defined above into the Spring container.

package dabin.spring01;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    
    

    @Override
    public void init() {
    
    
        registerBeanDefinitionParser("user",new UserBeanDefinitionParser());

        registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser());
    }

}

Let's see what the registerBeanDefinitionParser method does

private final Map<String, BeanDefinitionParser> parsers = new HashMap<>();

protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    
    
    this.parsers.put(elementName, parser);
}

That is to put the instances of the parsers UserBeanDefinitionParser and PhoneBeanDefinitionParser into the global Map, and the keys are user and phone.

Spring.handlers和Spring.schemas

Write Spring.handlers and Spring.schemas files. The default locations are placed in the META-INF folder of the project.

Spring.handlers

http\://www.dabin.com/schema/user=dabin.spring01.MyNamespaceHandler

Spring.schemas

http\://www.dabin.com/schema/user.xsd=org/user.xsd

The general process of Spring loading customization is to encounter the custom tag and then go to Spring.handlers and Spring.schemas to find the corresponding handler and XSD. The default location is under META-INF, and then find the corresponding handler and parsing elements. Parser, thus completing the parsing of the entire custom element, which means that Spring delegates the work of parsing the defined tags to the user.

Create test configuration file

After the above steps, you can use custom labels. Use the following in the xml configuration file:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:myTag="http://www.dabin.com/schema/user"
       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
       http://www.dabin.com/schema/user http://www.dabin.com/schema/user.xsd">

    <bean id="myTestBean" class="dabin.spring01.MyTestBean"/>

    <myTag:user id="user" email="[email protected]" userName="dabin" />

    <myTag:phone id="iphone" color="black" size="128" remark="iphone XR"/>

</beans>

xmlns:myTag means that the namespace of myTag is **http://www.dabin.com/schema/user. **At the judgment point at the beginning of the article if (delegate.isDefaultNamespace(ele)) will definitely return false and you will enter Parsing of custom tags

test

import dabin.spring01.MyTestBean;
import dabin.spring01.Phone;
import dabin.spring01.User;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class AppTest {
    @Test
    public void MyTestBeanTest() {
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml"));
        //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean");
        User user = (User) bf.getBean("user");
        Phone iphone = (Phone) bf.getBean("iphone");

        System.out.println(user);
        System.out.println(iphone);
    }

}

Output result:

("id":"user","userName":"dabin","email":"dabin@163. com”}
{"color":"black","size":128,"remark":"iphone XR"}

Parsing of custom tags

After understanding the use of custom tags, let's analyze the parsing of custom tags. The method used to parse custom tags is: parseCustomElement(Element ele, @Nullable BeanDefinition containingBd). Enter the method body:

public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    
    
    // 获取 标签对应的命名空间
    String namespaceUri = getNamespaceURI(ele);
    if (namespaceUri == null) {
    
    
        return null;
    }

    // 根据 命名空间找到相应的 NamespaceHandler
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    if (handler == null) {
    
    
        error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
        return null;
    }

    // 调用自定义的 Handler 处理
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

I believe that after understanding how to use custom tags, you will more or less have your own ideas about the implementation process of custom tags. In fact, the idea is very simple. It is nothing more than obtaining the corresponding namespace according to the corresponding bean, parsing the corresponding processor according to the namespace, and then parsing according to the user-defined processor.

Get the namespace of the label

The parsing of tags starts with the introduction of namespaces. Whether to distinguish default tags and custom tags in Spring or to distinguish between different tags in custom tags is based on the namespace provided by the tag. As for the processor How to extract the namespace of the corresponding element does not actually require us to implement it ourselves. Methods have been provided in org.w3c.dom.Node for us to call directly:

String namespaceUri = getNamespaceURI(ele);
@Nullable
public String getNamespaceURI(Node node) {
    return node.getNamespaceURI();
}

Here we can see through DEBUG that the namespaceUri corresponding to the myTag:user custom tag is http://www.dabin.com/schema/user

Read custom tag processor

Get Handler based on namespaceUri. We have already defined this mapping relationship in Spring.handlers, so we only need to find the class, initialize it and return it, and finally call the method of the Handler object for processing parse(). We also provide an implementation of this method. So the core of the above is how to find the Handler class. The calling method is:this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)

public NamespaceHandler resolve(String namespaceUri) {
    
    
    // 获取所有已经配置的 Handler 映射
    Map<String, Object> handlerMappings = getHandlerMappings();

    // 根据 namespaceUri 获取 handler的信息:这里一般都是类路径
    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() 方法
            namespaceHandler.init();

            // 记录在缓存
            handlerMappings.put(namespaceUri, namespaceHandler);
            return namespaceHandler;
        }
        catch (ClassNotFoundException ex) {
    
    
            throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
                    "] for namespace [" + namespaceUri + "]", ex);
        }
        catch (LinkageError err) {
    
    
            throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
                    className + "] for namespace [" + namespaceUri + "]", err);
        }
    }
}

First call to getHandlerMappings()obtain the mapping relationship handlerMappings in all configuration files, that is, we configure the mapping relationship between the namespace and the namespace processor in the Spring.handlers file. The relationship is <namespace, classpath>, and then according to the namespace namespaceUri from the mapping Obtain the corresponding information from the relationship. If it is empty or has been initialized, it will be returned directly. Otherwise, it will be initialized based on reflection, its method will be called at the same time init(), and finally the Handler object will be cached. Let's recall again what we said about the namespace processor in the example:

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    
    

    @Override
    public void init() {
    
    
        registerBeanDefinitionParser("user",new UserBeanDefinitionParser());

        registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser());
    }

}

When the custom namespace is processed, namespaceHandler.init() will be executed immediately to register the custom BeanDefinitionParser. Here, you can register multiple tag parsers. For example, in the current example, the <myTag:user tag uses new UserBeanDefinitionParser( ) parser; <myTag:phone uses the new PhoneBeanDefinitionParser() parser.

As we have said above, the registerBeanDefinitionParser method in init() actually puts the mapping relationship in a parsers object of a Map structure: private final Map<String, BeanDefinitionParser> parsers.

Tag parsing

After obtaining the parser and analyzed elements, Spring can delegate the parsing work to the custom parser. For label parsing, use: NamespaceHandler.parse(ele, new ParserContext(this.readerContext, this, containingBd )) method, enter the method body:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    
    
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    return (parser != null ? parser.parse(element, parserContext) : null);
}

Calling findParserForElement()the method to obtain the BeanDefinitionParser instance is actually to obtain init()the instance object registered in the method. as follows:

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    
    
    //获取元素名称,也就是<myTag:user中的 user
    String localName = parserContext.getDelegate().getLocalName(element);
    //根据 user 找到对应的解析器,也就是在
    //registerBeanDefinitionParser("user",new UserBeanDefinitionParser());
    //中注册的解析器
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
    
    
        parserContext.getReaderContext().fatal(
                "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

Get the localName, which in the above example is: user, and then get the UserBeanDefinitionParser instance object from the Map instance parsers. After returning the BeanDefinitionParser object, call it parse(). This method is implemented in AbstractBeanDefinitionParser:

We can see from DEBUG that the current tag is **<myTag:user, the corresponding localName of ** is user, the corresponding custom parser is UserBeanDefinitionParser, and the returned instance object is UserBeanDefinitionParser. Next we look at parser.parse (element, parserContext), which is implemented in AbstractBeanDefinitionParser:

public final BeanDefinition parse(Element element, ParserContext parserContext) {
    
    
    AbstractBeanDefinition definition = parseInternal(element, parserContext);
    if (definition != null && !parserContext.isNested()) {
    
    
        try {
    
    
            String id = resolveId(element, definition, parserContext);
            if (!StringUtils.hasText(id)) {
    
    
                parserContext.getReaderContext().error(
                        "Id is required for element '" + parserContext.getDelegate().getLocalName(element)
                                + "' when used as a top-level tag", element);
            }
            String[] aliases = null;
            if (shouldParseNameAsAliases()) {
    
    
                String name = element.getAttribute(NAME_ATTRIBUTE);
                if (StringUtils.hasLength(name)) {
    
    
                    aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
                }
            }
            BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
            //将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册
            registerBeanDefinition(holder, parserContext.getRegistry());
            if (shouldFireEvents()) {
    
    
                BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
                postProcessComponentDefinition(componentDefinition);
                parserContext.registerComponent(componentDefinition);
            }
        }
        catch (BeanDefinitionStoreException ex) {
    
    
            String msg = ex.getMessage();
            parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element);
            return null;
        }
    }
    return definition;
}

Although it is parsing a custom configuration file, we can see that most of the code in this function is used to process the function of converting the parsed AbstractBeanDefinition into a BeanDefinitionHolder and registering it, while the actual parsing is delegated to parseInternal, this code actually calls our custom parsing function. In parseInternal, the custom doParse function is not called directly, but a series of data preparations are performed, including preparation of attributes such as beanClass, scope, lazyInit, etc. We enter the AbstractSingleBeanDefinitionParser.parseInternal method:

protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
    
    
    // 创建一个BeanDefinitionBuilder,内部实际上是创建一个GenericBeanDefinition的实例,用于存储自定义标签的元素
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();

    // 获取父类元素
    String parentName = getParentName(element);
    if (parentName != null) {
    
    
        builder.getRawBeanDefinition().setParentName(parentName);
    }

    // 获取自定义标签中的 class,这个时候会去调用自定义解析中的 getBeanClass()
    Class<?> beanClass = getBeanClass(element);
    if (beanClass != null) {
    
    
        builder.getRawBeanDefinition().setBeanClass(beanClass);
    }
    else {
    
    
        // beanClass 为 null,意味着子类并没有重写 getBeanClass() 方法,则尝试去判断是否重写了 getBeanClassName()
        String beanClassName = getBeanClassName(element);
        if (beanClassName != null) {
    
    
            builder.getRawBeanDefinition().setBeanClassName(beanClassName);
        }
    }
    builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
    BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
    if (containingBd != null) {
    
    
        // Inner bean definition must receive same scope as containing bean.
        builder.setScope(containingBd.getScope());
    }
    if (parserContext.isDefaultLazyInit()) {
    
    
        // Default-lazy-init applies to custom bean definitions as well.
        builder.setLazyInit(true);
    }

    // 调用子类的 doParse() 进行解析
    doParse(element, parserContext, builder);
    return builder.getBeanDefinition();
}

public static BeanDefinitionBuilder genericBeanDefinition() {
    
    
    return new BeanDefinitionBuilder(new GenericBeanDefinition());
}

protected Class<?> getBeanClass(Element element) {
    
    
    return null;
}

protected void doParse(Element element, BeanDefinitionBuilder builder) {
    
    
}

In this method we mainly focus on two methods: getBeanClass(),doParse() . For getBeanClass()the method, the AbstractSingleBeanDefinitionParser class does not provide a specific implementation, but directly returns null, which means that it hopes that the subclass can override the method. Of course, if the method is not rewritten, it will be called to determine whether the subclass has been rewritten getBeanClassName(). this method. For doParse()it is a direct empty implementation. So for parseInternal()it always expects its subclasses to implement getBeanClass(), doParse(), among which doParse()is particularly important. If you don't provide an implementation, how to parse custom tags? Finally, review the custom parser: UserDefinitionParser again.

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    
    
    @Override
    protected Class getBeanClass(Element ele){
    
    
        return User.class;
    }

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

    }
}

Let's take a look at builder.addPropertyValue ("id",id). It actually parses the properties in the custom tag and stores them in the beanDefinition instance in BeanDefinitionBuilder.

private final AbstractBeanDefinition beanDefinition;

public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) {
    this.beanDefinition.getPropertyValues().add(name, value);
    return this;
}

Finally, convert AbstractBeanDefinition to BeanDefinitionHolder and register registerBeanDefinition(holder, parserContext.getRegistry()); this is the same as the registration of the default tag.

At this point, the analysis process of custom tags has been completed. In fact, the whole process is relatively simple: first, the handlers file will be loaded, the contents will be parsed to form a mapping of <namespaceUri, classpath>, and then the corresponding classpath can be obtained based on the obtained namespaceUri, and it will be initialized and waited. Corresponding Handler object, call parse()the method, in which the corresponding BeanDefinitionParser instance object is obtained according to the localName of the tag, and the method is called parse(). This method is defined in the AbstractBeanDefinitionParser abstract class, and the core logic is encapsulated in parseInternal()it . This method returns an AbstractBeanDefinition instance object, whose It is mainly implemented in AbstractSingleBeanDefinitionParser. For the custom Parser class, it needs to implement getBeanClass()or getBeanClassName()and doParse(). Finally, the AbstractBeanDefinition is converted into a BeanDefinitionHolder and registered.

Guess you like

Origin blog.csdn.net/Tyson0314/article/details/133281604