spring通过ClassPathXmlApplicationContext解析applicationContext.xml的自定义标签解析过程
解析类DefaultBeanDefinitionDocumentReader.java中delegate.parseCustomElement(ele)这个方法用来解析自定义,为了方便理解整个解析过程,先在xml配置文件中加入一个非常常用的自定义标签context:component-scan,通过跟踪这个标签的解析过程,从而理解整个自定义标签的解析过程,进入BeanDefinitionParserDelegate类中该方法的实现:
//spring版本为5.3
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
//1.获取到元素的namespaceUri
//component-scan标签的uri=http://www.springframework.org/schema/context
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
//2.根据传入的namespaceUri进行解析
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));
}
步骤一: 获取到元素对应的namespaceUri,对于context:component-scan标签namespaceUri=http://www.springframework.org/schema/context,这个值就是在xml配置文件里配置的
步骤二:NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);传入刚才拿到的uri的值进行解析,首先调用this.readerContext.getNamespaceHandlerResolver()获取DefaultNamespaceHandlerResolver对象,在这个类的构造方法中传入了一个文件路径META-INF/spring.handlers。
以context:component-scan为例,找到context这个自定义注解的所在位置,打开spring-context-5.3.3.jar中的META-INF/spring.handlers,文件中保存了NamespaceUri和Handler处理类的对应关系,因此DefaultNamespaceHandlerResolver这个类的作用就是用来解析和收集NamespaceUri和对应Handler的映射关系。
进入resolve方法,在方法里的第一行加一个断点,使用调试方式启动,在DefaultNamespaceHandlerResolver类中维护了一个handlerMappings,观察里面的值发现,Spring将所有依赖包中的META-INF/spring.handlers里配置的映射关系都维护进了这个Map,这是Spring使用Spi机制加载自定义标签的方式,所有实现自定义标签解析的Handler都需要在META-INF/spring.handlers文件中进行配置,这样Spring在启动的时候就会把所有的Handler都维护到handlerMappings中。
public NamespaceHandler resolve(String namespaceUri) {
//根据namespaceUri从Map中获取到对应的handler,获取到的是handler类全路径的字符串
Map<String, Object> handlerMappings = getHandlerMappings();
//handlerOrClassName是字符串类型,因此前面两个条件都不会进,只会进入这个条件分支
//以Component-scan为例,此时的className是“org.springframework.context.config.ContextNamespaceHandler”
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
//获取Handler的Class类型
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");
}
//依据Class创建Handler实例,这一步就创建出一个ContextNamespaceHandler对象
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
//这一步会调用ContextNamespaceHandler的init方法,每个Handler都会实现自己的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);
}
}
}
在resolve方法中,调用了namespaceHandler.init()方法,这是父类提供的抽象方法,由不同的子类具体实现。进入ContextNamespaceHandler,在init方法中对标签中所有冒号(:)后面的属性都对应了一个解析器,这里就看到了熟悉的component-scan、property-placeholder等。而this.registerBeanDefinitionParser()方法的逻辑也很简单,就是把name和parser的对应关系加入到其父类维护的一个parsers集合中,集合类型是这样的:private final Map<String, BeanDefinitionParser> parsers = new HashMap()
在完成了init初始化操作后,程序接着调用了handlerMappings.put(namespaceUri, namespaceHandler)将NamespaceUri和Handler实例重新维护进handlerMappings中,把之前维护的namespaceUri和handler的全类名覆盖掉了,最终将namespaceHandler实例返回。继续回到BeanDefinitionParserDelegate.parseCustomElement()方法,通过上面对resolve方法的分析,该方法返回了一个namespaceHandler实例,调用namespaceHandler.parse()方法完成对注解的解析,这个方法同样是抽象方法,由子类NamespaceHandlerSupport去做具体实现,我们跟到方法中
NamespaceHandlerSupport
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
//该方法通过传入的element对象返回对应的parser实例
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return parser != null ? parser.parse(element, parserContext) : null;
}
@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
//获取到标签的名字,例如<component-scan>标签,获取到的localName就是“component-scan”
String localName = parserContext.getDelegate().getLocalName(element);
//从parsers集合中通过localName获取到对应的Parser实例
BeanDefinitionParser parser = (BeanDefinitionParser)this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal("Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
首先调用this.findParserForElement(element, parserContext)方法获取到对应的parser对象。我们跟踪到这个方法中可以发现,就是从parsers集合中通过标签名称获取到对应的parser对象,parsers集合已经提到过,就是上面在调用init方法时维护的那个集合对象。
获取到parser对象后,调用parse方法。所有的parser最终都会实现BeanDefinitionParser接口。因此,所有的parser类都会重写parse方法,并在parse方法中实现自身的标签解析的逻辑。
上面对整个自定义标签的解析流程进行了记录,总结起来主要是以下流程:
1)在META-INF/spring.handlers文件中定义NamespaceUri和Handler的映射关系,以便Spring在启动的时候能够扫描到这个映射关系。
2)在Handler实现类中统一继承NamespaceHandlerSupport父类,并实现init()方法,在init()方法中为每一个标签名创建一个对应的Parser,并将其对应关系维护到父类的parsers集合中。
3)每一个Parser类都需要实现接口BeanDefinitionParser,并实现parse方法,在parse方法中实现具体的解析逻辑。
通过对自定义注解整体流程的分析,似乎自己实现一个自定义注解也很简单,来试一下。
第一步,在自己的代码工程下的classpath下创建一个META-INF文件夹,并在里面创建需要的文件,spring.handlers、spring.schema和custom-pring.xsd(这个文件是可以随意命名的)。第一个文件不用说了,上面已经提到过很多次了,第二个文件维护了xsd location和具体xsd文件的映射关系,打开这个文件:
https\://www.springframework.org/schema/context/spring-test.xsd=META-INF/custom-print.xsd
看到里面的内容就应该明白了,前面的key就是在spring配置文件的xsi:schemaLocation里配置的xsd的信息,后面的value指定的就是上面第三个文件,那不用说了,第三个文件就是对xml格式的限定文件。
<?xml version="1.0" encoding="UTF-8" ?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/test"
elementFormDefault="qualified">
<element name="print">
<complexType>
<attribute name="id" type="string"/>
<attribute name="xmlPath" type="string"/>
</complexType>
</element>
第二步,在spring.handlers文件中定义NamespaceUri和Handler的映射关系,并创建Handler类继承NamespaceHandlerSupport,这里随便定义了一个TestParserHandler。
http\://www.springframework.org/schema/test=com.sgcc.uvmp.config.TestParserHandler
第三步,在TestParserHandler中,实现init方法,为每一个标签名都定义一个Parser类,并维护对应关系。这里只定义了一个名称为"print"的标签和它的Parser类PrintBeanDefinitionParser
public class TestParserHandler extends NamespaceHandlerSupport {
@Override
public void init() {
this.registerBeanDefinitionParser("print", new PrintBeanDefinitionParser());
}
}
第四步,创建PrintBeanDefinitionParser类实现BeanDefinitionParser接口,实现parse方法,在方法中实现具体的业务逻辑。这个自定义的parser逻辑很简单,就是读取出配置文件配置的属性,填充到一个对应的类(PrintXml)中,并为这个类创建了一个BeanDefinition对象,加入到BeanDefinitionRegistry容器中,这样同样可以实现通过Spring管理这个类实例。
public class PrintBeanDefinitionParser implements BeanDefinitionParser {
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
//获取标签中的属性
String id = element.getAttribute("id");
String xmlPath = element.getAttribute("xmlPath");
//创建一个BeanDefinition对象,把需要实例化的类的class名填充到bd中
GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
genericBeanDefinition.setBeanClass(PrintXml.class);
genericBeanDefinition.setBeanClassName(PrintXml.class.getName());
//将对应的属性名称和属性值填充到bd的MutablePropertyValues对象中,这样在实例化这个类的时候,就会把对应的属性值填充到类实例中。
genericBeanDefinition.getPropertyValues().add("id" , id);
genericBeanDefinition.getPropertyValues().add("xmlPath" , xmlPath);
//将beanName和bd对象注册到BeanDefinitionRegsitry容器中,这样Spring在做Bean的实例化的时候就会扫描到这个类,并把它进行实例化。
parserContext.getRegistry().registerBeanDefinition("printXml" , genericBeanDefinition);
return null;
}
}
这样,一个自定义注解就完成了,和使用Spring提供的自定义注解方式相同,需要在spring的配置文件中对其进行配置即可。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:test="http://www.springframework.org/schema/test"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
<!--这里是自己定义的NamespaceUri-->
http://www.springframework.org/schema/test
<!--这里是自己定义的xsd的namespaceuri-->
https://www.springframework.org/schema/context/spring-test.xsd">
<!--自己定义的标签就可以这样使用了,逻辑比较简单,只是在里面定义了两个属性,和parser里获取的那两个属性是可以对应上的-->
<test:print id="hello" xmlPath="world"></test:print>
</beans>
来进行一下测试,可以看到PrintXml这个类我们并没有在Spring的配置文件里配置,在类上面也没有任何注解,但是通过context.getBean(PrintXml.class)是可以获取到这个类的实例的,说明自定义标签已经生效。
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
PrintXml printXml = context.getBean(PrintXml.class);
System.out.println(printXml);
这样我们就自己定义了一个标签,如果需要实现更加复杂的业务逻辑,就只在Parser类中的parse方法中做相应的实现即可,在下一节中,我们将进入component-scan的Parser类,详细记录Spring通过context:component-scan标签进行包扫描的全过程。
————————————————
原文链接:https://blog.csdn.net/eighthspace/article/details/112960963