死磕Spring之IoC篇 - 解析自定义标签(XML 文件)

该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读

Spring 版本:5.1.14.RELEASE

开始阅读这一系列文章之前,建议先查看《深入了解 Spring IoC(面试题)》这一篇文章

该系列其他文章请查看:《死磕 Spring 之 IoC 篇 - 文章导读》

解析自定义标签(XML 文件)

上一篇《BeanDefinition 的解析阶段(XML 文件)》文章分析了 Spring 处理 org.w3c.dom.Document 对象(XML Document)的过程,会解析里面的元素。默认命名空间(为空或者 http://www.springframework.org/schema/beans)的元素,例如 <bean /> 标签会被解析成 GenericBeanDefinition 对象并注册。本文会分析 Spring 是如何处理非默认命名空间的元素,通过 Spring 的实现方式我们如何自定义元素

先来了解一下 XML 文件中的命名空间:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
	<context:component-scan base-package="org.geekbang.thinking.in.spring.ioc.overview" />

    <bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User">
        <property name="id" value="1"/>
        <property name="name" value="小马哥"/>
    </bean>
</beans>

上述 XML 文件 <beans /> 的默认命名空间为 http://www.springframework.org/schema/beans,内部的 <bean /> 标签没有定义命名空间,则使用默认命名空间

<beans /> 还定义了 context 命名空间为 http://www.springframework.org/schema/context,那么内部的 <context:component-scan /> 标签就不是默认命名空间,处理方式也不同。其实 Spring 内部自定义了很多的命名空间,用于处理不同的场景,原理都一样,接下来会进行分析。

自定义标签的实现步骤

扩展 Spring XML 元素的步骤如下:

  1. 编写 XML Schema 文件(XSD 文件):定义 XML 结构

  2. 自定义 NamespaceHandler 实现:定义命名空间的处理器,实现 NamespaceHandler 接口,我们通常继承 NamespaceHandlerSupport 抽象类,Spring 提供了通用实现,只需要实现其 init() 方法即可

  3. 自定义 BeanDefinitionParser 实现:绑定命名空间下不同的 XML 元素与其对应的解析器,因为一个命名空间下可以有很多个标签,对于不同的标签需要不同的 BeanDefinitionParser 解析器,在上面的 init() 方法中进行绑定

  4. 注册 XML 扩展(META-INF/spring.handlers 文件):命名空间与命名空间处理器的映射

  5. 编写 Spring Schema 资源映射文件(META-INF/spring.schemas 文件):XML Schema 文件通常定义为网络的形式,在无网的情况下无法访问,所以一般在本地的也有一个 XSD 文件,可通过编写 spring.schemas 文件,将网络形式的 XSD 文件与本地的 XSD 文件进行映射,这样会优先从本地获取对应的 XSD 文件

Spring 内部自定义标签预览

spring-context 模块的 ClassPath 下可以看到有 META-INF/spring.handlersMETA-INF/spring.schemas 以及对应的 XSD 文件,如下:

  • META-INF/spring.handlers

    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
    
  • META-INF/spring.schemas

    http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd
    http\://www.springframework.org/schema/jee/spring-jee.xsd=org/springframework/ejb/config/spring-jee.xsd
    http\://www.springframework.org/schema/lang/spring-lang.xsd=org/springframework/scripting/config/spring-lang.xsd
    http\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task.xsd
    http\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache.xsd
    https\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd
    https\://www.springframework.org/schema/jee/spring-jee.xsd=org/springframework/ejb/config/spring-jee.xsd
    https\://www.springframework.org/schema/lang/spring-lang.xsd=org/springframework/scripting/config/spring-lang.xsd
    https\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task.xsd
    https\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache.xsd
    ### ... 省略
    

其他模块也有这两种文件,这里不一一展示,从上面的 spring.handlers 这里可以看到 context 命名空间对应的是 ContextNamespaceHandler 处理器,先来看一下:

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
	@Override
	public void init() {
		registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
		registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
		registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
		registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
		registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
		registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
	}
}

可以看到注册了不同的标签所对应的解析器,其中 component-scan 对应 ComponentScanBeanDefinitionParser 解析器,这里先看一下,后面再具体分析

Spring 如何处理非默认命名空间的元素

回顾到 《BeanDefinition 的加载阶段(XML 文件)》 文章中的 XmlBeanDefinitionReader#registerBeanDefinitions 方法,解析 Document 前会先创建 XmlReaderContext 对象(读取 Resource 资源的上下文对象),创建方法如下:

// XmlBeanDefinitionReader.java

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

public NamespaceHandlerResolver getNamespaceHandlerResolver() {
    if (this.namespaceHandlerResolver == null) {
        this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
    }
    return this.namespaceHandlerResolver;
}

protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
    ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
    return new DefaultNamespaceHandlerResolver(cl);
}

在 XmlReaderContext 对象中会有一个 DefaultNamespaceHandlerResolver 对象

回顾到 《BeanDefinition 的解析阶段(XML 文件)》 文章中的 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法,如果不是默认的命名空间,则执行自定义解析,调用 BeanDefinitionParserDelegate#parseCustomElement(Element ele) 方法,方法如下

// BeanDefinitionParserDelegate.java

@Nullable
public BeanDefinition parseCustomElement(Element ele) {
    return parseCustomElement(ele, null);
}

@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // <1> 获取 `namespaceUri`
    String namespaceUri = getNamespaceURI(ele);
    if (namespaceUri == null) {
        return null;
    }
    // <2> 通过 DefaultNamespaceHandlerResolver 根据 `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;
    }
    // <3> 根据 NamespaceHandler 命名空间处理器处理该标签
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

过程如下:

  1. 获取该节点对应的 namespaceUri 命名空间
  2. 通过 DefaultNamespaceHandlerResolver 根据 namespaceUri 获取相应的 NamespaceHandler 处理器
  3. 根据 NamespaceHandler 命名空间处理器处理该标签

关键就在与 DefaultNamespaceHandlerResolver 是如何找到该命名空间对应的 NamespaceHandler 处理器,我们只是在 spring.handlers 文件中进行关联,它是怎么找到的呢,我们进入 DefaultNamespaceHandlerResolver 看看

DefaultNamespaceHandlerResolver

org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver,命名空间的默认处理器

构造函数

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {

	/**
	 * The location to look for the mapping files. Can be present in multiple JAR files.
	 */
	public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";

	/** Logger available to subclasses. */
	protected final Log logger = LogFactory.getLog(getClass());

	/** ClassLoader to use for NamespaceHandler classes. */
	@Nullable
	private final ClassLoader classLoader;

	/** Resource location to search for. */
	private final String handlerMappingsLocation;

	/** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */
	@Nullable
	private volatile Map<String, Object> handlerMappings;

	public DefaultNamespaceHandlerResolver() {
		this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION);
	}

	public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader) {
		this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION);
	}

	public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String handlerMappingsLocation) {
		Assert.notNull(handlerMappingsLocation, "Handler mappings location must not be null");
		this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
		this.handlerMappingsLocation = handlerMappingsLocation;
	}
}

注意有一个 DEFAULT_HANDLER_MAPPINGS_LOCATION 属性为 META-INF/spring.handlers,我们定义的 spring.handlers 在这里出现了,说明命名空间和对应的处理器在这里大概率会有体现

还有一个 handlerMappingsLocation 属性默认为 META-INF/spring.handlers

resolve 方法

resolve(String namespaceUri) 方法,根据命名空间找到对应的 NamespaceHandler 处理器,方法如下:

@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
    // <1> 获取所有已经配置的命名空间与 NamespaceHandler 处理器的映射
    Map<String, Object> handlerMappings = getHandlerMappings();
    // <2> 根据 `namespaceUri` 命名空间获取 NamespaceHandler 处理器
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    // <3> 接下来对 NamespaceHandler 进行初始化,因为定义在 `spring.handler` 文件中,可能还没有转换成 Class 类对象
    // <3.1> 不存在
    if (handlerOrClassName == null) {
        return null;
    }
    // <3.2> 已经初始化
    else if (handlerOrClassName instanceof NamespaceHandler) {
        return (NamespaceHandler) handlerOrClassName;
    }
    // <3.3> 需要进行初始化
    else {
        String className = (String) handlerOrClassName;
        try {
            // 获得类,并创建 NamespaceHandler 对象
            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);
            // 初始化 NamespaceHandler 对象
            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);
        }
    }
}

过程如下:

  1. 获取所有已经配置的命名空间与 NamespaceHandler 处理器的映射,调用 getHandlerMappings() 方法
  2. 根据 namespaceUri 命名空间获取 NamespaceHandler 处理器
  3. 接下来对 NamespaceHandler 进行初始化,因为定义在 spring.handler 文件中,可能还没有转换成 Class 类对象
    1. 不存在则返回空对象
    2. 否则,已经初始化则直接返回
    3. 否则,根据 className 创建一个 Class 对象,然后进行实例化,还调用其 init() 方法

该方法可以找到命名空间对应的 NamespaceHandler 处理器,关键在于第 1 步如何将 spring.handlers 文件中的内容返回的

getHandlerMappings 方法

getHandlerMappings() 方法,从所有的 META-INF/spring.handlers 文件中获取命名空间与处理器之间的映射,方法如下:

private Map<String, Object> getHandlerMappings() {
    // 双重检查锁,延迟加载
    Map<String, Object> handlerMappings = this.handlerMappings;
    if (handlerMappings == null) {
        synchronized (this) {
            handlerMappings = this.handlerMappings;
            if (handlerMappings == null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
                }
                try {
                    // 读取 `handlerMappingsLocation`,也就是当前 JVM 环境下所有的 `META-INF/spring.handlers` 文件的内容都会读取到
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Loaded NamespaceHandler mappings: " + mappings);
                    }
                    // 初始化到 `handlerMappings` 中
                    handlerMappings = new ConcurrentHashMap<>(mappings.size());
                    CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                    this.handlerMappings = handlerMappings;
                }
                catch (IOException ex) {
                    throw new IllegalStateException(
                            "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
                }
            }
        }
    }
    return handlerMappings;
}

逻辑不复杂,会读取当前 JVM 环境下所有的 META-INF/spring.handlers 文件,将里面的内容以 key-value 的形式保存在 Map 中返回

到这里,对于 Spring XML 文件中的自定义标签的处理逻辑你是不是清晰了,接下来我们来看看 <context:component-scan /> 标签的具体实现

ContextNamespaceHandler

org.springframework.context.config.ContextNamespaceHandler,继承 NamespaceHandlerSupport 抽象类,context 命名空间(http://www.springframework.org/schema/context)的处理器,代码如下:

public class ContextNamespaceHandler extends NamespaceHandlerSupport {

	@Override
	public void init() {
		registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
		registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
		registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
		registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
		registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
		registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
	}
}

init() 方法在 DefaultNamespaceHandlerResolver#resolve 方法中可以看到,初始化该对象的时候会被调用,注册该命名空间下各种标签的解析器

registerBeanDefinitionParser 方法

registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser),注册标签的解析器,方法如下:

// NamespaceHandlerSupport.java

private final Map<String, BeanDefinitionParser> parsers = new HashMap<>();

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

将标签名称和对应的解析器保存在 Map 中

parse 方法

parse(Element element, ParserContext parserContext) 方法,解析标签节点,方法如下:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // <1> 获得元素对应的 BeanDefinitionParser 对象
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    // <2> 执行解析
    return (parser != null ? parser.parse(element, parserContext) : null);
}

@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 获得元素名
    String localName = parserContext.getDelegate().getLocalName(element);
    // 获得 BeanDefinitionParser 对象
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
                "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

逻辑很简单,从 Map<String, BeanDefinitionParser> parsers 找到标签对象的 BeanDefinitionParser 解析器,然后进行解析

ComponentScanBeanDefinitionParser

org.springframework.context.annotation.ComponentScanBeanDefinitionParser,实现了 BeanDefinitionParser 接口,<context:component-scan /> 标签的解析器

parse 方法

parse(Element element, ParserContext parserContext) 方法,<context:component-scan /> 标签的解析过程,方法如下:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // <1> 获取 `base-package` 属性
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    // 处理占位符
    basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
    // 根据分隔符进行分割
    String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
            ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

    // Actually scan for bean definitions and register them.
    // <2> 创建 ClassPathBeanDefinitionScanner 扫描器,用于扫描指定路径下符合条件的 BeanDefinition 们
    ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
    // <3> 通过扫描器扫描 `basePackages` 指定包路径下的 BeanDefinition(带有 @Component 注解或其派生注解的 Class 类),并注册
    Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
    // <4> 将已注册的 `beanDefinitions` 在当前 XMLReaderContext 上下文标记为已注册,避免重复注册
    registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

    return null;
}

过程如下:

  1. 获取 base-package 属性,处理占位符,根据分隔符进行分割
  2. 创建 ClassPathBeanDefinitionScanner 扫描器,用于扫描指定路径下符合条件的 BeanDefinition 们,调用 configureScanner(ParserContext parserContext, Element element) 方法
  3. 通过扫描器扫描 basePackages 指定包路径下的 BeanDefinition(带有 @Component 注解或其派生注解的 Class 类),并注册
  4. 将已注册的 beanDefinitions 在当前 XMLReaderContext 上下文标记为已注册,避免重复注册

上面的第 3 步的解析过程和本文的主题有点不符,过程也比较复杂,下一篇文章再进行分析

configureScanner 方法

configureScanner(ParserContext parserContext, Element element) 方法,创建 ClassPathBeanDefinitionScanner 扫描器,方法如下:

protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
    // <1> 默认使用过滤器(过滤出 @Component 注解或其派生注解的 Class 类)
    boolean useDefaultFilters = true;
    if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
        useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
    }

    // Delegate bean definition registration to scanner class.
    // <2> 创建 ClassPathBeanDefinitionScanner 扫描器 `scanner`,用于扫描指定路径下符合条件的 BeanDefinition 们
    ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
    // <3> 设置生成的 BeanDefinition 对象的相关默认属性
    scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults());
    scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns());

    // <4> 根据标签的属性进行相关配置

    // <4.1> `resource-pattern` 属性的处理,设置资源文件表达式,默认为 `**/*.class`,即 `classpath*:包路径/**/*.class`
    if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) {
        scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE));
    }

    try {
        // <4.2> `name-generator` 属性的处理,设置 Bean 的名称生成器,默认为 AnnotationBeanNameGenerator
        parseBeanNameGenerator(element, scanner);
    }
    catch (Exception ex) {
        parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
    }

    try {
        // <4.3> `scope-resolver`、`scoped-proxy` 属性的处理,设置 Scope 的模式和元信息处理器
        parseScope(element, scanner);
    }
    catch (Exception ex) {
        parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
    }

    // <4.4> `exclude-filter`、`include-filter` 属性的处理,设置 `.class` 文件的过滤器
    parseTypeFilters(element, scanner, parserContext);

    // <5> 返回 `scanner` 扫描器
    return scanner;
}

过程如下:

  1. 默认使用过滤器(过滤出 @Component 注解或其派生注解的 Class 类)
  2. 创建 ClassPathBeanDefinitionScanner 扫描器 scanner,用于扫描指定路径下符合条件的 BeanDefinition 们
  3. 设置生成的 BeanDefinition 对象的相关默认属性
  4. 根据标签的属性进行相关配置
    1. resource-pattern 属性的处理,设置资源文件表达式,默认为 **/*.class,即 classpath*:包路径/**/*.class
    2. name-generator 属性的处理,设置 Bean 的名称生成器,默认为 AnnotationBeanNameGenerator
    3. scope-resolverscoped-proxy 属性的处理,设置 Scope 的模式和元信息处理器
    4. exclude-filterinclude-filter 属性的处理,设置 .class 文件的过滤器
  5. 返回 scanner 扫描器

至此,对于 <context:component-scan /> 标签的解析过程已经分析完

spring.schemas 的原理

META-INF/spring.handlers 文件的原理在 DefaultNamespaceHandlerResolver 中已经分析过,那么 Sping 是如何处理 META-INF/spring.schemas 文件的?

先回到 《BeanDefinition 的加载阶段(XML 文件)》 中的 XmlBeanDefinitionReader#doLoadDocument 方法,如下:

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    // <3> 通过 DefaultDocumentLoader 根据 Resource 获取一个 Document 对象
    return this.documentLoader.loadDocument(inputSource,
            getEntityResolver(), // <1> 获取 `org.xml.sax.EntityResolver` 实体解析器,ResourceEntityResolver
            this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware()); // <2> 获取 XML 文件验证模式,保证 XML 文件的正确性
}

protected EntityResolver getEntityResolver() {
    if (this.entityResolver == null) {
        // Determine default EntityResolver to use.
        ResourceLoader resourceLoader = getResourceLoader();
        if (resourceLoader != null) {
            this.entityResolver = new ResourceEntityResolver(resourceLoader);
        }
        else {
            this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
        }
    }
    return this.entityResolver;
}

1 步先获取 org.xml.sax.EntityResolver 实体解析器,默认为 ResourceEntityResolver 资源解析器,根据 publicId 和 systemId 获取对应的 DTD 或 XSD 文件,用于对 XML 文件进行验证

ResourceEntityResolver

org.springframework.beans.factory.xml.ResourceEntityResolver,XML 资源实例解析器,获取对应的 DTD 或 XSD 文件

构造函数
public class ResourceEntityResolver extends DelegatingEntityResolver {
    /** 资源加载器 */
	private final ResourceLoader resourceLoader;

	public ResourceEntityResolver(ResourceLoader resourceLoader) {
		super(resourceLoader.getClassLoader());
		this.resourceLoader = resourceLoader;
	}
}

public class DelegatingEntityResolver implements EntityResolver {
	/** Suffix for DTD files. */
	public static final String DTD_SUFFIX = ".dtd";

	/** Suffix for schema definition files. */
	public static final String XSD_SUFFIX = ".xsd";

	private final EntityResolver dtdResolver;

	private final EntityResolver schemaResolver;

	public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
		this.dtdResolver = new BeansDtdResolver();
		this.schemaResolver = new PluggableSchemaResolver(classLoader);
	}
}

注意 schemaResolver 为 XSD 的解析器,默认为 PluggableSchemaResolver 对象

resolveEntity 方法

resolveEntity(@Nullable String publicId, @Nullable String systemId) 方法,获取命名空间对应的 DTD 或 XSD 文件,方法如下:

// DelegatingEntityResolver.java
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
        throws SAXException, IOException {
    if (systemId != null) {
        // DTD 模式
        if (systemId.endsWith(DTD_SUFFIX)) {
            return this.dtdResolver.resolveEntity(publicId, systemId);
        }
        // XSD 模式
        else if (systemId.endsWith(XSD_SUFFIX)) {
            return this.schemaResolver.resolveEntity(publicId, systemId);
        }
    }
    // Fall back to the parser's default behavior.
    return null;
}

// ResourceEntityResolver.java
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
        throws SAXException, IOException {

    // <1> 调用父类的方法,进行解析,获取本地 XSD 文件资源
    InputSource source = super.resolveEntity(publicId, systemId);

    // <2> 如果没有获取到本地 XSD 文件资源,则尝试通直接通过 systemId 获取(网络形式)
    if (source == null && systemId != null) {
        // <2.1> 将 systemId 解析成一个 URL 地址
        String resourcePath = null;
        try {
            String decodedSystemId = URLDecoder.decode(systemId, "UTF-8");
            String givenUrl = new URL(decodedSystemId).toString();
            // 解析文件资源的相对路径(相对于系统根路径)
            String systemRootUrl = new File("").toURI().toURL().toString();
            // Try relative to resource base if currently in system root.
            if (givenUrl.startsWith(systemRootUrl)) {
                resourcePath = givenUrl.substring(systemRootUrl.length());
            }
        }
        catch (Exception ex) {
            // Typically a MalformedURLException or AccessControlException.
            if (logger.isDebugEnabled()) {
                logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex);
            }
            // No URL (or no resolvable URL) -> try relative to resource base.
            resourcePath = systemId;
        }
        // <2.2> 如果 URL 地址解析成功,则根据该地址获取对应的 Resource 文件资源
        if (resourcePath != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]");
            }
            // 获得 Resource 资源
            Resource resource = this.resourceLoader.getResource(resourcePath);
            // 创建 InputSource 对象
            source = new InputSource(resource.getInputStream());
            // 设置 publicId 和 systemId 属性
            source.setPublicId(publicId);
            source.setSystemId(systemId);
            if (logger.isDebugEnabled()) {
                logger.debug("Found XML entity [" + systemId + "]: " + resource);
            }
        }
        // <2.3> 否则,再次尝试直接根据 systemId(如果是 "http" 则会替换成 "https")获取 XSD 文件(网络形式)
        else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) {
            // External dtd/xsd lookup via https even for canonical http declaration
            String url = systemId;
            if (url.startsWith("http:")) {
                url = "https:" + url.substring(5);
            }
            try {
                source = new InputSource(new URL(url).openStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
            }
            catch (IOException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
                }
                // Fall back to the parser's default behavior.
                source = null;
            }
        }
    }
    return source;
}

过程如下:

  1. 调用父类的方法,进行解析,获取本地 XSD 文件资源,如果是 XSD 模式,则先通过 PluggableSchemaResolver 解析
  2. 如果没有获取到本地 XSD 文件资源,则尝试通直接通过 systemId 获取(网络形式)
    1. 将 systemId 解析成一个 URL 地址
    2. 如果 URL 地址解析成功,则根据该地址获取对应的 Resource 文件资源
    3. 否则,再次尝试直接根据 systemId(如果是 "http" 则会替换成 "https")获取 XSD 文件(网络形式)

先尝试获取本地的 XSD 文件,获取不到再获取远程的 XSD 文件

PluggableSchemaResolver

org.springframework.beans.factory.xml.PluggableSchemaResolver,获取 XSD 文件(网络形式)对应的本地的文件资源

构造函数
public class PluggableSchemaResolver implements EntityResolver {

	public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";

	private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);

	@Nullable
	private final ClassLoader classLoader;

	/** Schema 文件地址 */
	private final String schemaMappingsLocation;

	/** Stores the mapping of schema URL -> local schema path. */
	@Nullable
	private volatile Map<String, String> schemaMappings;

	public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
		this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
	}
}

注意这里的 DEFAULT_SCHEMA_MAPPINGS_LOCATIONMETA-INF/spring.schemas,看到这个可以确定实现原理就在这里了

schemaMappingsLocation 属性默认为 META-INF/spring.schemas

resolveEntity 方法

resolveEntity(@Nullable String publicId, @Nullable String systemId) 方法,获取命名空间对应的 DTD 或 XSD 文件(本地),方法如下:

@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
    if (logger.isTraceEnabled()) {
        logger.trace("Trying to resolve XML entity with public id [" + publicId +
                "] and system id [" + systemId + "]");
    }

    if (systemId != null) {
        // <1> 获得对应的 XSD 文件位置,从所有 `META-INF/spring.schemas` 文件中获取对应的本地 XSD 文件位置
        String resourceLocation = getSchemaMappings().get(systemId);
        if (resourceLocation == null && systemId.startsWith("https:")) {
            // Retrieve canonical http schema mapping even for https declaration
            resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
        }
        if (resourceLocation != null) { // 本地 XSD 文件位置
            // <2> 创建 ClassPathResource 对象
            Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
            try {
                // <3> 创建 InputSource 对象,设置 publicId、systemId 属性,返回
                InputSource source = new InputSource(resource.getInputStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
                if (logger.isTraceEnabled()) {
                    logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
                }
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
                }
            }
        }
    }

    // Fall back to the parser's default behavior.
    return null;
}

过程如下:

  1. 获得对应的 XSD 文件位置 resourceLocation,从所有 META-INF/spring.schemas 文件中获取对应的本地 XSD 文件位置,会先调用 getSchemaMappings() 解析出本地所有的 XSD 文件的位置信息
  2. 根据 resourceLocation 创建 ClassPathResource 对象
  3. 创建 InputSource 对象,设置 publicId、systemId 属性,返回
getSchemaMappings 方法

getSchemaMappings()方法, 解析当前 JVM 环境下所有的 META-INF/spring.handlers 文件的内容,方法如下:

private Map<String, String> getSchemaMappings() {
    Map<String, String> schemaMappings = this.schemaMappings;
    // 双重检查锁,实现 schemaMappings 单例
    if (schemaMappings == null) {
        synchronized (this) {
            schemaMappings = this.schemaMappings;
            if (schemaMappings == null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
                }
                try {
                    // 读取 `schemaMappingsLocation`,也就是当前 JVM 环境下所有的 `META-INF/spring.handlers` 文件的内容都会读取到
                    Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Loaded schema mappings: " + mappings);
                    }
                    // 将 mappings 初始化到 schemaMappings 中
                    schemaMappings = new ConcurrentHashMap<>(mappings.size());
                    CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
                    this.schemaMappings = schemaMappings;
                }
                catch (IOException ex) {
                    throw new IllegalStateException(
                            "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
                }
            }
        }
    }
    return schemaMappings;
}

逻辑不复杂,会读取当前 JVM 环境下所有的 META-INF/spring.schemas 文件,将里面的内容以 key-value 的形式保存在 Map 中返回,例如保存如下信息:

key=http://www.springframework.org/schema/context/spring-context.xsd
value=org/springframework/context/config/spring-context.xsd

这样一来,会先获取本地 org/springframework/context/config/spring-context.xsd 文件,不存在则尝试获取 http://www.springframework.org/schema/context/spring-context.xsd 文件,避免无网情况下无法获取 XSD 文件

自定义标签实现示例

例如我们有一个 User 实例类和一个 City 枚举:

package org.geekbang.thinking.in.spring.ioc.overview.domain;

import org.geekbang.thinking.in.spring.ioc.overview.enums.City;
public class User implements BeanNameAware {
    private Long id;
    private String name;
    private City city;
    // ... 省略 getter、setter 方法
}

package org.geekbang.thinking.in.spring.ioc.overview.enums;
public enum City {
    BEIJING,
    HANGZHOU,
    SHANGHAI
}

编写 XML Schema 文件(XSD 文件)

org\geekbang\thinking\in\spring\configuration\metadata\users.xsd

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://time.geekbang.org/schema/users"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://time.geekbang.org/schema/users">

    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>

    <!-- 定义 User 类型(复杂类型) -->
    <xsd:complexType name="User">
        <xsd:attribute name="id" type="xsd:long" use="required"/>
        <xsd:attribute name="name" type="xsd:string" use="required"/>
        <xsd:attribute name="city" type="City"/>
    </xsd:complexType>

    <!-- 定义 City 类型(简单类型,枚举) -->
    <xsd:simpleType name="City">
        <xsd:restriction base="xsd:string">
            <xsd:enumeration value="BEIJING"/>
            <xsd:enumeration value="HANGZHOU"/>
            <xsd:enumeration value="SHANGHAI"/>
        </xsd:restriction>
    </xsd:simpleType>

    <!-- 定义 user 元素 -->
    <xsd:element name="user" type="User"/>
</xsd:schema>

自定义 NamespaceHandler 实现

package org.geekbang.thinking.in.spring.configuration.metadata;

import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class UsersNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // 将 "user" 元素注册对应的 BeanDefinitionParser 实现
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

自定义 BeanDefinitionParser 实现

package org.geekbang.thinking.in.spring.configuration.metadata;

import org.geekbang.thinking.in.spring.ioc.overview.domain.User;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    @Override
    protected Class<?> getBeanClass(Element element) {
        return User.class;
    }

    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        setPropertyValue("id", element, builder);
        setPropertyValue("name", element, builder);
        setPropertyValue("city", element, builder);
    }

    private void setPropertyValue(String attributeName, Element element, BeanDefinitionBuilder builder) {
        String attributeValue = element.getAttribute(attributeName);
        if (StringUtils.hasText(attributeValue)) {
            builder.addPropertyValue(attributeName, attributeValue); // -> <property name="" value=""/>

        }
    }
}

注册 XML 扩展(spring.handlers 文件)

META-INF/spring.handlers

## 定义 namespace 与 NamespaceHandler 的映射
http\://time.geekbang.org/schema/users=org.geekbang.thinking.in.spring.configuration.metadata.UsersNamespaceHandler

编写 Spring Schema 资源映射文件(spring.schemas 文件)

META-INF/spring.schemas

http\://time.geekbang.org/schema/users.xsd = org/geekbang/thinking/in/spring/configuration/metadata/users.xsd

使用示例

<?xml version="1.0" encoding="UTF-8"?>
<beans
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:users="http://time.geekbang.org/schema/users"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://time.geekbang.org/schema/users
        http://time.geekbang.org/schema/users.xsd">

    <!-- <bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User">
           <property name="id" value="1"/>
           <property name="name" value="小马哥"/>
           <property name="city" value="HANGZHOU"/>
       </bean>  -->

    <users:user id="1" name="小马哥" city="HANGZHOU"/>

</beans>

至此,通过使用 users 命名空间下的 user 标签也能定义一个 Bean

Mybatis 对 Spring 的集成项目中的 <mybatis:scan /> 标签就是这样实现的,可以参考:NamespaceHandlerMapperScannerBeanDefinitionParserXSD 等文件

总结

Spring 默认命名空间为 http://www.springframework.org/schema/beans,也就是 <bean /> 标签,解析过程在上一篇《BeanDefinition 的解析阶段(XML 文件)》文章中已经分析过了。

非默认命名空间的处理方式需要单独的 NamespaceHandler 命名空间处理器进行处理,这中方式属于扩展 Spring XML 元素,也可以说是自定义标签。在 Spring 内部很多地方都使用到这种方式。例如 <context:component-scan /><util:list />、AOP 相关标签都有对应的 NamespaceHandler 命名空间处理器

对于这种自定义 Spring XML 元素的实现步骤如下:

  1. 编写 XML Schema 文件(XSD 文件):定义 XML 结构

  2. 自定义 NamespaceHandler 实现:定义命名空间的处理器,实现 NamespaceHandler 接口,我们通常继承 NamespaceHandlerSupport 抽象类,Spring 提供了通用实现,只需要实现其 init() 方法即可

  3. 自定义 BeanDefinitionParser 实现:绑定命名空间下不同的 XML 元素与其对应的解析器,因为一个命名空间下可以有很多个标签,对于不同的标签需要不同的 BeanDefinitionParser 解析器,在上面的 init() 方法中进行绑定

  4. 注册 XML 扩展(META-INF/spring.handlers 文件):命名空间与命名空间处理器的映射

  5. XML Schema 文件通常定义为网络的形式,在无网的情况下无法访问,所以一般在本地的也有一个 XSD 文件,可通过编写 META-INF/spring.schemas 文件,将网络形式的 XSD 文件与本地的 XSD 文件进行映射,这样会优先从本地获取对应的 XSD 文件

关于上面的实现步骤的原理本文进行了比较详细的分析,稍微总结一下:

  1. Spring 会扫描到所有的 META-INF/spring.schemas 文件内容,每个命名空间对应的 XSD 文件优先从本地获取,用于 XML 文件的校验
  2. Spring 会扫描到所有的 META-INF/spring.handlers 文件内容,可以找到命名空间对应的 NamespaceHandler 处理器
  3. 根据找到的 NamespaceHandler 处理器找到标签对应的 BeanDefinitionParser 解析器
  4. 根据 BeanDefinitionParser 解析器解析该元素,生成对应的 BeanDefinition 并注册

本文还分析了 <context:component-scan /> 的实现原理,底层会 ClassPathBeanDefinitionScanner 扫描器,用于扫描指定路径下符合条件的 BeanDefinition 们(带有 @Component 注解或其派生注解的 Class 类)。@ComponentScan 注解底层原理也是基于 ClassPathBeanDefinitionScanner 扫描器实现的,这个扫描器和解析 @Component 注解定义的 Bean 相关。有关于面向注解定义的 Bean 在 Spring 中是如何解析成 BeanDefinition 在后续文章进行分析。

最后用一张图来结束面向资源(XML)定义 Bean 的 BeanDefinition 的解析过程:

posted @ 2021-02-25 21:49  月圆吖  阅读(1550)  评论(0编辑  收藏  举报