逐行解读Spring(二) - 自定义标签解析与component-scan原理

本文深入解析Spring框架中的自定义标签,包括自定义标签的定义与内置标签的作用,详细介绍了源码解析过程,从自定义标签解析、标签工作原理到过滤器匹配流程,以及实践中的应用。通过实例展示了如何使用自定义注解和自定义标签,总结了自定义标签的解析过程和`<component-scan>`标签的主要功能。
摘要由CSDN通过智能技术生成


上一篇我们讲了默认标签- bean标签的解析,今天我们讲一下自定义标签的解析。

一、自定义标签是什么?

1. 自定义标签的定义

这个问题其实上一篇有讲过,这边再复述一遍,在springxml配置文件中,我们可以把所有的标签分为两类:自定义标签和默认标签,区别如下

<!-- 标签前面有 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的声明而不是使用xmlbean 标签了。

那么为什么一个类加上了这些注解之后,就能被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就是自定义标签的namespaceUrivalue则是对应的NamespaceHandler的全限定名。

那么简单总结一下,我们的自定义标签解析的流程就是:

  1. 加载所有jar中META-INF/spring.handlers文件中的namespaceUriNamespaceHandler的全限定名的映射关系到handlerMappings

  2. 根据namespaceUrihandlerMappings获取对象

    • 如果从handlerMappings获取到的对象为空,直接返回

    • 如果获取到的是NamespaceHandler对象,直接使用

    • 如果获取到的对象是string类型,则实例化这个string对应的全限定名的NamespaceHandler对象,并调用init()方法,然后将 namespaceUri-NamespaceHandler对象关系放回handlerMappings

  3. 将自定义标签委托给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的注入代码...
        // 我们可以看到
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值