死磕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 的解析過程:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM