Spring源碼閱讀筆記05:自定義xml標籤解析

  • 2020 年 3 月 14 日
  • 筆記

  在上篇文章中,提到了在Spring中存在默認標籤與自定義標籤兩種,並且詳細分析了默認標籤的解析,本文就來分析自定義標籤的解析,像Spring中的AOP就是通過自定義標籤來進行配置的,這裡也是為後面學習AOP原理打下基礎。

  這裡先回顧一下,當Spring完成了從配置文件到Document的轉換並提取對應的root後,將開始所有元素的解析,而在這一過程中便會區分默認標籤與自定義標籤兩種格式,並分別解析,可以再看一下這部分的源碼加深理解:

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);      }  }

  從上面的函數中也可以看出,當Spring拿到一個元素時首先要做的是根據命名空間進行解析,如果是默認的命名空間,則使用parseDefaultElement()方法進行元素解析,否則使用parseCustomElement()方法進行解析。在本文中,所有的功能解析都是圍繞其中的那句代碼delegate.parseCustomElement(root)開展的。

  在分析自定義標籤的解析過程之前,我們先了解一下自定義標籤的使用過程,這裡參考spring文檔中的例子。

1. 自定義標籤使用

  擴展Spring自定義標籤配置大致需要以下幾個步驟:

  • 定義一個XML文件來描述你的自定義標籤元素
  • 創建一個Handler,擴展自NamespaceHandlerSupport
  • 創建若干個BeanDefinitionParser的實現,用來解析XML文件中的定義
  • 將上述文件註冊到Spring中,這裡其實是做一下配置

  接下來我們將創建一個自定義XML元素,便於通過一個更容易的方式配置SimpleDateFormat類型的bean。配置好之後我們可以通過下面的方式來定義一個SimpleDateFormat類型的bean:

<myns:dateformat id = "dateFormat" pattern = "yyyy-MM-dd HH:mm" lenient = "true"/>

1.1 編寫schema

  給Spring IoC容器創建XML擴展標籤的第一步是創建一個新的XML模式來描述對應的標籤(下面是我們將要用來配置SimpleDateFormat對象的schema):

<?xml version="1.0" encoding="UTF-8"?>  <xsd:schema xmlns="http://www.mycompany.com/schema/myns"      xmlns:xsd="http://www.w3.org/2001/XMLSchema"      xmlns:beans="http://www.springframework.org/schema/beans"      targetNamespace="http://www.mycompany.com/schema/myns"      elementFormDefault="qualified"      attributeFormDefault="unqualified">        <xsd:import namespace="http://www.springframework.org/schema/beans"/>        <xsd:element name="dateformat">          <xsd:complexType>              <xsd:complexContent>                  <xsd:extension base="beans:identifiedType">                      <xsd:attribute name="lenient" type="xsd:boolean"/>                      <xsd:attribute name="pattern" type="xsd:string" use="required"/>                  </xsd:extension>              </xsd:complexContent>          </xsd:complexType>      </xsd:element>  </xsd:schema>

  定義了上面的schema之後,我們就可以直接使用元素<myns:dateformat/>來配置SimpleDateFormat類型的對象了:

<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>

  如果沒有做上面的工作,我們可能就需要通過下面的方式來配置SimpleDateFormat類型的對象了:

<bean id="dateFormat" class="java.text.SimpleDateFormat">      <constructor-arg value="yyyy-HH-dd HH:mm"/>      <property name="lenient" value="true"/>  </bean>

1.2 編寫一個BeanDefinitionParser

  這個是繼承自AbstractSingleBeanDefinitionParser,主要是用來將自定義標籤解析成BeanDefinition。

public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser{      protected Class getBeanClass(Element element) {          return SimpleDateFormat.class;      }      protected void doParse(Element element, BeanDefinitionBuilder bean) {          // this will never be null since the schema explicitly requires that a value be supplied          String pattern = element.getAttribute("pattern");          bean.addConstructorArg(pattern);          // this however is an optional property          String lenient = element.getAttribute("lenient");          if (StringUtils.hasText(lenient)) {              bean.addPropertyValue("lenient", Boolean.valueOf(lenient));          }      }  }

1.3 編寫一個NamespaceHandler

  這個是繼承自NamespaceHandlerSupport,主要是將上面的BeanDefinitionParser註冊到Spring容器:

public class MyNamespaceHandler extends NamespaceHandlerSupport{        public void init() {          registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());      }    }

1.4 編寫Spring.handlers和Spring.schemas文件

  這兩個文件默認位置是在工程資源目錄的/META-INF/文件夾下,內容如下(注意要改成自己的包名):

META-INF/spring.handlers  http://www.mycompany.com/schema/myns=spring.customElement.MyNamespaceHandler    META-INF/spring.schemas  http://www.mycompany.com/schema/myns/myns.xsd=spring/customElement/myns.xsd

1.5 自定義標籤使用示例

  使用自定義的擴展標籤和使用Spring提供的默認標籤是類似的,可以按照如下配置一個SimpleDateFormat類型的bean:

<?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:myns="http://www.mycompany.com/schema/myns"      xsi:schemaLocation="  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  http://www.mycompany.com/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">        <!-- as a top-level bean -->      <myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>  </beans>

  配置好之後可以測試一下:

public static void main(String[] args) {      XmlBeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("customElement.xml"));      SimpleDateFormat myTestBean = (SimpleDateFormat)xmlBeanFactory.getBean("defaultDateFormat");      System.out.println( "now time --- "+ myTestBean.format(new Date()));  }    // 輸出結果:  now time --- 2020-03-07 20:37

2. 自定義標籤解析

  了解了自定義標籤的使用之後,我們來探究一下自定義標籤的解析過程。接着文章開頭提到的,我們要從BeanDefinitionParserDelegate的parseCustomElement()方法開始:

public BeanDefinition parseCustomElement(Element ele) {      return parseCustomElement(ele, null);  }    public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {      // 獲取對應的名稱空間      String namespaceUri = getNamespaceURI(ele);      // 根據命名空間找到對應的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));  }

  這裡可以看出對自定義標籤進行解析的思路是根據Element獲取對應的名稱空間,然後根據名稱空間獲取對應的處理器,最後根據用戶自定義的處理器進行解析,可是看起來簡單,實現起來就不是這麼簡單了,先來看一下名稱空間的獲取吧。

2.1 獲取標籤的名稱空間

  自定義標籤的解析是從名稱空間的提取開始的,無論是區分默認標籤和自定義標籤,還是區分自定義標籤對應的不同處理器,都是以標籤所提供的名稱空間為基礎的。至於如何提取對應元素的名稱空間,已經有現成的實現可供使用,spring中是直接調用org.w3c.dom.Node提供的相應方法來完成名稱空間的提取:

public String getNamespaceURI(Node node) {      return node.getNamespaceURI();  }

2.2 獲取自定義標籤處理器

  有了名稱空間,就可以此來提取對應的NamespaceHandler了,這項工作是由下面這句代碼來完成的:

NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

  這裡readerContext的getNamespaceHandlerResolver()方法返回的其實是DefaultNamespaceHandlerResolver,所以我們直接進入其resolve()方法中往下看:

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.init();              // 記錄在緩存              handlerMappings.put(namespaceUri, namespaceHandler);              return namespaceHandler;          }          catch (ClassNotFoundException ex) {              throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +                      namespaceUri + "] not found", ex);          }          catch (LinkageError err) {              throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +                      namespaceUri + "]: problem with handler class file or dependent class", err);          }      }  }

  上面函數中的流程還是比較清晰的,在前面的自定義標籤使用示例中有說到,如果要使用自定義標籤,需要在Spring.handlers文件中配置名稱空間與名稱空間處理器的映射關係。只有這樣,Spring才能根據映射關係找到匹配的處理器。

  而尋找匹配的處理器就是在上面函數中實現的,當獲取到自定義的NamespaceHandler之後就可以進行處理器初始化並解析了。這裡我們再回憶一下前面自定義標籤示例中,對於名稱空間處理器的內容(我們在其init()方法中註冊了一個解析器)。

  在上面的代碼中,獲取到自定義名稱空間處理器後會馬上執行其init()方法來進行自定義BeanDefinitionParser的註冊。當然在init()中可以註冊多個標籤解析器,如<myns:A、<myns:B等,使得myns的名稱空間中可以支持多種標籤解析。

  註冊好之後,名稱空間處理器就可以根據標籤的不同來調用不同的解析器進行解析。根據上面的函數和之前的例子,我們基本可以判斷getHandlerMappings()的主要功能就是讀取Spring.handlers配置文件並將配置文件緩存在map中:

private Map<String, Object> getHandlerMappings() {      // 如果沒有被緩存則開始進行緩存      if (this.handlerMappings == null) {          synchronized (this) {              if (this.handlerMappings == null) {                  try {                      // this.handlerMappingsLocation在構造函數中已經被初始化為:META-INF/Spring.handlers                      Properties mappings =                              PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);                      if (logger.isDebugEnabled()) {                          logger.debug("Loaded NamespaceHandler mappings: " + mappings);                      }                      Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(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 this.handlerMappings;  }

  這裡是藉助工具類PropertiesLoaderUtils對Spring.handlers配置文件進行了讀取,然後將讀取的內容放到緩存中並返回。

2.3 標籤解析

  獲取到解析器以及要解析的元素後,Spring將解析工作委託給自定義解析器來解析,即下面代碼所完成的:

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

  此時我們拿到的handler其實是我們自定義的MyNamespaceHandler了,但是我們前面並沒有實現parse()方法,所以這裡這個應該是調用的父類中的parse()方法,看一下NamespaceHandlerSupport中的parse()方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {      // 尋找解析器並進行解析操作      return findParserForElement(element, parserContext).parse(element, parserContext);  }    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {      // 獲取元素名稱,也就是<myns:dateformat中的dateformat,在上面示例中,localName為dateformat      String localName = parserContext.getDelegate().getLocalName(element);      // 根據dateformat找到對應的解析器,也就是在registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());      // 註冊的解析器      BeanDefinitionParser parser = this.parsers.get(localName);      if (parser == null) {          parserContext.getReaderContext().fatal(              "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);      }      return parser;  }

  首先是尋找元素對應的解析器,然後調用其parse()方法。結合我們前面的示例,其實就是首先獲取在MyNamespaceHandler類中的init()方法中註冊對應的SimpleDateFormatBeanDefinitionParser實例,並調用其parse()方法進行進一步解析,同樣這裡parse()方法我們前面是沒有實現的,我們也試着從其父類找一下:

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 = new String[0];              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()) {              // 需要通知監聽器則進行處理                  BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);                  postProcessComponentDefinition(componentDefinition);                  parserContext.registerComponent(componentDefinition);              }          }          catch (BeanDefinitionStoreException ex) {              parserContext.getReaderContext().error(ex.getMessage(), 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,此時會調用自定義解析器中的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));      if (parserContext.isNested()) {          // 若存在父類則使用父類的scope屬性          builder.setScope(parserContext.getContainingBeanDefinition().getScope());      }      if (parserContext.isDefaultLazyInit()) {          // 配置延遲加載          builder.setLazyInit(true);      }      // 調用子類重寫的doParse方法進行解析      doParse(element, parserContext, builder);      return builder.getBeanDefinition();  }    // 這裡就是調用前面示例中我們自己寫的doParse()方法  protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {      doParse(element, builder);  }

  到這裡就完成了對自定義標籤轉換成BeanDefinition的整個過程了,回顧一下整個過程,在我們定義的SimpleDateFormatBeanDefinitionParser中我們只是做了與自己業務邏輯相關的部分,剩下的包括創建BeanDefinition以及進行相應默認屬性的設置,Spring都幫我們默認實現了,我們當然也可以自己來完成這一過程,比如AOP就是這樣做的,但是本文還是用最簡單的方式來做一個說明。

3. 總結

  其實從Spring對自定義標籤的解析中也可以體會到Spring的可擴展式設計思路,通過暴露一些接口,我們就能夠方便地實現自己的個性化業務,不僅如此,Spring自己便是這項功能的踐行者,像AOP、事務都是通過這種方式來定製對應的標籤來完成配置需求的。

  到這裡我們已經完成了Spring中全部的解析工作的學習,也就是說到這裡我們已經學習了Spring將bean從配置文件加載到內存的完整過程,接下來的任務便是如果使用這些bean,這才是IoC容器的重頭戲,後面會詳細學習的。