是誰去讀取 BeanDefinition 的?

碼農談IT發表於2023-09-22

來源:江南一點雨

前面松哥寫文章和小夥伴們仔細捋了捋 Spring 中的 BeanDefinition 存在的幾種情況,那麼 BeanDefinition 是誰來載入呢?如果是 Java 程式碼配置,那不用說,都是註解掃描去載入 BeanDefinition 的,但是如果是 XML 或者其他格式的配置檔案,則有專門的 BeanDefinition 載入器,今天我們們就來看看這個專門的 BeanDefinition 載入器。

1. BeanDefinitionReader

這是一個介面,從名字上就能看出來專門用來讀取 BeanDefinition,介面如下:

public interface BeanDefinitionReader {
 BeanDefinitionRegistry getRegistry();
 @Nullable
 ResourceLoader getResourceLoader();
 @Nullable
 ClassLoader getBeanClassLoader();
 BeanNameGenerator getBeanNameGenerator();
 int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
 int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
 int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
 int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}

這裡幾個方法我來說一下:

  • getRegistry:這個是獲取一個 BeanDefinition 的註冊器,就是把 BeanDefinition 載入出來之後,我們要將之註冊到容器中,所以這裡的 BeanDefinitionRegistry 說白了其實就是某一個容器。
  • getResourceLoader:這個是獲取配置資源的載入器,像我們平時載入 XML 配置檔案的時候,只需要寫檔名即可,系統會自動去 classpath 下查詢檔案,就依賴於這裡的 ResourceLoader。
  • getBeanClassLoader:這個地方就是返回類載入器。
  • getBeanNameGenerator:這個是獲取 beanName 的生成器,有的時候,我們在 XML 檔案中配置的時候,可能沒有給 Bean 設定 name 或者 id 屬性,那麼將會透過這個生成器去生成 beanName,這個松哥在最近錄製的 Spring 原始碼分析影片中已經詳細分析過了,這裡就不再囉嗦了。
  • loadBeanDefinitions:這是四個過載方法,都是根據傳入的配置檔案,去載入 BeanDefinition。雖然有四個過載方法,真正去載入 BeanDefinition 的方法其實是 loadBeanDefinitions(Resource resource),另外三個方法兜兜轉轉最終都會來到這個方法中。

2. AbstractBeanDefinitionReader

BeanDefinitionReader 是介面,定義了操作規範,而 AbstractBeanDefinitionReader 為這個介面提供基本的實現。AbstractBeanDefinitionReader 也還有子類,不過 AbstractBeanDefinitionReader 的子類主要是為了處理不同型別的配置檔案,如 properties 配置、XML 配置等,其他的通用操作基本上就由 AbstractBeanDefinitionReader 來完成了。

這個類的原始碼比較長,我這裡就不貼出來了。該類主要解決了 getRegistry、getResourceLoader、getBeanClassLoader 以及 getBeanNameGenerator 方法的返回值,這四個方法的返回值其實都好處理,傳入對應的值返回就行了。

loadBeanDefinitions 由於存在四個過載方法,這個方法的大致處理流程是 loadBeanDefinitions(String... locations) 呼叫 loadBeanDefinitions(String location)loadBeanDefinitions(String location) 呼叫 loadBeanDefinitions(Resource... resources)loadBeanDefinitions(Resource... resources) 則呼叫 loadBeanDefinitions(Resource resource),所以最後的重任都落在 loadBeanDefinitions(Resource resource) 方法上了,另外三個過載方法只是為了處理載入的配置資源而已。而真正幹活的 loadBeanDefinitions(Resource resource) 方法在 AbstractBeanDefinitionReader 類中並未重寫,這個將來要在各個具體的實現類裡邊去完成了。

2.1 PropertiesBeanDefinitionReader

這個是用來讀取 properties 配置檔案的,我們平時可能透過 XML 檔案來配置 Bean,其實 Spring 裡邊也支援使用 properties 來配置 Bean,不過 properties 配置起來比較麻煩,所以這個配置目前已經廢棄了。

2.2 XmlBeanDefinitionReader

這個就是各位小夥伴最熟悉的透過 XML 檔案來配置 Bean 了。經過前面的解析,小夥伴們已經知道了 XmlBeanDefinitionReader 是重寫了 loadBeanDefinitions(Resource resource) 方法,然後透過這個方法去載入配置檔案的。

@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
 return loadBeanDefinitions(new EncodedResource(resource));
}

可以看到,這裡將傳入的 resource 包裝為一個 EncodedResource 物件,然後呼叫另外一個過載方法,EncodedResource 物件主要是處理配置檔案的編碼問題,防止出現亂碼。

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
 if (!currentResources.add(encodedResource)) {
  throw new BeanDefinitionStoreException(
    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
 }
 try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
  InputSource inputSource = new InputSource(inputStream);
  if (encodedResource.getEncoding() != null) {
   inputSource.setEncoding(encodedResource.getEncoding());
  }
  return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
 }
 catch (IOException ex) {
  throw new BeanDefinitionStoreException(
    "IOException parsing XML document from " + encodedResource.getResource(), ex);
 }
 finally {
  currentResources.remove(encodedResource);
  if (currentResources.isEmpty()) {
   this.resourcesCurrentlyBeingLoaded.remove();
  }
 }
}

當前類裡邊定義了一個 resourcesCurrentlyBeingLoaded 物件,這是一個 ThreadLocal,用來儲存當前正在解析的 XML 檔案,首先會嘗試將當前的要解析的 encodedResource 加入到 currentResources 集合中,如果加不進去,說明當前的 encodedResource 正在處理中,那麼就丟擲異常。

否則就去讀取 encodedResource 中的內容,並將之轉為一個 InputSource,這個 InputSource 是 XML 解析中用到的物件。然後呼叫 doLoadBeanDefinitions 方法去做進一步的解析。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
  throws BeanDefinitionStoreException 
{
  Document doc = doLoadDocument(inputSource, resource);
  int count = registerBeanDefinitions(doc, resource);
  return count;
}

這個方法首先呼叫 doLoadDocument 將 XML 檔案解析為一個 Document 物件。

Document 就是 XML 解析時獲取到的檔案物件,Document 物件代表了一個 XML 檔案的模型樹,所有的其他 Node 都以一定的順序包含在 Document 物件之內,排列成一個樹狀結構,以後對 XML 檔案的所有操作都與解析器無關,直接在這個 Document 物件上進行操作即可。主流的 XML 解析方式有 SAX 解析、DOM 解析以及 Pull 解析。如果大家對於 XML 檔案解析不熟悉的話,可以自行復習,松哥這裡就不再囉嗦了。

接下來就是呼叫 registerBeanDefinitions 方法去解析這個 doc 物件,這個方法的返回值就是一共發現了多少個 BeanDefinition。

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

可以看到,這裡又有了一個新的物件是 BeanDefinitionDocumentReader,這是一個介面,該介面只有一個實現類 DefaultBeanDefinitionDocumentReader,並且這個介面中也只有一個 registerBeanDefinitions 方法,這個方法就是用來解析 Document 物件並註冊成為 BeanDefinition 的。

@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
 this.readerContext = readerContext;
 doRegisterBeanDefinitions(doc.getDocumentElement());
}
protected void doRegisterBeanDefinitions(Element root) {
 BeanDefinitionParserDelegate parent = this.delegate;
 this.delegate = createDelegate(getReaderContext(), root, parent);
 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);
   if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
    return;
   }
  }
 }
 preProcessXml(root);
 parseBeanDefinitions(root, this.delegate);
 postProcessXml(root);
 this.delegate = parent;
}

小夥伴們看到,從 doRegisterBeanDefinitions 方法就開始 XML 解析了,這裡首先讀取節點的 profile 屬性,由於 profile 屬性在配置的時候,可以有多個,多個的話用 ,; 或者空格隔開,所以這裡拿到 profile 之後,先用 MULTI_VALUE_ATTRIBUTE_DELIMITERS 去做一個字串拆分(還記得松哥之前講的 name 屬性的配置方式嗎?就是那個!),將 profile 屬性值拆分為一個陣列 specifiedProfiles,然後判斷系統當前環境是否包含 profile 中的值,如果不包含,就直接 return,後面的內容就不需要解析了。

接下來就是 XML 處理了,有預處理和後置處理兩個方法,但是這兩個目前都是空方法,沒有實現。中間的 parseBeanDefinitions 方法是 XML 解析方法:

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 ele) {
    if (delegate.isDefaultNamespace(ele)) {
     parseDefaultElement(ele, delegate);
    }
    else {
     delegate.parseCustomElement(ele);
    }
   }
  }
 }
 else {
  delegate.parseCustomElement(root);
 }
}

這裡就是遍歷 XML 中的所有子節點,遍歷然後判斷這個子節點是預設標籤還是自定義標籤,像我們平時寫的 importaliasbeanbeans 這些都算是預設標籤,而像匯入 properties 的 <context:property-placeholder location="classpath:db.properties"/> 這種標籤就屬於自定義標籤。

我們先來看預設標籤的解析:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
 if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
  importBeanDefinitionResource(ele);
 }
 else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
  processAliasRegistration(ele);
 }
 else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
  processBeanDefinition(ele, delegate);
 }
 else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
  // recurse
  doRegisterBeanDefinitions(ele);
 }
}

大家看到,根據不同的節點型別,呼叫不同的解析方法。如果節點是 beans,beans 中還是定義 bean 的,所以對於 beans 標籤遞迴呼叫 doRegisterBeanDefinitions 方法進行解析。

我們們看一個比較常見的 bean 標籤解析吧,其他標籤解析也都差不多:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
 if (bdHolder != null) {
  bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
  try {
   // Register the final decorated instance.
   BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
  }
  catch (BeanDefinitionStoreException ex) {
   getReaderContext().error("Failed to register bean definition with name '" +
     bdHolder.getBeanName() + "'", ele, ex);
  }
  // Send registration event.
  getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
 }
}

這個方法做了三件事:

  1. 首先呼叫 delegate.parseBeanDefinitionElement 方法去解析 Element,解析 XML 標籤中的各種屬性值,解析完成之後,將屬性值填充到 BeanDefinition 物件中並返回,這裡建立的 BeanDefinition 型別就是 GenericBeanDefinition 型別的。
  2. delegate.decorateBeanDefinitionIfRequired 方法去檢查當前 Bean 的各種屬性是否需要裝飾。什麼樣的情況需要裝飾呢?這個主要是針對一些比較特殊的屬性,例如我們之前講 Bean 的屬性注入的時候,有一種叫做 p namespace 的屬性注入,就是在標籤中新增 p:username='javaboy' 來實現屬性值的注入,那麼這種屬性就需要特殊處理,這個方法就是用來解決這一類問題的。
  3. 呼叫 BeanDefinitionReaderUtils.registerBeanDefinition 方法完成 BeanDefinition 的註冊,註冊到一個 Map 集合中,集合的 key 就是 beanName,集合的 value 則是 BeanDefinition 物件。

好啦,這就是 XmlBeanDefinitionReader 中的一個大致流程。

2.3 GroovyBeanDefinitionReader

這個是使用 groovy 指令碼配置 Bean,groovy 我們這裡就不展開了。

今天就主要和小夥伴們介紹了 Spring 中 XML 檔案的載入規則。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2985313/,如需轉載,請註明出處,否則將追究法律責任。

相關文章