文章目录
上一篇我们讲了默认标签-
bean
标签的解析,今天我们讲一下自定义标签的解析。
一、自定义标签是什么?
1. 自定义标签的定义
这个问题其实上一篇有讲过,这边再复述一遍,在spring
的xml
配置文件中,我们可以把所有的标签分为两类:自定义标签和默认标签,区别如下
<!-- 标签前面有 xxx:即是spring的自定义标签,我们也可以自己定义一个xiaozize:的标签-之后会讲到 -->
<context:component-scan base-package="com.xiaoxizi.spring"/>
<!-- 该标签对应的命名空间在xml文件头部beans标签中声明 -->
<beans xmlns:context="http://www.springframework.org/schema/context" ... />
<!-- 默认标签没有 xx: 前缀 -->
<bean class="com.xiaoxizi.spring.service.AccountServiceImpl"
id="accountService" scope="singleton" primary="true"/>
<!-- 对应的命名空间也在xml文件头部beans标签中声明 -->
<beans xmlns="http://www.springframework.org/schema/beans" ... />
需要注意的是,自定义标签的概念,并不完全只指我们开发时自己定义的标签,而是spring的开发者为之后拓展预留的拓展点,这个拓展点我们可以用,spring的开发人员在为spring添加新功能时,也可以使用。
2. 关于spring
内置的自定义标签context:component-scan
我们现在的开发中,更多的情况下,其实是使用@Configuration
、@Component
、@Service
等注解来进行bean
的声明而不是使用xml
的bean
标签了。
那么为什么一个类加上了这些注解之后,就能被spring管理了呢?
实际上这些拓展功能是spring
通过自己预留的自定义标签的拓展点进行拓展的,对于上述的功能,具体是使用的context:component-scan
标签。
我们今天就通过对自定义标签context:component-scan
的解析来跟踪一下相应的源码,理解spring
自定义标签解析的流程,同时也对context:component-scan
实现的功能做一个讲解,看一下@Component
等标签的实现原理。
二、源码解析
1. 自定义标签解析过程
由于上一篇对xml的源码跟过了,这期我们之间定位到相应代码org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#parseBeanDefinitions
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)) {
// 解析默认标签
parseDefaultElement(ele, delegate);
}
else {
// 可以看到代理主要进行自定义标签的解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
// 可以看到代理主要进行自定义标签的解析
delegate.parseCustomElement(root);
}
}
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取标签对于的namespaceUrl, 即配置文件头部beans标签里面那些xmlns:xxx=www.xxx.com
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 获取自定义标签对应的NamespaceHandler,从这里我们可以看到,对于每一个namespaceUri应该都有唯一一个对应的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;
}
// 把自定义标签委托给对应的NamespaceHandler解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
我们先看一下NamespaceHandler
这个自定义标签的解析接口的结构:
public interface NamespaceHandler {
// 初始化,我们可以合理猜测,这个方法将会在NamespaceHandler实例化之后,使用之前调用
void init();
// xml解析入口
@Nullable
BeanDefinition parse(Element element, ParserContext parserContext);
// 装饰接口,其实用的比较少,上一篇有稍微带到过一下,默认bean标签解析完之后,可以有一个机会对解析出来的beanDefinition进行装饰,实际开发中很少使用
// 有兴趣的同学可以自行看下源码,源码在 org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#processBeanDefinition
@Nullable
BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext);
}
接下来当然是需要看一下获取NamespaceHandler
的流程:
public NamespaceHandler resolve(String namespaceUri) {
// 获取到了一个handlerMapping,具体逻辑我们之后再看
Map<String, Object> handlerMappings = getHandlerMappings();
// 通过namespaceUri获取到一个对象
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
// 如果handlerOrClassName是一个NamespaceHandler对象,则直接返回 - 拿到对应的handler了
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
// 如果handlerOrClassName不是NamespaceHandler对象,则是String对象
String className = (String) handlerOrClassName;
// 通过String获取到一个Class对象,那么这个String对象肯定是一个类的全限定名啦
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
// handlerClass必须继承自NamespaceHandler,很好理解,毕竟是spring提供的拓展点,自然需要符合它定义的规则
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("...");
}
// 直接通过反射构造一个实例,点进去看会发现是调用的无参构造器,我们就不看了
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// !!! 调用了init()方法,和我们之前的推测一致
namespaceHandler.init();
// !!! 把handler对象塞回了handlerMappings,所以我们下次再通过namespaceUri获取时,会直接拿到一个NamespaceHandler对象
// 也即每个namespaceUri对应的NamespaceHandler对象是单例的,而init()方法也只会调用一次
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
// 去除掉了异常处理
}
}
由上述源码其实我们已经得知了NamespaceHandler
的一个初始化过程,但其实还有一个疑问,就是这个handlerMappings
中最初的那些namespaceUri
对应的handler的类名是哪来的呢?这个时候我们就需要去看一下getHandlerMappings()
的过程啦
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
// 双重检查加锁,看来我们的handlerMappings之后加载一次
if (handlerMappings == null) {
// 可以看到这边是去加载了文件
// 文件加载的过程我们就不去跟了,跟主流程关系不大,我们主要看一下这个文件位置
// this.handlerMappingsLocation是哪里
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
handlerMappings = new ConcurrentHashMap<>(mappings.size());
// 然后把文件中的kev-value属性都合并到了一个map里
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
// 干掉了异常处理代码
}
}
}
return handlerMappings;
}
// 字段的定义, 需要说一下当前类是DefaultNamespaceHandlerResolver,喜欢自己探索的同学可以直接空降
/** Resource location to search for. */
private final String handlerMappingsLocation;
// 可以看到这个值是Resolver的构造器中设值的
public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String handlerMappingsLocation) {
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
this.handlerMappingsLocation = handlerMappingsLocation;
}
// 默认是取的DEFAULT_HANDLER_MAPPINGS_LOCATION这个常量
public DefaultNamespaceHandlerResolver() {
this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION);
}
// 我们看一下这个常量的值
public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";
如果对SPI
比较熟悉的同学,应该已经知道这是个什么套路了,并且对META-INF
这个目录也比较熟悉,那么现在,我们看一下这个META-INF/spring.handlers
文件中到底写了一些什么东西,以context:component-scan
标签为例,我们知道这个标签是spring-context包里面提供的,直接去找这个jar包的对应文件,看一下里面的内容:
## 我们可以很明显的看到一个key=value结构
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
我们在回忆一下自定义标签的定义:
<!-- 标签前面有 xxx:即是spring的自定义标签,我们也可以自己定义一个xiaozize:的标签-之后会讲到 -->
<context:component-scan base-package="com.xiaoxizi.spring"/>
<!-- 该标签对应的命名空间在xml文件头部beans标签中声明 -->
<beans xmlns:context="http://www.springframework.org/schema/context" ... />
可以看到我们的META-INF/spring.handlers
文件中key
就是自定义标签的namespaceUri
,value
则是对应的NamespaceHandler
的全限定名。
那么简单总结一下,我们的自定义标签解析的流程就是:
-
加载所有jar中
META-INF/spring.handlers
文件中的namespaceUri
和NamespaceHandler
的全限定名的映射关系到handlerMappings
-
根据
namespaceUri
从handlerMappings
获取对象-
如果从
handlerMappings
获取到的对象为空,直接返回 -
如果获取到的是
NamespaceHandler
对象,直接使用 -
如果获取到的对象是string类型,则实例化这个string对应的全限定名的
NamespaceHandler
对象,并调用init()
方法,然后将namespaceUri
-NamespaceHandler
对象关系放回handlerMappings
-
-
将自定义标签委托给2获取到的
NamespaceHandler
对象解析-调用parse
方法(如果2未获取到对应的NamespaceHandler
对象,则此自定义标签无法解析,直接跳过)
2. context:component-scan
标签工作原理
接下来我们来看一下 context:component-scan
标签的工作原理,从spring-context包的META-INF/spring.handlers
文件我们可以找到该标签对应的处理器:
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
直接找到这个类:
public class ContextNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
// 删掉了一些我们不关注的标签的Parser的注入代码...
// 我们可以看到