Spring自定义XML标签解析及其原理分析

一、自定义XML标签

先新建一个类

public class User {
    private String userName;
    private String email;
    ...省略setter、getter
}

新建一个BeanDefinitionParser

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class<?> getBeanClass(Element element) {
        return User.class;
    }
    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String name = element.getAttribute("userName");
        String email = element.getAttribute("email");
        if (StringUtils.hasText(name))
            builder.addPropertyValue("userName", name);
        if (StringUtils.hasText(email))
            builder.addPropertyValue("email", email);
    }
}

新建一个NameSpaceHandler

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

在resources/META-INF目录下建一个spring-test.xsd文件(用于XML检验)

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

XML Schema可查阅 http://www.w3school.com.cn/schema/index.asp

在resources/META-INF目录下新建spring.handlers文件写上

http\://www.season.com/schema/user=com.season.meta.UserNameSpaceHandler

在resources/META-INF目录下新建spring.schemas文件写上

http://www.season.com/schema/user.xsd=META-INF/spring-test.xsd

在resources目录下建一个beans.xml

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:myname="http://www.season.com/schema/user"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.season.com/schema/user
       http://www.season.com/schema/user.xsd">

    <myname:user id="user" email="[email protected]" userName="season"/>
</beans>

测试:

@Test
public void testNameSpaceHandler(){
    ApplicationContext actx = new ClassPathXmlApplicationContext("beans.xml");
    User user = (User) actx.getBean("user");
    System.out.println(user);
}

二、原理分析

取XML配置的是XmlBeanDefinitionReader,它会先用SAX技术把XML配置文件读取成一个Document对象,然后利用BeanDefinitionDocumentReader对Document对象的节点进行解析,最后把bean封装成BeanDefinition存储在BeanRegistry中(参见:Spring IOC实现原理笔记(二) – 加载XML配置),如下:

XmlBeanDefinitionReader类

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    //实际创建的是DefaultBeanDefinitionDocumentReader类
     BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader();
     int countBefore = this.getRegistry().getBeanDefinitionCount();
    //【标记1】用BeanDefinitionDocumentReader对Document对象的节点进行解析
     documentReader.registerBeanDefinitions(doc, this.createReaderContext(resource));
     return this.getRegistry().getBeanDefinitionCount() - countBefore;
}

上面【标记1】出就是解析节点的开始,节点分默认节点和自定义节点两种情况。其中解析自定义节点的源码如下:

DefaultBeanDefinitionDocumentReader类


    //解析自定义节点
    public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
        String namespaceUri = this.getNamespaceURI(ele);
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if(handler == null) {
            ...
            return null;
        } else {
            return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
        }
    }

上面代码中,首先会拿到节点的中的命名空间URI,(在上面的例子中是:http://www.season.com/schema/user),接着从NamespaceHandlerResolver中拿到对应该命名空间URI解析的NamespaceHandler,然后调用NamespaceHandler的parse进行解析。

关键代码是this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri),其中readerContext是上面【标记1】处的createReaderContext(resource)创建出来的,如下:

XmlBeanDefinitionReader类

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

//上面调用了getNamespaceHandlerResolver方法
public NamespaceHandlerResolver getNamespaceHandlerResolver() {
    if(this.namespaceHandlerResolver == null) {
        this.namespaceHandlerResolver = this.createDefaultNamespaceHandlerResolver();
    }
    return this.namespaceHandlerResolver;
}

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

上面可以看到getNamespaceHandlerResolver()获取到的是DefaultNamespaceHandlerResolver实例,所以接下来去看该类

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {
    ...
    //【标记2】
    public DefaultNamespaceHandlerResolver(ClassLoader classLoader) {
        this(classLoader, "META-INF/spring.handlers");
    }

    public NamespaceHandler resolve(String namespaceUri) {
        Map<String, Object> handlerMappings = this.getHandlerMappings();
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        if(handlerOrClassName == null) {
            return null;
        } else if(handlerOrClassName instanceof NamespaceHandler) {
            return (NamespaceHandler)handlerOrClassName;
        } else {
            String className = (String)handlerOrClassName;
            ...
               Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
               ...
              //【标记3】
               NamespaceHandler namespaceHandler = (NamespaceHandler)BeanUtils.instantiateClass(handlerClass);
               //【标记4】
               namespaceHandler.init();
               handlerMappings.put(namespaceUri, namespaceHandler);
               return namespaceHandler;
            ...
        }
    }

    private Map<String, Object> getHandlerMappings() {
        if(this.handlerMappings == null) {
            synchronized(this) {
                if(this.handlerMappings == null) {
                    try {
                        //【标记4】
                        Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                        ...
                        Map<String, Object> handlerMappings = new ConcurrentHashMap(mappings.size());
                        CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                        this.handlerMappings = handlerMappings;
                    } catch...
                }
            }
        }
        return this.handlerMappings;
    }
    ...
}

DefaultNamespaceHandlerResolver是会把所有NamespaceHandler缓存到handlerMappings(ConcurrentHashMap实例)里,resolve方法就是从handlerMappings获取相应的NamespaceHandler返回出去。

说说上面的标记的地方:
【标记2】:Spring默认的NamespaceHandler配置文件路径为META-INF/spring.handlers
【标记3】:未初始化的NamespaceHandler是通过反射实例出来的
【标记4】:实例化出NamespaceHandler后,第一件事是调用其init方法
【标记5】:加载META-INF/spring.handlers配置文件,并记录到handlerMappings。

总结

Spring在解析XML的时候,解析到是自定义节点,就会找相对应的NameSpaceHandler去解析;而Spring默认加载写在META-INF/spring.handlers配置文件中配置的NameSpaceHandler,通过反射将其实例化

2、NameSpaceHandler解析节点

那NameSpaceHandler如何解析的呢?以上面的例子来分析。首先解析自定义节点的源码中,Spring拿到相应的NameSpaceHandler后,会调用器parse方法,而上面自定义的UserNameSpaceHandler没看到有parse方法,那就去父类,如下:

NamespaceHandlerSupport类

public abstract class NamespaceHandlerSupport implements NamespaceHandler {
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        return this.findParserForElement(element, parserContext).parse(element, parserContext);
    }

    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
        //拿到标签名称(对应上面例子的 user)
        String localName = parserContext.getDelegate().getLocalName(element);
        BeanDefinitionParser parser = (BeanDefinitionParser)this.parsers.get(localName);
        ...
        return parser;
    }

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

上面看到parse方法调用了findParserForElement拿到BeanDefinitionParser,再调BeanDefinitionParser的parse方法进行解析。要拿到BeanDefinitionParser看这句代码this.parsers.get(localName),其中parsers是一个Map,键存标签名称,值为相对应的BeanDefinitionParser。那我们自定义的BeanDefinitionParser(对应上面的UserBeanDefinitionParser)什么时候放进这个Map中呢?

回顾下【标记4】处代码,发现实例化出NamespaceHandler后,第一件事是调用其init方法,我们自定义的UserNameSpaceHandler在init方法中调用了registerBeanDefinitionParser方法,该方法在父类中,做的事情就是把我们传入的键值对存入parsers这个Map中,所以可以被找到并取出来进行解析节点的操作了。

猜你喜欢

转载自blog.csdn.net/seasonLai/article/details/82696373