死磕Spring之IoC篇 - 解析自定義標籤(XML 檔案)

月圓吖發表於2021-02-25

該系列文章是本人在學習 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 的解析過程:

死磕Spring之IoC篇 - 解析自定義標籤(XML 檔案)

相關文章