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

該系列文章是本人在學習 Spring 的過程當中總結下來的,裏面涉及到相關源碼,可能對讀者不太友好,請結合個人源碼註釋 Spring 源碼分析 GitHub 地址 進行閱讀html

Spring 版本:5.1.14.RELEASEjava

開始閱讀這一系列文章以前,建議先查看《深刻了解 Spring IoC(面試題)》這一篇文章git

該系列其餘文章請查看:《死磕 Spring 之 IoC 篇 - 文章導讀》github

解析自定義標籤(XML 文件)

上一篇《BeanDefinition 的解析階段(XML 文件)》文章分析了 Spring 處理 org.w3c.dom.Document 對象(XML Document)的過程,會解析裏面的元素。默認命名空間(爲空或者 http://www.springframework.org/schema/beans)的元素,例如 <bean /> 標籤會被解析成 GenericBeanDefinition 對象並註冊。本文會分析 Spring 是如何處理非默認命名空間的元素,經過 Spring 的實現方式咱們如何自定義元素面試

先來了解一下 XML 文件中的命名空間:spring

<?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 元素的步驟以下:mybatis

  1. 編寫 XML Schema 文件(XSD 文件):定義 XML 結構app

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

相關文章
相關標籤/搜索