天天看點

源碼學習之Spring容器建立原理

作者:京東雲

1 前言

衆所周知,Spring可以幫我們管理我們需要的bean。在我們需要用到這些bean的時候,可以很友善的擷取到它,然後進行一系列的操作。比如,我們定義一個bean MyTestBean

public class MyTestBean {
    private String testStr = "testStr";

    public String getTestStr() {
        return testStr;
    }

    public void setTestStr(String testStr) {
        this.testStr = testStr;
    }           

然後xml配置一下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      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-3.1.xsd">
   <bean id = "myTestBean" class="bean.MyTestBean"/>
</beans>           

編寫一下測試代碼,測試一下,就會看到測試通過的結果。

public class BeanFactoryTest {

    @Test
    public void testSimpleLoad() {
        BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
        MyTestBean bean = (MyTestBean)bf.getBean("myTestBean");
        assert "testStr".equals(bean.getTestStr());
    }
}           

直接使用BeanFactory作為容器對于Spring來說不常見,這裡隻是用來測試,以便可以更快更好地分析Spring内部原理。其涉及到的一些元件,貫穿整個Spring容器當中,對于我們了解其他Spring容器也有很大的幫助。限于篇幅,這裡隻介紹該容器建立的部分,即對于new XmlBeanFactory(new ClassPathResource(“beanFactoryTest.xml”)),Spring都幹了些什麼。

2 Spring容器建立原理

2.1 整體實作流程

首先我們大緻了解一下Spring容器建立的整體過程。

源碼學習之Spring容器建立原理

整體時序圖

可以看到,該Spring容器建立大緻分為以下幾部分:

  1. 資源的封裝,以Resource封裝配置檔案
  2. 加載BeanDefinition
  3. 解析配置檔案,擷取Document
  4. 解析及注冊BeanDefinition
  5. 标簽的解析,分為預設标簽和自定義标簽的解析

下面我們就以這樣的順序對各個部分從代碼實作上進行具體分析。

2.2 核心類介紹

在進行具體建立邏輯之前,我們先對Spring容器建立的核心類進行介紹,以便我們更好地掌握它的實作過程。

2.2.1 DefaultListableBeanFactory

XmlBeanFactory繼承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整個bean加載的核心部分,是Spring注冊及加載bean的預設實作。XmlBeanFactory與DefaultListableBeanFactory的不同之處在于,XmlBeanFactory使用了自定義的XML讀取器XmlBeanDefinitionReader,實作了個性化的BeanDefinitionReader讀取。

源碼學習之Spring容器建立原理

容器加載相關類圖

2.2.2 XmlBeanDefinitionReader

XmlBeanDefinitionReader用于資源檔案讀取、解析及Bean注冊。讀取Xml配置檔案的流程大緻為,首先使用ResourceLoader将資源檔案路徑轉換為對應的Resource檔案,然後将Resource檔案轉換為Document檔案,最後對Document及Element進行解析。

源碼學習之Spring容器建立原理

配置檔案讀取相關類圖

2.2.3 BeanDefinition

在Spring中,BeanDefinition是配置檔案元素标簽在容器中的内部表示形式,包含了元素的所有資訊。Spring将配置檔案中的轉換為BeanDefinition,并将這些BeanDefinition注冊到BeanDefinitionRegistry中。BeanDefinitionRegistry以map形式儲存,後續操作直接從BeanDefinitionRegistry中讀取配置資訊。

源碼學習之Spring容器建立原理

BeanDefinition及其實作類

2.3 配置檔案的封裝

在Java中不同的資源都要抽象成URL,然後使用不同類型的URLStreamHandler處理不同的URL表示的資源。但是,Spring對其内部使用到的資源實作了自己的抽象結構:Resource接口封裝底層資源。主要原因有3點:

  1. URL沒有預設定義相對Classpath或ServletContext等資源的handler
  2. URL沒有提供基本的方法,例如檢查目前資源是否存在是否可讀等
  3. 自定義URL handler需要了解URL實作機制

對不同來源的資源檔案都有相應的Resource實作:檔案(FileSystemResource)、Classpath資源(ClassPathResource)、URL資源(UrlResource)、InputStream資源(InputStreamResource)、Byte數組(ByteArrayResource)等。

源碼學習之Spring容器建立原理

資源檔案處理相關類圖

2.4 加載BeanDefinition

下面我們就從代碼層次看看整個容器究竟是怎麼實作的。觀察測試代碼,我們可以将XmlBeanFactory的構造方法作為切入點進行分析。

public XmlBeanFactory(Resource resource) throws BeansException {
   this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
    super(parentBeanFactory);
    // 加載BeanDefinition
    this.reader.loadBeanDefinitions(resource);
}           

主要做了兩件事,一是調用父類的構造方法,二是加載BeanDefinition。

首先我們先進入父類構造方法,最終進到AbstractAutowireCapableBeanFactory構造方法中。

public AbstractAutowireCapableBeanFactory() {
   super();
   // 忽略BeanNameAware、BeanFactoryAware和BeanClassLoaderAware接口的自動裝配功能
   ignoreDependencyInterface(BeanNameAware.class);
   ignoreDependencyInterface(BeanFactoryAware.class);
   ignoreDependencyInterface(BeanClassLoaderAware.class);
}           

主要是ignoreDependencyInterface方法,它的主要功能是忽略給定接口的自動裝配功能。實作上很簡單,就是把這些Class加入到ignoredDependencyInterfaces集合中,ignoredDependencyInterfaces是Set>類型。

再看加載Bean的方法,執行的是XmlBeanDefinitionReader類的loadBeanDefinitions方法。進入方法,可以看到主要就是做了兩件事,一是構造InputSource,這個類全路徑名是org.xml.sax.InputSource,這步的目的就是通過SAX讀取XML檔案事先準備一下InputSource對象。而真正的加載Bean的邏輯在doLoadBeanDefinitions方法中。

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
   InputStream inputStream = encodedResource.getResource().getInputStream();
   // 建構InputSource,用于解析XML
InputSource inputSource = new InputSource(inputStream);
   if (encodedResource.getEncoding() != null) {
      inputSource.setEncoding(encodedResource.getEncoding());
   }
// 實際加載BeanDefinition的執行邏輯
   return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}           

doLoadBeanDefinitions方法首先加載XML檔案,得到Document對象,然後根據Document對象注冊Bean。我們首先看下得到Document對象的過程。

2.5 擷取Document

擷取Document,首先通過getValidationModeForResource擷取XML驗證模式,然後解析得到Document對象。

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
   // 首先擷取XML驗證模式,然後SAX方式解析得到Document
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
         getValidationModeForResource(resource), isNamespaceAware());
}           

常用的XML驗證模式有兩種:DTD和XSD,實際使用哪種驗證模式在getValidationModeForResource中進行了解析。這個方法判斷是DTD驗證還是XSD驗證,僅僅是判斷一下XML是否包含DOCTYPE字元串。

而解析得到Document的方法很簡單,就是通過SAX解析XML文檔的套路。這裡不再贅述。

2.6 解析及注冊BeanDefinition

當把檔案轉換為Document後,接下來的提取及注冊bean就是我們的重頭戲了。調用了以下方法。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
   int countBefore = getRegistry().getBeanDefinitionCount();
// 注冊BeanDefinition
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
   return getRegistry().getBeanDefinitionCount() - countBefore;
}           

在這個方法中很好地應用了單一職責原則,将邏輯處理委托給單一的類進行處理,而這個邏輯處理類就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一個接口,執行個體化的工作在createBeanDefinitionDocumentReader()中完成,真正的類型是DefaultBeanDefinitionDocumentReader。而它的registerBeanDefinitions方法很簡單,僅僅是先根據Document擷取了root,實際注冊在doRegisterBeanDefinitions方法中。

protected void doRegisterBeanDefinitions(Element root) {
   if (this.delegate.isDefaultNamespace(root)) {
      String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
      if (StringUtils.hasText(profileSpec)) {
         String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
               profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
        // 如果環境變量不包含指定profile,則流程結束 
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
            return;
        }
      }
   }
   preProcessXml(root);
// 解析BeanDefinition
   parseBeanDefinitions(root, this.delegate);
   postProcessXml(root);
}           

注冊過程首先對profile進行處理,如果是環境變量定義的則進行處理,否則不進行處理。然後就是解析bean。這裡調用了preProcessXml(root)和postProcessXml(root)兩個方法,但是發現這兩個方法是空方法。這裡應用了模闆方法模式。如果繼承自DefaultBeanDefinitionDocumentReader的子類需要在Bean解析前後做一些處理的話,隻需要重寫這兩個方法即可。

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

而解析bean需要判斷元素是否是預設命名空間,如果是則調用parseDefaultElement(ele, delegate)方法,不是則調用delegate.parseCustomElement(ele)方法。判斷是否是預設命名空間,調用isDefaultNamespace方法,元素或者節點的命名空間與Spring中固定的命名空間http://www.springframework.org/schema/beans 進行對比,一緻則認為是預設的,否則就認為是自定義的。

2.7 預設标簽的解析

預設标簽的解析邏輯一目了然,分别對4種不同标簽(import、alias、bean和beans)做了不同的處理。

2.7.1 bean标簽的解析及注冊

對bean标簽的解析是通過processBeanDefinition(ele, delegate)方法進行的。大緻邏輯總結如下:

  1. 首先委托BeanDefinitionDelegate類的parseBeanDefinitionElement方法進行元素解析,傳回BeanDefinitionHolder類型的執行個體bdHolder,經過這個方法後,bdHolder執行個體已經包含我們配置檔案中配置的各種屬性了,例如class、name、id、alias等屬性。
  2. 如果bdHolder不為空,若存在預設标簽的子節點下再有自定義屬性,還需要再次對自定義标簽進行解析。
  3. 解析後,對bdHolder進行注冊。注冊操作委托給了BeanDefinitionReaderUtils的registerBeanDefinition方法。
  4. 發出響應事件,通知相關監聽器,這個bean已經加載完了。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
   // 解析元素資訊,用bdHolder封裝
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
   if (bdHolder != null) {
// 如果标簽下有自定義屬性,則對自定義屬性進行解析
      bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
      // 對bdHolder進行注冊
      BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
      // 發送注冊事件給監聽器.
      getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
   }
}           

1)元素解析及資訊提取

首先我們從元素解析及資訊提取開始:delegate.parseBeanDefinitionElement(ele)。進入BeanDefinitionParserDelegate類parseBeanDefinitionElement方法,主要完成如下内容:

  1. 提取元素的id和name屬性
  2. 解析其他所有屬性,封裝到GenericBeanDefinition類型的執行個體中,對應this.parseBeanDefinitionElement(ele, beanName, containingBean)方法
  3. 如果bean沒有指定beanName,則使用預設規則為此Bean生成beanName
  4. 将擷取到的資訊封裝到BeanDefinitionHolder執行個體中

beanName取值政策是,首先取id,如果沒有指定id則取name[0](因為name可以指定多個),如果name也沒有指定,則采取自動生成方式生成。

最終bean元素的所有屬性和子元素資訊都儲存到GenericBeanDefinition中了。至此就完成了XML文檔到GenericBeanDefinition的轉換。

2)預設标簽中自定義标簽元素的解析

如果這個bean使用的是預設的标簽配置,但是其中的子元素卻使用了自定義配置,這時這部分内容就起作用了。入口是delegate.decorateBeanDefinitionIfRequired(ele, bdHolder)方法。它分别對元素的所有屬性和子元素進行了decorateIfRequired方法的調用。decorateIfRequired方法會判斷,如果是自定義節點,則找出自定義類型所對應的NamespaceHandler并進行進一步解析。

public BeanDefinitionHolder decorateIfRequired(
      Node node, BeanDefinitionHolder originalDef, BeanDefinition containingBd) {
   String namespaceUri = getNamespaceURI(node);
   if (!isDefaultNamespace(namespaceUri)) {
// 如果元素是自定義元素,則根據命名空間找到對應的命名空間處理器
      NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
      if (handler != null) {
 // 自定義命名空間處理器處理bdHolder
         return handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd));
      }
   }
   return originalDef;
}           

3)BeanDefinition的注冊

BeanDefinition注冊分為通過beanName注冊BeanDefinition和注冊别名兩部分。進入BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.getReaderContext().getRegistry())方法内部,如下

public static void registerBeanDefinition(
      BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
      throws BeanDefinitionStoreException {
   String beanName = definitionHolder.getBeanName();
// 通過beanName注冊BeanDefinition
   registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
   String[] aliases = definitionHolder.getAliases();
   if (aliases != null) {
      for (String alias : aliases) {
 // 注冊别名
         registry.registerAlias(beanName, alias);
      }
   }
}           

通過beanName注冊BeanDefinition主要進行了4個步驟:

  1. 對AbstractBeanDefinition的校驗,主要是對于AbstractBeanDefinition的methodOverrides屬性的
  2. 對于beanName已經注冊的情況的處理,如果已經設定了不允許bean的覆寫,會抛出異常,否則進行覆寫
  3. 加入Map緩存,beanName為key,BeanDefinition為value
  4. 清除之前留下的對應beanName的緩存
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) {
   if (beanDefinition instanceof AbstractBeanDefinition) {
 // 對AbstractBeanDefinition的校驗
      ((AbstractBeanDefinition) beanDefinition).validate();
   }
   BeanDefinition oldBeanDefinition = this.beanDefinitionMap.get(beanName);
   if (oldBeanDefinition != null) {
      if (!isAllowBeanDefinitionOverriding()) {
 // 如果beanName已經注冊,并且設定了不允許bean覆寫,會抛出異常
         throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
               "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
               "': There is already [" + oldBeanDefinition + "] bound.");
      }
 // 将beanDefinition存到Map,beanName為key
      this.beanDefinitionMap.put(beanName, beanDefinition);
   }
   else {
      if (hasBeanCreationStarted()) {
         synchronized (this.beanDefinitionMap) {
 // 将beanDefinition存到Map,beanName為key
            this.beanDefinitionMap.put(beanName, beanDefinition);
         }
      }
      else {
         // 将beanDefinition存到Map,beanName為key
         this.beanDefinitionMap.put(beanName, beanDefinition);
      }
   }
   if (oldBeanDefinition != null || containsSingleton(beanName)) {
     // 清除之前留下的對應beanName的緩存 
resetBeanDefinition(beanName);
   }
}           

注冊别名的原理相對簡單,分為4個步驟:

  1. alias和beanName相同情況處理,此時會删除原有的alias
  2. alias覆寫處理,若aliasName已經使用了并已經指向了另一個beanName,且設定了别名不能覆寫,則會抛出異常
  3. alias循環檢查,如果出現了别名循環的情況,則抛出異常
  4. 注冊alias
public void registerAlias(String name, String alias) {
    if (alias.equals(name)) {
        this.aliasMap.remove(alias);
    } else {
        String registeredName = (String)this.aliasMap.get(alias);
        if (registeredName != null) {
            if (registeredName.equals(name)) {
                return;
            }
 // 若aliasName已經使用了并已經指向了另一個beanName,且設定了别名不能覆寫,則會抛出異常
            if (!this.allowAliasOverriding()) {
                throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + name + "': It is already registered for name '" + registeredName + "'.");
            }
        }
// alias循環檢查
        this.checkForAliasCircle(name, alias);
// alias注冊
        this.aliasMap.put(alias, name);
    }

}           

4)通知監聽器解析及注冊完成

通過代碼this.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder))完成此功能。這裡的實作隻是為了擴充,當需要對注冊BeanDefinition事件進行監聽時可以通過注冊監聽器的方式并将處理邏輯寫入監聽器中,目前Spring沒有對此事件進行任何處理。

2.7.2 alias标簽的解析

對alias标簽的解析是通過processAliasRegistration(ele)方法處理的。

protected void processAliasRegistration(Element ele) {
   String name = ele.getAttribute(NAME_ATTRIBUTE);
   String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
   boolean valid = true;
   if (!StringUtils.hasText(name)) {
      valid = false;
   }
   if (!StringUtils.hasText(alias)) {
      valid = false;
   }
   if (valid) {
 // alias标簽解析
      getReaderContext().getRegistry().registerAlias(name, alias);
 // 通知監聽器
      getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
   }
}           

進入方法内部可以看到,this.getReaderContext().getRegistry().registerAlias(name, alias)方法實作了alias标簽的解析,而這個方法實際就是前面注冊别名的那個方法。this.getReaderContext().fireAliasRegistered(name, alias, this.extractSource(ele))這個方法用于在别名注冊後通知監聽器做相應的處理,這裡的實作隻是為了擴充,目前Spring沒有對此事件進行任何處理。

2.7.3 import标簽的解析

對import标簽的解析,Spring大緻分為以下步驟:

  1. 擷取import标簽的resource屬性配置的路徑
  2. 解析路徑中的系統屬性,格式如“${user.dir}”,對應方法this.getReaderContext().getEnvironment().resolveRequiredPlaceholders(location)
  3. 判斷resource屬性配置的路徑是絕對路徑還是相對路徑
  4. 如果是絕對路徑,則調用bean的解析過程進行解析
  5. 如果是相對路徑則計算出絕對路徑後進行解析
  6. 通知監聽器,解析完成

不管是絕對路徑下import标簽的解析還是相對路徑下import标簽的解析,通過跟蹤代碼發現,最後都會調到XmlBeanDefinitionReader類的loadBeanDefinitions方法,而這個方法在加載bean部分已經了解了。

2.7.4 嵌入式beans标簽的解析

該标簽解析調用的是DefaultBeanDefinitionDocumentReader的doRegisterBeanDefinitions方法,而這個方法已經在解析及注冊BeanDefinitions部分了解了。嵌入式beans标簽和非嵌入式beans标簽的解析過程其實是一樣的。

2.8 自定義标簽的解析

自定義标簽非常有用,我們熟知的标簽就是采用自定義标簽的原理實作的。下面來探究一下自定義标簽的解析原理。

前面提到過解析自定義标簽的入口,檢視以下具體實作:

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    String namespaceUri = this.getNamespaceURI(ele);
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    if (handler == null) {
        return null;
    } else {
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }
}           

這裡傳入的containingBd為null。可以看到自定義标簽解析的思路特别簡單。無非是根據标簽元素擷取對應的命名空間,根據命名空間解析對應的處理器,然後根據使用者自定義的處理器進行解析。

2.8.1 解析自定義标簽處理器

通過元素可以擷取它的命名空間,有了命名空間就可以進行NamespaceHandler提取了。在readerContext初始化的時候其屬性namespaceHandlerResolver被初始化為DefaultNamespaceHandlerResolver的執行個體。是以調用resolve方法實際調用的是DefaultNamespaceHandlerResolver的方法。

public NamespaceHandler resolve(String namespaceUri) {
// 擷取命名空間到處理器的映射關系
   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;
      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;
   }
}

private Map<String, Object> getHandlerMappings() {
   if (this.handlerMappings == null) {
      synchronized (this) {
         if (this.handlerMappings == null) {
 // 加載配置檔案META-INF/spring.handlers
         Properties mappings =
               PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
         Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
         CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
         this.handlerMappings = handlerMappings;
         }
      }
   }
   return this.handlerMappings;
}           

可以看到自定義标簽處理器的解析流程,首先通過this.getHandlerMappings()方法解析配置檔案擷取命名空間到處理器的映射關系,Map儲存。然後執行個體化該命名空間處理器,調用init()初始化方法。而定義的this.handlerMappingsLocation變量在調用構造方法的時候代碼寫死了,是META-INF/spring.handlers。這個配置檔案是使用者自己去編寫的,定義命名空間到處理器類的映射。命名空間處理器類的實作也是需要使用者去實作,使用者可以繼承NamespaceHandlerSupport抽象類實作一下init()抽象方法。

2.8.2 标簽解析

我們已經得到了由哪個标簽處理器進行處理,接下來标簽解析由handler.parse(ele, new ParserContext(this.readerContext, this, containingBd))去實作。

public BeanDefinition parse(Element element, ParserContext parserContext) {
// 擷取元素解析器,進行解析
   return findParserForElement(element, parserContext).parse(element, parserContext);
}

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
   // 根據節點名稱擷取parser
String localName = parserContext.getDelegate().getLocalName(element);
   BeanDefinitionParser parser = this.parsers.get(localName);
   return parser;
}           

在父類NamespaceHandlerSupport中可以看到,解析功能首先找到解析器,然後進行解析。查找解析器首先擷取節點名稱,然後通過Map parsers擷取對應節點的解析器。而這個Map的指派一般在使用者實作的命名空間處理器init()方法中調用。

而自定義标簽的解析任務由parse方法完成。可以看到,首先通過parseInternal方法将标簽元素轉換成了BeanDefinition,然後解析id和name屬性并用BeanDefinitionHolder封裝元素資訊,接着進行BeanDefinition的注冊,最後通知監聽器。

public final BeanDefinition parse(Element element, ParserContext parserContext) {
 // 解析元素
    AbstractBeanDefinition definition = this.parseInternal(element, parserContext);
    if (definition != null && !parserContext.isNested()) {
 // 解析id
         String id = this.resolveId(element, definition, parserContext);
         String[] aliases = null;
         if (this.shouldParseNameAsAliases()) {
  // 解析name
             String name = element.getAttribute("name");
             if (StringUtils.hasLength(name)) {
                 aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
             }
         }
 // holder封裝元素資訊
         BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
         // 注冊
 this.registerBeanDefinition(holder, parserContext.getRegistry());
         if (this.shouldFireEvents()) {
             BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
             this.postProcessComponentDefinition(componentDefinition);
  // 通知監聽器
             parserContext.registerComponent(componentDefinition);
         }
    }
    return definition;
}           

對于後面三步前面都介紹過了,隻需看下parseInternal方法邏輯。

protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
    // 解析parentName
 String parentName = this.getParentName(element);
    if (parentName != null) {
        builder.getRawBeanDefinition().setParentName(parentName);
    }
 // 解析beanClass
    Class<?> beanClass = this.getBeanClass(element);
    if (beanClass != null) {
        builder.getRawBeanDefinition().setBeanClass(beanClass);
    } else {
        String beanClassName = this.getBeanClassName(element);
        if (beanClassName != null) {
            builder.getRawBeanDefinition().setBeanClassName(beanClassName);
        }
    }
 // 解析source
    builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
    if (parserContext.isNested()) {
        builder.setScope(parserContext.getContainingBeanDefinition().getScope());
    }
 // 解析lazyInit
    if (parserContext.isDefaultLazyInit()) {
        builder.setLazyInit(true);
    }

    this.doParse(element, parserContext, builder);
    return builder.getBeanDefinition();
}

protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
    this.doParse(element, builder);
}

protected void doParse(Element element, BeanDefinitionBuilder builder) {
}           

可以看到,parseInternal方法實際就是先解析parentName、beanClass、source、scope和lazyInit,以BeanDefinition封裝。然後調用doParse方法,這個方法也是使用者自定義解析器需要實作的方法。最後傳回這個BeanDefinition。

3 總結

至此,我們一步一步地分析了Spring容器建立基本原理的所有内容。沒想到短短一句new XmlBeanFactory(new ClassPathResource(“beanFactoryTest.xml”)),Spring做了這麼多事:配置檔案的封裝、Document擷取和解析及注冊BeanDefinition等。解析及注冊BeanDefinition對預設标簽和自定義标簽進行不同的處理。對于預設标簽,又分别對bean标簽、alias标簽、import标簽和beans标簽進行不同的處理。其中bean标簽解析邏輯最為複雜也最為基礎,而剩下的那幾個标簽又複用了bean标簽處理的部分邏輯。

我們從中不僅可以學到Spring容器建立的基本原理,還可以學到許多編碼規範及技巧,了解到好的代碼是什麼樣子的。比如其中應用到的單一職責原則、模闆方法模式等等。而且還可以發現,它的代碼邏輯很清晰,往往通過它的方法名稱就知道這個方法的功能,并且每個方法也不會特别長,增加了代碼的可讀性和可維護性。并且代碼封裝性很好,很多複雜的功能代碼都可以複用。

在我們之後的開發工作中需要不斷學習好的編碼技巧及規範,應用到日常開發工作當中,最終形成我們自己的編碼技巧及風格。

4 參考資料

《Spring技術内幕:深入解析Spring架構與設計原理》

《Spring源碼深度解析》

作者:曹銘(大件技術工坊)