概述
之前我們已經介紹了spring中默認標簽的解析,解析來我們將分析自定義標簽的解析,我們先回顧下自定義標簽解析所使用的方法,如下圖所示:
我們看到自定義標簽的解析是通過BeanDefinitionParserDelegate.parseCustomElement(ele)進行的,解析來我們進行詳細分析。
自定義標簽的使用
擴展 Spring 自定義標簽配置一般需要以下幾個步驟:
- 創建一個需要擴展的組件
- 定義一個 XSD 文件,用於描述組件內容
- 創建一個實現 AbstractSingleBeanDefinitionParser 接口的類,用來解析 XSD 文件中的定義和組件定義
- 創建一個 Handler,繼承 NamespaceHandlerSupport ,用於將組件注冊到 Spring 容器
- 編寫 Spring.handlers 和 Spring.schemas 文件
下面就按照上面的步驟來實現一個自定義標簽組件。
創建組件
該組件就是一個普通的 JavaBean,沒有任何特別之處。這里我創建了兩個組件,為什么是兩個,后面有用到
User.java
package chenhao.spring01; /** * @author: ChenHao * @Description: * @Date: Created in 16:26 2019/7/2 * @Modified by: */ public class User { private String id; private String userName; private String email;public void setId(String id) { this.id = id; }public void setUserName(String userName) { this.userName = userName; }public void setEmail(String email) { this.email = email; } @Override public String toString() { final StringBuilder sb = new StringBuilder("{"); sb.append("\"id\":\"") .append(id).append('\"'); sb.append(",\"userName\":\"") .append(userName).append('\"'); sb.append(",\"email\":\"") .append(email).append('\"'); sb.append('}'); return sb.toString(); } }
Phone.java
package chenhao.spring01; /** * @author: ChenHao * @Description: * @Date: Created in 16:26 2019/7/2 * @Modified by: */ public class Phone { private String color; private int size; private String remark; public void setColor(String color) { this.color = color; } public void setSize(int size) { this.size = size; } public void setRemark(String remark) { this.remark = remark; } @Override public String toString() { final StringBuilder sb = new StringBuilder("{"); sb.append("\"color\":\"") .append(color).append('\"'); sb.append(",\"size\":") .append(size); sb.append(",\"remark\":\"") .append(remark).append('\"'); sb.append('}'); return sb.toString(); } }
定義 XSD 文件
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.chenhao.com/schema/user" targetNamespace="http://www.chenhao.com/schema/user" elementFormDefault="qualified"> <xsd:element name="user"> <xsd:complexType> <xsd:attribute name="id" type="xsd:string" /> <xsd:attribute name="userName" type="xsd:string" /> <xsd:attribute name="email" type="xsd:string" /> </xsd:complexType> </xsd:element> <xsd:element name="phone"> <xsd:complexType> <xsd:attribute name="id" type="xsd:string" /> <xsd:attribute name="color" type="xsd:string" /> <xsd:attribute name="size" type="xsd:int" /> <xsd:attribute name="remark" type="xsd:string" /> </xsd:complexType> </xsd:element> </xsd:schema>
在上述XSD文件中描述了一個新的targetNamespace,並在這個空間里定義了一個name為user和phone的element 。user里面有三個attribute。主要是為了驗證Spring配置文件中的自定義格式。再進一步解釋,就是,Spring位置文件中使用的user自定義標簽中,屬性只能是上面的三種,有其他的屬性的話,就會報錯。
Parser 類
定義一個 Parser 類,該類繼承 AbstractSingleBeanDefinitionParser ,並實現 getBeanClass()
和 doParse()
兩個方法。主要是用於解析 XSD 文件中的定義和組件定義。這里定義了兩個Parser類,一個是解析User類,一個用來解析Phone類。
UserBeanDefinitionParser.java
package chenhao.spring01; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; import org.w3c.dom.Element; /** * @author: ChenHao * @Description: * @Date: Created in 16:29 2019/7/2 * @Modified by: */ public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @Override protected Class getBeanClass(Element ele){ return User.class; } @Override protected void doParse(Element element, BeanDefinitionBuilder builder) { String id = element.getAttribute("id"); String userName=element.getAttribute("userName"); String email=element.getAttribute("email"); if(StringUtils.hasText(id)){ builder.addPropertyValue("id",id); } if(StringUtils.hasText(userName)){ builder.addPropertyValue("userName", userName); } if(StringUtils.hasText(email)){ builder.addPropertyValue("email", email); } } }
PhoneBeanDefinitionParser.java
package chenhao.spring01; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; import org.w3c.dom.Element; /** * @author: ChenHao * @Description: * @Date: Created in 16:29 2019/7/2 * @Modified by: */ public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @Override protected Class getBeanClass(Element ele){ return Phone.class; } @Override protected void doParse(Element element, BeanDefinitionBuilder builder) { String color = element.getAttribute("color"); int size=Integer.parseInt(element.getAttribute("size")); String remark=element.getAttribute("remark"); if(StringUtils.hasText(color)){ builder.addPropertyValue("color",color); } if(StringUtils.hasText(String.valueOf(size))){ builder.addPropertyValue("size", size); } if(StringUtils.hasText(remark)){ builder.addPropertyValue("remark", remark); } } }
Handler 類
定義 Handler 類,繼承 NamespaceHandlerSupport ,主要目的是將上面定義的解析器Parser類注冊到 Spring 容器中。
package chenhao.spring01; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; /** * @author: ChenHao * @Description: * @Date: Created in 16:38 2019/7/2 * @Modified by: */ public class MyNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); } }
我們看看 registerBeanDefinitionParser 方法做了什么
private final Map<String, BeanDefinitionParser> parsers = new HashMap<>(); protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { this.parsers.put(elementName, parser); }
就是將解析器 UserBeanDefinitionParser和 PhoneBeanDefinitionParser 的實例放到全局的Map中,key為user和phone。
Spring.handlers和Spring.schemas
編寫Spring.handlers和Spring.schemas文件,默認位置放在工程的META-INF文件夾下
Spring.handlers
http\://www.chenhao.com/schema/user=chenhao.spring01.MyNamespaceHandler
Spring.schemas
http\://www.chenhao.com/schema/user.xsd=org/user.xsd
而 Spring 加載自定義的大致流程是遇到自定義標簽然后 就去 Spring.handlers 和 Spring.schemas 中去找對應的 handler 和 XSD ,默認位置是 META-INF 下,進而有找到對應的handler以及解析元素的 Parser ,從而完成了整個自定義元素的解析,也就是說 Spring 將向定義標簽解析的工作委托給了 用戶去實現。
創建測試配置文件
經過上面幾個步驟,就可以使用自定義的標簽了。在 xml 配置文件中使用如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:myTag="http://www.chenhao.com/schema/user" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.chenhao.com/schema/user http://www.chenhao.com/schema/user.xsd"> <bean id="myTestBean" class="chenhao.spring01.MyTestBean"/> <myTag:user id="user" email="chenhao@163.com" userName="chenhao" /> <myTag:phone id="iphone" color="black" size="128" remark="iphone XR"/> </beans>
xmlns:myTag表示myTag的命名空間是 http://www.chenhao.com/schema/user ,在文章開頭的判斷處 if (delegate.isDefaultNamespace(ele)) 肯定會返回false,將進入到自定義標簽的解析
測試
import chenhao.spring01.MyTestBean; import chenhao.spring01.Phone; import chenhao.spring01.User; import org.junit.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.core.io.ClassPathResource; /** * @author: ChenHao * @Description: * @Date: Created in 10:36 2019/6/19 * @Modified by: */ public class AppTest { @Test public void MyTestBeanTest() { BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean"); User user = (User) bf.getBean("user"); Phone iphone = (Phone) bf.getBean("iphone"); System.out.println(user); System.out.println(iphone); } }
輸出結果:
項目整體文件目錄如下
自定義標簽的解析
了解了自定義標簽的使用后,接下來我們分析下自定義標簽的解析,自定義標簽解析用的是方法:parseCustomElement(Element ele, @Nullable BeanDefinition containingBd),進入方法體:
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; } // 調用自定義的 Handler 處理 return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); }
相信了解了自定義標簽的使用方法后,或多或少會對向定義標簽的實現過程有一個自己的 想法 其實思路非常的簡單,無非是根據對應的bean 獲取對應的命名空間 ,根據命名空間解析對應的處理器,然后根據用戶自定義的處理器進行解析。
獲取標簽的命名空間
標簽的解析是從命名空間的提起開始的,元論是區分 Spring中默認標簽和自定義標 還是 區分自定義標簽中不同標簽的處理器都是以標簽所提供的命名空間為基礎的,而至於如何提取對應元素的命名空間其實並不需要我們親內去實現,在 org.w3c.dom.Node 中已經提供了方法供我們直接調用:
String namespaceUri = getNamespaceURI(ele); @Nullable public String getNamespaceURI(Node node) { return node.getNamespaceURI(); }
這里我們通過DEBUG看出myTag:user自定義標簽對應的 namespaceUri 是 http://www.chenhao.com/schema/user
讀取自定義標簽處理器
根據 namespaceUri 獲取 Handler,這個映射關系我們在 Spring.handlers 中已經定義了,所以只需要找到該類,然后初始化返回,最后調用該 Handler 對象的 parse()
方法處理,該方法我們也提供了實現。所以上面的核心就在於怎么找到該 Handler 類。調用方法為:this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)
public NamespaceHandler resolve(String namespaceUri) { // 獲取所有已經配置的 Handler 映射 Map<String, Object> handlerMappings = getHandlerMappings(); // 根據 namespaceUri 獲取 handler的信息:這里一般都是類路徑 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 的init() 方法 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); } } }
首先調用 getHandlerMappings()
獲取所有配置文件中的映射關系 handlerMappings ,就是我們在 Spring.handlers 文件中配置 命名空間與命名空間處理器的映射關系,該關系為 <命名空間,類路徑>,然后根據命名空間 namespaceUri 從映射關系中獲取相應的信息,如果為空或者已經初始化了就直接返回,否則根據反射對其進行初始化,同時調用其 init()
方法,最后將該 Handler 對象緩存。我們再次回憶下示例中對於命名空間處理器的內容:
public class MyNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); } }
當得到自定義命名空間處理后會馬上執行 namespaceHandler.init() 來進行自定義 BeanDefinitionParser的注冊,在這里,你可以注冊多個標簽解析器,如當前示例中 <myTag:user 標簽就使用 new UserBeanDefinitionParser()解析器; <myTag:phone就使用new PhoneBeanDefinitionParser()解析器。
上面我們已經說過, init()中的registerBeanDefinitionParser 方法 其實就是將映射關系放在一個 Map 結構的 parsers 對象中:private final Map<String, BeanDefinitionParser> parsers
。
標簽解析
得到了解析器和分析的元素后,Spring就可以將解析工作委托給自定義解析器去解析了,對於標簽的解析使用的是:NamespaceHandler.parse(ele, new ParserContext(this.readerContext, this, containingBd))方法,進入到方法體內:
public BeanDefinition parse(Element element, ParserContext parserContext) { BeanDefinitionParser parser = findParserForElement(element, parserContext); return (parser != null ? parser.parse(element, parserContext) : null); }
調用 findParserForElement()
方法獲取 BeanDefinitionParser 實例,其實就是獲取在 init()
方法里面注冊的實例對象。如下:
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { //獲取元素名稱,也就是<myTag:user中的 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; }
獲取 localName,在上面的例子中就是 : user,然后從 Map 實例 parsers 中獲取 UserBeanDefinitionParser實例對象。返回 BeanDefinitionParser 對象后,調用其 parse()
,該方法在 AbstractBeanDefinitionParser 中實現:
我們可以從DEBUG中看出,當前標簽是 <myTag:user ,對應的localName是user,對應的自定義解析器是UserBeanDefinitionParser,返回的是UserBeanDefinitionParser實例對象。接下來我們看看 parser.parse (element, parserContext),該方法在 AbstractBeanDefinitionParser 中實現:
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)); } } BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases); //將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder 並注冊 registerBeanDefinition(holder, parserContext.getRegistry()); if (shouldFireEvents()) { 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等屬性的准備。 我們進入到AbstractSingleBeanDefinitionParser.parseInternal方法中:
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { // 創建一個BeanDefinitionBuilder,內部實際上是創建一個GenericBeanDefinition的實例,用於存儲自定義標簽的元素 BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); // 獲取父類元素 String parentName = getParentName(element); if (parentName != null) { builder.getRawBeanDefinition().setParentName(parentName); } // 獲取自定義標簽中的 class,這個時候會去調用自定義解析中的 getBeanClass() Class<?> beanClass = getBeanClass(element); if (beanClass != null) { builder.getRawBeanDefinition().setBeanClass(beanClass); } else { // beanClass 為 null,意味着子類並沒有重寫 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) { // Inner bean definition must receive same scope as containing bean. builder.setScope(containingBd.getScope()); } if (parserContext.isDefaultLazyInit()) { // Default-lazy-init applies to custom bean definitions as well. builder.setLazyInit(true); } // 調用子類的 doParse() 進行解析 doParse(element, parserContext, builder); return builder.getBeanDefinition(); } public static BeanDefinitionBuilder genericBeanDefinition() { return new BeanDefinitionBuilder(new GenericBeanDefinition()); } protected Class<?> getBeanClass(Element element) { return null; } protected void doParse(Element element, BeanDefinitionBuilder builder) { }
在該方法中我們主要關注兩個方法:getBeanClass()
、doParse()
。對於 getBeanClass()
方法,AbstractSingleBeanDefinitionParser 類並沒有提供具體實現,而是直接返回 null,意味着它希望子類能夠重寫該方法,當然如果沒有重寫該方法,這會去調用 getBeanClassName()
,判斷子類是否已經重寫了該方法。對於 doParse()
則是直接空實現。所以對於 parseInternal()
而言它總是期待它的子類能夠實現 getBeanClass()
、doParse()
,其中 doParse()
尤為重要,如果你不提供實現,怎么來解析自定義標簽呢?最后將自定義的解析器:UserDefinitionParser 再次回觀。
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @Override protected Class getBeanClass(Element ele){ return User.class; } @Override protected void doParse(Element element, BeanDefinitionBuilder builder) { String id = element.getAttribute("id"); String userName=element.getAttribute("userName"); String email=element.getAttribute("email"); if(StringUtils.hasText(id)){ builder.addPropertyValue("id",id); } if(StringUtils.hasText(userName)){ builder.addPropertyValue("userName", userName); } if(StringUtils.hasText(email)){ builder.addPropertyValue("email", email); } } }
我們看看 builder.addPropertyValue ("id",id) ,實際上是將自定義標簽中的屬性解析,存入 BeanDefinitionBuilder 中的 beanDefinition實例中
private final AbstractBeanDefinition beanDefinition; public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) { this.beanDefinition.getPropertyValues().add(name, value); return this; }
最后 將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder 並注冊 registerBeanDefinition(holder, parserContext.getRegistry());這就和默認標簽的注冊是一樣了。
至此,自定義標簽的解析過程已經分析完成了。其實整個過程還是較為簡單:首先會加載 handlers 文件,將其中內容進行一個解析,形成 <namespaceUri,類路徑> 這樣的一個映射,然后根據獲取的 namespaceUri 就可以得到相應的類路徑,對其進行初始化等到相應的 Handler 對象,調用 parse()
方法,在該方法中根據標簽的 localName 得到相應的 BeanDefinitionParser 實例對象,調用 parse()
,該方法定義在 AbstractBeanDefinitionParser 抽象類中,核心邏輯封裝在其 parseInternal()
中,該方法返回一個 AbstractBeanDefinition 實例對象,其主要是在 AbstractSingleBeanDefinitionParser 中實現,對於自定義的 Parser 類,其需要實現 getBeanClass()
或者 getBeanClassName()
和 doParse()
。最后將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder 並注冊 。