??在之前的章節(jié)中,我們提到了在
Spring
中存在默認標簽與自定義標簽兩種,而在上一章節(jié)中我們分析了Spring
中自定義標簽的加載過程.同樣,我們還是先再次回顧一下,當完成從配置文件到Document
的轉換并提取對應的root
后,將開始了所有元素的解析,而在這一過程中便開始了默認標簽與自定義標簽兩中格式的區(qū)分,方法如下:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
??在本章中,所有的功能都是圍繞其中的一句代碼
delegate.parseCustomElement(root)
開展的.從上面的函數我們可以看出,當Spring
拿到一個元素時首先要做的是根據命名空間進行解析,如果是默認的命名空間,則使用parseDefaultElement
方法進行元素解析,否則使用parseCustomElement
方法進行解析.在分析自定義標簽的解析過程前,我們先了解一下自定義標簽的使用過程.
4.1 自定義標簽使用
??在很多情況下,我們需要為系統(tǒng)提供可配置化支持,簡單的做法可以直接基于
Spring
的標準bean
來配置,但配置較為復雜或者需要更多豐富控制的時候,會顯得非常笨拙.一般的做法會用原生態(tài)的方式去解析定義好的XML
文件,然后轉化為配置對象.這種方式當然可以解決所有問題,但實現起來比較繁瑣,特別是在配置非常復雜的時候,解析工作是一個不得不考慮的負擔.Spring
提供了可擴展Schema
的支持,這個一個不錯的折中方案,擴展Spring
自定義標簽配置大致需要以下幾個步驟(前提是要把Spring
的Core
包加入項目中).
- 創(chuàng)建一個需要擴展的組件.
- 定義一個
XSD
文件描述組件內容.- 創(chuàng)建一個文件,實現
BeanDefinitionParser
接口,用來解析XSD
文件中的定義和組件定義.- 創(chuàng)建一個
Handler
文件,擴展自NamespaceHandlerSupport
,目的是將組件注冊到Spring
容器.- 編寫
Spring.handlers
和Spring.schemas
文件.?現在我們就按照上面的步驟和大家一起體驗自定義標簽的過程.
(1)首先我們創(chuàng)建一個普通的
POJO
,這個POJO
沒有任何特別之處,只是用來接收配置文件.
public class User {
private String userName;
private String email;
// 省略get/set方法
}
(2)定義一個
XSD
文件描述組件內容.
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.test.com/schema/user"
xmlns:tns="http://www.test.com/schema/user"
elementFormDefault="qualified">
<element name="user">
<complexType>
<attribute name="id" type="string" />
<attribute name="userName" type="string" />
<attribute name="email" type="string" />
</complexType>
</element>
</schema>
??在上面的
XSD
文件中描述了一個新的targetNamespace
,并在這個空間中定義了一個name
為user
的element
,user
有3個屬性id
,userName
和string
.這3個類主要用于驗證Spring
配置文件中自定義格式.XSD
文件是XML
,DTD
的替代者,使用XML Schema
語言進行編寫,這里對XSD Schema
不做太多解釋,大家有興趣可以自己研究一下.
(3)創(chuàng)建一個文件,實現
BeanDefinitionParser
接口,用來解析XSD
文件中的定義和組件定義.
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
// Element對應的類
protected Class getBeanClass(Element element) {
return User.class;
}
// 從element中解析并提取對應的元素
protected void doParse(Element element, BeanDefinitionBuilder bean) {
String userName = element.getAttribute("userName");
String email = element.getAttribute("email");
// 將提取的數據放入到BeanDefinitionBuilder中,待到完成所有bean的解析后統(tǒng)一注冊到beanFatory中
if (StringUtils.hasText(userName)) {
bean.addPropertyValue("userName", userName);
}
if (StringUtils.hasText(email)) {
bean.addPropertyValue("email", email);
}
}
}
(4) 創(chuàng)建一個
Handler
文件,擴展自NamespaceHandlerSupport
,目的是將組件注冊到Spring
容器.
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
??以上代碼很簡單,無非是當遇到自定義標簽
<user:aaa>
這樣類似于以user
開頭的元素,就會把這個元素扔給對應的UserBeanDefinitionParser
去解析.
(5) 編寫
Spring.handlers
和Spring.schemas
文件,默認位置是在工程的/ META-INF/
文件夾下,當然,你可以通過Spring
的擴展或者修改源碼的方式改變路徑.
- Spring.handlers :
http\://www.test.com/schema/user=test.MyNamespaceHandler
- Spring.schema :
http\://www.test.com/schema/user.xsd=META-INF/Spring-test.xsd
?到這里,自定義的配置就結束了,而
Spring
加載自定義的大致流程是遇到自定義標簽然后就去Spring.handlers
和Spring.schemas
中去找對應的handler
和XSD
,默認位置是/META-INF/
下,進而有找到對應的handler
以及解析元素的Parser
,從而完成整個自定義元素的解析,也就是說自定義與Spring
中默認的標準配置不同在于Spring
將自定義標簽解析的工作委托給了用戶去實現.
(6) 創(chuàng)建測試配置文件,在配置文件中引入對應的命名空間以及
XSD
后,便可以直接使用自定義標簽了.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myname="http://www.test.com/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.test.com/schema/user http://www.test.com/scheame/user.xsd">
<myname:user id="testbean" userName="aaa" email="bbb" />
</beans>
(7) 測試
public class test {
public static void main(String[] args) {
ApplicationContext bf = new ClassPathXmlApplicationContext("test/test.xml");
User user = (User) bf.getBean("testbean");
System.out.println(user.getUserName() + ", " + user.getEmail());
}
}
?不出意外的話,應該可以看到控制臺打印了如下結果:
?aaa
,bbb
?在上面的例子中,我們實現了通過自定義標簽通過屬性的方式將user
類型的Bean
賦值,在Spring
中自定義標簽非常常用,例如我們熟知的事物標簽:tx(<tx:annotation-driven>)
.
4.2 自定義標簽解析
??了解了自定義標簽的使用后,我們帶著強烈的好奇心來探究一下自定義的解析過程.
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
// containingBd為父類bean,對頂層元素的解析應該設置為null
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 獲取對應的命名空間
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 根據命名空間找到對應的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));
}
??相信了解了自定義標簽的使用方法后,或多或少會對自定義標簽的實現過程有一個自己的想法.其實實現思路非常的簡單,無非是根據對應的
bean
獲取對應的命名空間,根據命名空間解析對應的處理器,然后根據用戶自定義的處理器進行解析.可是有些事情說起來簡單做起來難,我們接下來先看看如何獲取命名空間.
4.2.1 獲取標簽的命名空間
??標簽的解析是從命名空間的提起開始的,無論是區(qū)分
Spring
中默認標簽和自定義標簽還是區(qū)分自定義標簽中不同標簽的處理器都是以標簽所提供的命名空間為基礎的,而至于如何提取對應元素的命名空間其實不需要我們親自去實現,在org.w3c.dom.Node
中已經提供了方法供我們直接調用:
public String getNamespaceURI(Node node){
return node.getNamespaceURI();
}
4.2.2 提取自定義標簽處理器
??有了命名空間,就可以進行
NamespaceHandler
的提取了,繼續(xù)之前的parseCustomElement
方法的跟蹤,分析NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
,在readerContext
初始化的時候其屬性namespaceHandlerResolver
已經被初始化為了DefaultNamespaceHandlerResolver
的實例,所以這里調用的resolve
方法其實調用的是DefaultNamespaceHandlerResolver
類中的方法.我們進入DefaultNamespaceHandlerResolver
的resolve
方法進行查看.
@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
// 獲取所有已經配置的handler映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 根據命名空間找到對應的信息
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
// 已經做過解析的情況直接從緩存讀取
return (NamespaceHandler) handlerOrClassName;
}
else {
// 沒有做過解析則返回類路徑
String className = (String) handlerOrClassName;
try {
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);
}
}
}
??上面的函數清晰地闡述了解析自定義
NamespaceHandler
的過程,通過之前的示例程序我們了解到如果要使用自定義標簽,那么其中一項必不可少的操作就是在Spring.handlers
文件中配置命名空間與命名空間處理的映射關系.只有這樣,Spring
才能根據映射關系找到匹配的處理器,而尋找匹配的處理器就是在上面方法中實現,當獲取到自定義的NamespaceHandler
之后就可以進行處理器初始化并解析了.我們這里在回憶一下對命名空間處理器的定義內容:
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
??當得到自定義命名空間處理后會馬上執(zhí)行
namespaceHandler.init()
來進行自定義BeanDefinitionParser
的注冊.在這里,可以注冊多個標簽解釋器,當前實例中只有支持<myname:user>
的寫法,也可以在這里注冊多個解析器,如<myname:A> <myname:B>
等,是的myname
的命名空間中可以支持多種標簽解析.
??注冊后,命名空間處理器就可以根據標簽的不同來調用不同的解析器進行解析.那么,根據上面的函數與之前介紹過的例子,我們基本上可以推斷getHandlerMappings
的主要功能就是讀取Spring.handlers
配置文件并將配置文件緩存在map
中.
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
// 如果沒有被緩存則開始進行緩存
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
// 將Properties格式文件合并到Map格式的handlerMappings中
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;
}
??同我們想象的一樣,接住了工具類
PropertiesLoaderUtils
對屬性handlerMappingsLocation
進行了配置文件的讀取,handlerMappingsLocation
被默認初始化為META-INF/Spring.handlers
.
4.2.3 標簽解析
??得到了解析器以及要分析的元素后,
Spring
就可以將解析工作委托給自定義解析器去解析了.在Spring
中的代碼為:
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
??以之前提到的示例進行分析,此時的
handler
已經被實例化成我們自定義的MyNamespaceHandler
了,而MyNamespaceHandler
也已經完成了初始化的工作,但是在我們實現的自定義命名空間處理器中并沒有實現parse
方法,所以推斷,這個方法是父類中的實現,查看父類NamespaceHandlerSupport
中的parse
方法.
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 尋找解析器并進行解析操作
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
??解析過程中首先是尋找元素對應的解析器,進而調用解析器中的
parse
方法,那么結合示例來講,其實就是首先獲取在MyNameSpaceHandler
類中的init
方法中注冊的對應的UserBeanDefinitionParser
實例,并調用其parse
方法進行進一步解析.
@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 獲取元素名稱,也就是<myname:user中的user,若在示例中,此時localName為user
String localName = parserContext.getDelegate().getLocalName(element);
// 根據user找到對應的解析器,也就是在registerBeanDefinitionParser("user", new UserBeanDefinitionParser());注冊的解析器
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
而對于
parse
方法的處理
@Override
@Nullable
public final BeanDefinition parse(Element element, ParserContext parserContext) {
AbstractBeanDefinition definition = parseInternal(element, parserContext);
if (definition != null && !parserContext.isNested()) {
try {
String id = resolveId(element, definition, parserContext);
if (!StringUtils.hasText(id)) {
parserContext.getReaderContext().error(
"Id is required for element '" + parserContext.getDelegate().getLocalName(element)
+ "' when used as a top-level tag", element);
}
String[] aliases = null;
if (shouldParseNameAsAliases()) {
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
// 將AbstractBeanDefinition轉化為BeanDefinitionHolder并注冊
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
registerBeanDefinition(holder, parserContext.getRegistry());
if (shouldFireEvents()) {
// 需要通知監(jiān)聽器則進行處理
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
}
catch (BeanDefinitionStoreException ex) {
String msg = ex.getMessage();
parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element);
return null;
}
}
return definition;
}
??雖說是對自定義配置文件的解析,但是,我們可以看到,在這個方法中大部分的代碼是用來處理將解析后的
AbstractBeanDefinition
轉化為BeanDefinitionHolder
并注冊的功能,而真正去做解析的事情委托給了函數parseInternal
,正是這句代碼調用了我們自定義的解析函數.
??在parseInternal
中并不是直接調用自定義的doParse
函數,而是進行了一系列的數據準備,包括對beanClass
scope
lazyInit
等屬性的準備.
@Override
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
// 獲取自定義標簽中的class,此時會調用自定義解析器如UserBeanDefinitionParser中的getBeanClass方法
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
// 若子類沒有重寫getBeanClass方法則嘗試檢查子類是否重寫getBeanClassName方法
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
if (containingBd != null) {
// 若存在父類則使用父類的scope屬性
// Inner bean definition must receive same scope as containing bean.
builder.setScope(containingBd.getScope());
}
if (parserContext.isDefaultLazyInit()) {
// 調用子類重寫的doParse方法進行解析
// Default-lazy-init applies to custom bean definitions as well.
builder.setLazyInit(true);
}
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
??回顧一下全部的自定義標簽處理過程,雖然在實例中我們定義
UserBeanDefinitionParser
,但是在其中我們只是做了與自己業(yè)務邏輯相關的部分.不過我們沒做但是并不代表沒有,在這個處理過程中同樣也是按照Spring
中默認標簽的處理方式進行,包括創(chuàng)建BeanDefinition
以及進行相應默認屬性的設置,對于這些工作Spring
都默默地幫我們實現了,只是暴露出一些接口來供用戶實現個性化的業(yè)務.通過對本章的了解,相信讀者對Spring
中自定義標簽的使用以及在解析自定義標簽過程中Spring
為我們做了哪些工作會有一個全面的了解,到此為止我們已經完成了Spring
中全部的解析工作,也就是說到現在為止我們已經理解了Spring
將bean
從配置文件到加載到內存中的全過程,而接下來的任務便是如何使用這些bean
.