跳至主要內容

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

Java突击队大约 21 分钟

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

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

Spring 版本:5.1.14.RELEASE

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

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

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

上一篇**《BeanDefinition 的解析阶段(XML 文件)》**文章分析了 Spring 处理 org.w3c.dom.Document 对象(XML Document)的过程,会解析里面的元素。默认命名空间(为空或者 http://www.springframework.org/schema/beans)的元素,例如open in new window <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、 编写XMLSchema文件(XSD文件):定义XML结构;
2、 自定义NamespaceHandler实现:定义命名空间的处理器,实现NamespaceHandler接口,我们通常继承NamespaceHandlerSupport抽象类,Spring提供了通用实现,只需要实现其init()方法即可;
3、 自定义BeanDefinitionParser实现:绑定命名空间下不同的XML元素与其对应的解析器,因为一个命名空间下可以有很多个标签,对于不同的标签需要不同的BeanDefinitionParser解析器,在上面的init()方法中进行绑定;
4、 注册XML扩展(META-INF/spring.handlers文件):命名空间与命名空间处理器的映射;
5、 编写SpringSchema资源映射文件(META-INF/spring.schemas文件):XMLSchema文件通常定义为网络的形式,在无网的情况下无法访问,所以一般在本地的也有一个XSD文件,可通过编写spring.schemas文件,将网络形式的XSD文件与本地的XSD文件进行映射,这样会优先从本地获取对应的XSD文件;

Spring 内部自定义标签预览

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

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

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 文件)》open in new window 文章中的 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 文件)》open in new window 文章中的 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(ParserContextparserContext,Elementelement)方法;
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 文件)》open in new window 中的 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 /> 标签就是这样实现的,可以参考:NamespaceHandleropen in new windowMapperScannerBeanDefinitionParseropen in new windowXSD 等文件open in new window

总结

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

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

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

1、 编写XMLSchema文件(XSD文件):定义XML结构;
2、 自定义NamespaceHandler实现:定义命名空间的处理器,实现NamespaceHandler接口,我们通常继承NamespaceHandlerSupport抽象类,Spring提供了通用实现,只需要实现其init()方法即可;
3、 自定义BeanDefinitionParser实现:绑定命名空间下不同的XML元素与其对应的解析器,因为一个命名空间下可以有很多个标签,对于不同的标签需要不同的BeanDefinitionParser解析器,在上面的init()方法中进行绑定;
4、 注册XML扩展(META-INF/spring.handlers文件):命名空间与命名空间处理器的映射;
5、 XMLSchema文件通常定义为网络的形式,在无网的情况下无法访问,所以一般在本地的也有一个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 的解析过程:

 

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址:open in new window