又来填坑啦,上一篇讲完默认标签的解析,这篇笔记记录一下自定义标签的解析吧。php
咱们知道,Spring
源码的核心模块是 Spring-core
和 Spring-beans
,在此基础上衍生出其余模块,例如 context
、 cache
、 tx
等模块,都是根据这两个基础模块进行扩展的。java
聪明如你,应该想到咱们代码中经常使用的缓存注解 @Cacheable
、事务注解 @Transaction
,还有阿里巴巴的 RPC
中间件 Dubbo
,在配置文件中经过 <dubbo:service/>
或者 <dubbo:reference/>
进行服务注册和订阅,这些都都属于 Spring
的自定义标签的实现,经过自定义标签能够实现更增强大的功能!node
做为一个有追求的程序员,固然不能知足于框架自带默认的标签,为了扩展性和配置化要求,这时候就须要学习自定义标签和使用自定义标签~git
先来看一张源码图片(红框框圈着是重点哟)程序员
刚才说了缓存和事务,那就拿这两个举例,还有一个标签 <myname:>
(这个我也不太清楚,网上查的资料也很少,因此按照个人理解你们跟说下)github
首先咱们看到,<tx>
<cache>
<mvc>
和 <myname>
都是自定义标签,左一是配置文件,进行 bean
的定义,顶部的 xmlns
是命名空间,表示标签所属的定义文件,像事务、缓存、MVC
的命名空间都是固定的。spring
而 myname
至关于万金油,既能够定义为事务,又能够定义为缓存,只要咱们在命名空间中进行相应的定义就能正确的识别。这个就是咱们待会要使用到的自定义标签,经过命名空间定位到咱们想要的处理逻辑。缓存
中间的是缓存定义的 xsd
文件,经过 <xsd:element name="annotation-driven">
定义元素,<xsd:complexType>
区间内定义属性列表,<xsd:attribute>
定义单个属性,详细分析能够看下注释~bash
右边的是事务定义的 xsd
文件,大致内容的跟中间同样,虽然元素名称 <annotation-driven>
有相同的,可是下面的属性定义是有所区别的。mvc
因此咱们对自定义注解有个大概的了解,xsd
描述文件是个其中一个关键,在配置文件顶部的命名空间是标签进行解析时,进行定位的配置,固然还有处理器,下面使用时进行介绍。
不知道理解的对不对,若是有误的话请大佬们指出,我会进行修改的!
Spring
提供了可扩展的 Schema
的支持,扩展 Spring
自定义标签配置须要如下几个步骤:
XSD
描述文件BeanDefinitionParse
接口,用来解析 XSD
文件中的定义和组件定义。Handler
文件,扩展自 NamespaceHandlerSupport
,将组件注册到 Spring
容器Spring.handlers
和 Spring.schemas
文件刚开始看到这些流程时,我仍是有点慌的,毕竟从一个使用默认标签的萌新小白,忽然要我本身定义,感受到很新鲜,因此请各位跟着下面的流程一块儿来看吧~
这个没啥好说的,就是一个普通的类:
public class Product {
private Integer productId;
private String unit;
private String name;
}
复制代码
XSD
描述文件custom-product.xsd
<xsd:schema targetNamespace="http://vip-augus.github.io/schema/product" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<!-- 注释 3.4 自定义元素 -->
<xsd:element name="product">
<xsd:complexType>
<!-- 这个是类注册时的名字,组件中请不要占用该字段~ -->
<xsd:attribute name="id" type="xsd:string"/>
<!-- 属性定义列表,名字和类型 -->
<xsd:attribute name="productId" type="xsd:integer"/>
<xsd:attribute name="unit" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
复制代码
我在上面的描述文件中,定义了一个新的 targetNamespace
,同时定义了一个 叫 product
的新元素,而且将组件中的属性都列在 <xsd:attribute>
中。XSD
文件是 XML
DTD
的替代者,具体就很少深刻,感兴趣的同窗能够继续深刻了解。
base.label.custom.ProductBeanDefinitionParser
public class ProductBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class getBeanClass(Element element) {
// 返回对应的类型
return Product.class;
}
// 从 element 中解析并提取对应的元素
@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String productId = element.getAttribute("productId");
String productName = element.getAttribute("name");
String productUnit = element.getAttribute("unit");
// 将提取到的数据放入 BeanDefinitionBuilder 中,等到完成全部 bean 的解析以后统一注册到 beanFactory 中
if (productId != null) {
// element.getAttribute("") 方法取出来的都是 string 类型,使用时记得手动转换
builder.addPropertyValue("productId", Integer.valueOf(productId));
}
if (StringUtils.hasText(productName)) {
builder.addPropertyValue("name", productName);
}
if (StringUtils.hasText(productUnit)) {
builder.addPropertyValue("unit", productUnit);
}
}
}
复制代码
关键点在于,咱们的解析器是继承于 AbstractSingleBeanDefinitionParser
,重载了两个方法,详细用途请看注释~
base.label.custom.ProductBeanHandler
public class ProductBeanHandler extends NamespaceHandlerSupport {
@Override
public void init() {
// 将组件解析器进行注册到 `Spring` 容器
registerBeanDefinitionParser("product", new ProductBeanDefinitionParser());
}
}
复制代码
这个类也比较简单,关键是继承了 NamespaceHandlerSupport
,对他进行了扩展,在该类初始化时将组件解析器进行注册到 Spring
容器中。
spring.hanlders
和 spring.schemas
文件我将文件位置放在 resources
-> META-INF
目录下:
spring.handlers
http\://vip-augus.github.io/schema/product=base.label.custom.ProductBeanHandler
复制代码
spring.schemas
http\://vip-augus.github.io/schema/product.xsd=custom/custom-product.xsd
复制代码
到了这一步,自定义的配置就结束了。下面是如何使用
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注意 schema 位置,最后两行是我新增的自定义配置 -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:myname="http://vip-augus.github.io/schema/product" xsi:schemaLocation="http://www.springframework.org/schema/beans http://vip-augus.github.io/schema/product http://vip-augus.github.io/schema/product.xsd">
<!-- 自定义标签使用 -->
<myname:product id="product" productId="1" name="Apple" unit="台"/>
</beans>
复制代码
public class ProductBootstrap {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("custom/custom-label.xml");
Product product = (Product) context.getBean("product");
// 输出 Product{, productId ='1', unit='台', name='Apple'}
System.out.println(product.toString());
}
}
复制代码
如今来回顾一下,Spring
遇到自定义标签是,加载自定义的大体流程:
spring.hanlders
和 spring.schemas
:在两个文件中找到对应的 handler
和 XSD
,默认位置在 resources
-> META-INF
。Handler
注册 Parser
:扩展了 NamespaceHandlerSupport
的类,在初始化注册解析器Parser
:扩展了 AbstractSingleBeanDefinitionParser
,经过重载方法进行属性解析,完成解析。上面已经将自定义注解的使用讲了,接下来说的是源码中如何对自定义标签进行解析。
在上一篇笔记中,讲了如何解析默认标签,Spring
判断一个标签不是默认标签的话,就会将这个标签解析交给自定义标签的解析方法
直接定位到解析自定义标签的方法吧:
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition)
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 注释 3.8 ① 找到命名空间
String namespaceUri = getNamespaceURI(ele);
// ② 根据命名空间找到对应的 NamespaceHandler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
// ③ 调用自定义的 NamespaceHandler 进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
复制代码
看着流程是否是以为很熟悉,咱们刚才在自定义标签使用时,定义的文件顺序是同样的,下面来说下这三个方法,具体代码不会贴太多,主要记录一些关键方法和流程,详细代码和流程请下载我上传的工程~
public String getNamespaceURI(Node node) {
return node.getNamespaceURI();
}
复制代码
这个方法具体作的事情很简单,并且传参的类型 org.w3c.dom.Node
,已经提供了现成的方法,因此咱们只须要调用便可。
具体解析方法这这个类中:
org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver#resolve
public NamespaceHandler resolve(String namespaceUri) {
// 注释 3.9 获取全部已经配置的 handler 映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 从 map 中取出命名空间对应的 NamespaceHandler 的 className
// 这个映射 map 值,没有的话,会进行实例化类,而后放入 map,等下次一样命名空间进来就能直接使用了
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);
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);
// 调用 handler 的 init() 方法
namespaceHandler.init();
// 放入 handler 映射中
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
}
复制代码
找对应的 NamespaceHandler
,关键方法在于 getHandlerMappings()
:
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
// 若是没有缓存,进行缓存加载,公共变量,加锁进行操做,细节好评👍
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
}
}
return handlerMappings;
}
复制代码
因此咱们能看到,找 Handler
时,使用的策略是延迟加载,在 map
缓存中找到了直接返回,没找到对应的 Handler
,将处理器实例化,执行 init()
方法,接着将 Handler
放入 map
缓存中,等待下一个使用。
回忆一下,咱们在自定义标签解析的时候,是没有重载 parse()
方法,因此定位进去,看到实际调用方法是这两行:
org.springframework.beans.factory.xml.NamespaceHandlerSupport#parse
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找解析器并进行解析操做
BeanDefinitionParser parser = findParserForElement(element, parserContext);
// 真正解析调用调用的方法
return (parser != null ? parser.parse(element, parserContext) : null);
}
复制代码
第一步获取解析器,就是咱们以前在 init()
方法中,注册到 Spring
容器的解析器。
第二步才是解析器进行解析的方法,咱们的解析器扩展的是 AbstractSingleBeanDefinitionParser
,因此实际是调用了咱们解析器父类的父类 AbstractBeanDefinitionParser
的 parse
方法:
org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#parse
public final BeanDefinition parse(Element element, ParserContext parserContext) {
// 注释 3.10 实际自定义标签解析器调用的方法,在 parseInternal 方法中,调用了咱们重载的方法
AbstractBeanDefinition definition = parseInternal(element, parserContext);
...
return definition;
}
复制代码
解析关键方法
org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#parseInternal
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
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);
}
// 注释 3.11 在这里调用了咱们写的解析方法
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
复制代码
这里我要倒着讲,在第二步解析时,不是直接调用了自定义的 doParse
方法,而是进行了一系列的数据准备,包括了 beanClass
、 class
、 lazyInit
等属性的准备。
第一步解析,在我省略的代码中,是将第二步解析后的结果进行包装,从 AbstractBeanDefinition
转换成 BeanDefinitionHolder
,而后进行注册。转换和注册流程在第一篇笔记已经介绍过了,再也不赘述。
到这里为止,咱们自定义标签的解析就完成了~
在咱们自定义标签时,是否是感受使用起来很简单,只需定义几个文件,而后在自定义解析器中写上业务处理逻辑,而后就能使用。
在咱们分析完整个解析流程,就能看到,Spring
在背后默默帮咱们完成了不少事情,相似默认标签解析过程,根据命名空间找到对应的处理器,而后再找到解析器,在解析器里面调用咱们个性化的处理逻辑。
这两篇文章填了默认标签和自定义标签解析的坑,也完整的介绍了 Spring
将 bean
从配置中加载到内存中的全过程,下一篇开始分析解析类的加载~
因为我的技术有限,若是有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正
spring-analysis-note 码云 Gitee 地址
spring-analysis-note Github 地址
Spring 源码深度解析 / 郝佳编著. -- 北京 : 人民邮电出版社