Spring 源碼閱讀(三):自定義標籤解析原理

在很多情況下,我們需要爲系統提供可配置化支持。因此 spring 提供了可擴展 schema 的支持,擴展spring 自定義標籤配置大致需要以下幾個步驟:

  • 創建一個需要擴展的組件
  • 定義一個 XSD 文件描述組件內容
  • 創建一個文件,實現 BeanDefinitionParser 接口,用於解析 XSD 文件中的定義和組件定義
  • 創建一個 Handler 文件,擴展自 NamespaceHandlerSupport,目的是將組件註冊到 Spring 容器
  • 編寫 spring.handlers 和 spring.schemas 文件

接下就按照上面的步驟來看看 spring 自定義標籤的整個過程。

1.解析自定義標籤

爲了擴展 xml 配置文件的標籤,spring 提供了一個機制用於對自定義的元素進行解析。

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
	// 通過 w3c 獲取節點所在的 namespace URI
	String namespaceUri = getNamespaceURI(ele);
	// 解析 namespace URI,獲取 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;
	}
	
	// 調用自定義的 NamespaceHandler 進行解析
	return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

方法只做了三件事:

  • 獲取節點所屬的 namespace URI
  • 解析 namespace URI 得到 NamespaceHandler
  • 調用 NamespaceHandler.parse() 方法解析自定義標籤

那麼問題來了,NamespaceHandler 是什麼?官方文檔是這樣定義的:NamespaceHandler 是 spring 解析在 xml 配置文件配置的自定義標籤的基本接口。因此可以知道我們遇到如 <context: componenet-scan /> 這樣的標籤時,會實現 NamespaceHandler 接口,可是 spring 怎麼知道我自己的 NamespaceHandler 呢?並且 spring 在解析時怎麼驗證標籤配置是否正確呢?是的,我們需要在 META-INF 目錄添加兩個文件:spring.schemas 和 spring.handlers。spring.schemas 告知 spring 標籤的 xsd 文件在哪;spring.handlers 告知 spring 解析標籤的 NamespaceHandler 的全路徑名。形式分別如下:

http://www.springframework.org/schema/context/spring-context-4.3.xsd=org/springframework/context/config/spring-context-4.3.xsd
http://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler

從名字上看 NamespaceHandler 是處理 namespace 的,它並不直接用於解析自定義標籤,它底層會委託給 BeanDefinitionParser 接口。那麼 BeanDefinitionParser 又是在哪註冊的呢?我們在看 NamespaceHandler 接口的文檔時,注意到它的 init() 方法,**init() 方法是在解析自定義標籤之前被調用。**因此我們可以在 init() 方法裏面註冊我們自定義的 BeanDefinitionParser 接口。
我們拿 <context: component-scan /> 舉例子,看 spring 本身怎麼做的。在上面說明 spring.schemas 和 spring.handlers 文件時就是拿它作爲示例,因此就不再說了,同時也可以看到,此標籤的 NamespaceHandler 是 org.springframework.context.config.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());
		// 對 componenet-scan 的解析
		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());
	}

}
public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
	// ...省略字段定義
	
	@Override
	public BeanDefinition parse(Element element, ParserContext parserContext) {
		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.
		ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
		Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
		registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

		return null;
	}
	
	// ...省略其他方法
}

所以瞭解其工作原理之後,發現也並不負載,主要 Spring 幫我們定義好了開發標準,我們只需要按照它的約定去實現我們自己的邏輯就可以。如果看過 dubbo 源碼的肯定也知道,dubbo 跟 spring 集成,也是因爲 dubbo 定義了自己的 DubboNamespaceHandler,註冊了對各個標籤的解析器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章