【修煉內功】[spring-framework] [2] BeanDefinitionReader

本文已收錄 【修煉內功】躍遷之路

spring-framework.jpg

林中小舍.png

寫在最前~

距spring-framework開篇的那篇文章已經一個月了,若是再照這樣的速度下去,這個flag估計大機率又要呵呵~
最近發生了一些事情讓我迷茫於應該堅持什麼,爲何還要癡迷於工做兩三年本就應該掌握的東西上~
‘年’(夕獸)就要來了,總要準備點兒什麼纔能有資本‘除夕’不是~
言歸正傳,切入正題!html

上一篇介紹了Spring中的Resource,一個相似於Linux中「一切皆文件」的概念,屏蔽了對不一樣類型資源操做的差別性,本篇繼續深刻,但必定沒有各位想象或者期待中的那麼深java

平時接觸Spring必定離不開兩個基本的核心概念:容器和控制反轉,然而這兩個概念都不是這篇文章要展開討論的;平時使用Spring都是從各類ApplicationContext開始(如最多見的ClassPathXmlApplicationContext或者SpringBoot中的SpringApplication),然而,ApplicationContext也不是本篇的重點~(大寫調皮,後文詳解)node

在使用Spring的過程當中,咱們會(經過xml <bean class='xxx'></bean>、註解@Bean@Component等等方式)建立大量的<u>bean</u>,衆所周知,bean是被容器所管理的,bean之間的依賴注入依靠的是控制反轉,那在咱們使用bean以前,其是如何被註冊到容器中的呢?git

要解釋這個問題其實很簡單:step 1 解析;step 2 註冊。本篇,咱們將重心放在bean的解析上(但不包含全部方式的解析)github

Spring在註冊bean以前的解析方式有不少種,但無外乎三大類:1. 文件形式的配置;2. 註解形式的配置;3. 硬編碼方式。本篇只談第一類中的一種形式 -- xml配置文件的解析spring

爲何是xml這種千年老古董,由於市面上的書歷來都只講xml配置的解析啊(手動調皮),以上爲玩笑(如下嚴肅臉),由於xml文件對於理解bean的解析更爲容易,在註解大行其道的當下,但願我能堅持到某一天來撰寫一篇文章解釋註解在bean解析、註冊上的運行機制segmentfault

除了一些老項目(不可抗拒的歷史緣由)或者特殊場景依然在使用xml配置外,我相信大多數的程序猿都是向前看的,畢竟Spring(Boot)也在力推零xml配置瀏覽器

Absolutely no code generation and no requirement for XML configuration.

https://docs.spring.io/spring...bash

因此,我但願各位不要將重點放在如何解析xml配置上,而是將其做爲「藥引子」,引出其背後更爲重要的概念(BeanDefinitionBeanFactoryBeanRegistry網絡

基於xml配置的ApplicationContext常見的有ClassPathXmlApplicationContextFileSystemXmlApplicationContext(見名識意),基於xml配置的BeanFactory則是XmlBeanFactory(ApplicationContext與BeanFactory的關係會放到後文中),而全部這些在解析bean的時候使用的都是XmlBeanDefinitionReader (具體如何使用會在之後的篇幅中介紹,本篇請將重點暫時放在BeanDefinitionReader上)

XmlBeanDefinitionReader實現自BeanDefinitionReader,而在BeanDefinitionReader的實現裏,除了XmlBeanDefinitionReader以外還有PropertiesBeanDefinitionReaderGroovyBeanDefinitionReader,一個解析properties文件、一個解析groovy腳本,因爲過於小衆、原理相通,此處不表(若是諸君有興趣及能力,能夠開發本身的YamlBeanDefinitionReaderKotlinBeanDefinitionReader等也未必不可)

Beans.BeanDefinitionReader

看似複雜的一張類關係圖,理解起來其實並不複雜

解析

BeanDefinitionReader定義了一系列bean初始化的接口,如下列出幾個比較直觀的

public interface BeanDefinitionReader {
  // 加載單個配置文件
  int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
  int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
  
  // 加載多個配置文件
  int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
  int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}

接口中定義了針對單個配置文件及多個配置文件的加載方法

Q:類名明明是 Reader,但方法名爲何是 load而不是 parse(加載和解析貌似不是一個概念)?

AbstractBeanDefinitionReader實現了公共的處理邏輯

XmlBeanDefinitionReader在解析的過程當中主要藉助了兩種接口,DocumentLoaderBeanDefinitionDocumentReader

  • DocumentLoader主要負責xml的驗證和讀取(默認實現爲DefaultDocumentLoader
  • BeanDefinitionDocumentReader主要負責Document的解析和註冊(默認實現爲DefaultBeanDefinitionDocumentReader

Beans.Active.XmlBeanDefinitionReader

若是以爲上圖過於簡單、不夠細節,能夠參考下圖(因爲Spring源碼的複雜性,依然不能覆蓋全部的細節)

Beans.Sequence.XmlBeanDefinitionReader

  • 橙色 部分爲驗證、讀取xml(Document、Element)
  • 藍色 部分爲Document的解析(BeanDefinitionBeanDefinitionHolder
  • 綠色 部分爲bean的註冊(BeanDefinitionRegistryAliasRegistry

以上,引出了幾個重要的新概念(BeanDefinitionBeanDefinitionHolderBeanFactoryBeanRegistry),客官莫急,待我慢慢道來

XML的驗證和讀取

xml的驗證和讀取能夠跟蹤到代碼XmlBeanDefinitionReader#doLoadDocument

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware());
}

documentLoader.loadDocument內部實現則是常規的xml文件到Document的讀取邏輯,這裏簡要介紹一下EntityResolver

通常Spring配置文件以下

<?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.xsd">
    <bean id="" class="">
        <property name="" value=""/>
        <property name="" ref=""/>
    </bean>
</beans>

在解析以前須要對xml的內容進行校驗,校驗須要依賴XSD或DTD聲明文件,如上http://www.springframework.or...(瀏覽器中可直接打開)

這裏有一個問題,基於Spring的應用不可能所有運行在可連通互聯網的環境中,若是沒有網絡環境,基於xml配置的Spring初始化均會失敗

EntityResolver的做用是提供一個尋找本地XSD或DTD聲明文件的方法,具體的過程再也不詳解,能夠跟到XmlBeanDefinitionReader#getEntityResolver查看

spring-beans的聲明文件都可在spring-beans模塊中找到

至此,可在離線的環境中獲取XSD或DTD文件,以驗證並解析xml配置文件,爲後續bean的解析作足準備

Document的解析

Document的解析、註冊主要使用了BeanDefinitionDocumentReader(由DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions跟入)

Profiles

在進行bean的解析以前,首先須要匹配==profiles==

什麼是profiles?

https://docs.spring.io/spring...

Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments.

簡單來說,咱們能夠爲不一樣的環境設置不一樣的配置,有選擇性的讓Spring加載

如何使用profiles?

xml文件中

<beans profile="dev"></beans>

SpringBoot properties文件名中

application-dev.properties

SpringBoot yaml文件中

server:
    port: 80
---
spring:
    profiles: dev
server:
    port: 8080

註解中

@Bean
@Profile({"dev"})
public MyBean myDevBean() {
    return new MyDevBean();
}

@Bean
@Profile({"production"})
public MyBean myProductionBean() {
    return new MyProductionBean();
}

等等方式...

如何激活profile?

java -jar xxx.jar -Dspring.profiles.active=dev

profile的配置會被讀取並記錄在Environment中,在進行bean的解析以前,第一步須要判斷當前beans的profile是否與Environment中記錄的profile相匹配,只有匹配的纔會被加載,並進入解析、註冊流程,這也便作到了配置的環境隔離(具體的處理過程參見DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions

解析

在spring-beans模塊的範疇內,默認的命名空間只包含四種標籤:<beans><bean><alias><import>,但在實際使用過程當中,咱們用到的遠遠不止這三種,好比<tx:advice><aop:config>等等,這些標籤的命名空間並不在==beans==內,前者的命名空間爲==tx==,XSD文件在spring-tx.jar!org/springframework/transaction/config/spring-tx.xsd3,後者的命名空間爲==aop==,XSD文件在spring-tx.jar!org/springframework/aop/config/spring-aop.xsd4

除了默認命名空間內的標籤外,其餘命名空間的標籤都是以一種擴展(自定義)的形式存在

// DefaultBeanDefinitionDocumentReader.java
/**
 * Parse the elements at the root level in the document:
 * "import", "alias", "bean".
 * @param root the DOM root element of the document
 */
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);
    }
}

不管默認標籤仍是自定義標籤,實際的標籤解析過程均由BeanDefinitionParserDelegate提供

默認命名空間(標籤)解析

默認命名空間的標籤無外乎<bean><alias><import>(以及<beans>嵌套)

// DefualtBeanDefinitionDocumentReader.java
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        // 解析 <import>
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        // 解析 <alias>
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        // 解析 <bean>
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // 遞歸解析 <beans> 嵌套
        doRegisterBeanDefinitions(ele);
    }
}

對於<import>標籤,解析出location,處理佔位符,遞歸調用loadBeanDefinitions進行解析註冊

對於<alias>標籤,解析出bean的namealias,並註冊(AliasRegistry

對於<bean>標籤,狀況則會複雜不少,但也無外乎<bean> 屬性解析、<constructor-arg>解析、<property>解析等等

具體的解析細節再也不贅述,你們能夠自行查看源碼

// BeanDefinitionParserDelegate#parseBeanDefinitionElement
try {
    // 建立BeanDefinition
    AbstractBeanDefinition bd = createBeanDefinition(className, parent);

    // <bean>屬性
    parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
    // <description>
    bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));

    // <meta>
    parseMetaElements(ele, bd);
    // <lookup-method>
    parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
    // <replaced-method>
    parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

    // <constructor-arg>
    parseConstructorArgElements(ele, bd);
    // <property>
    parsePropertyElements(ele, bd);
    // <qualifier>
    parseQualifierElements(ele, bd);

    bd.setResource(this.readerContext.getResource());
    bd.setSource(extractSource(ele));

    return bd;
}

標籤解析以後的結果存放到了那裏?BeanDefinition

BeanDefinition只是bean的定義,存放構造該bean實例所須要的元信息,其中包含你能想到的一切有關bean的屬性信息

// BeanDefinition.java
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    // class name
     String getBeanClassName();
    
    // 構造函數
    ConstructorArgumentValues getConstructorArgumentValues();

    // 屬性
    MutablePropertyValues getPropertyValues();

    // 初始化的方法名
    String getInitMethodName();
    
    // 銷燬的方法名
    String getDestroyMethodName();
    
    // ...
}

除此以外,還有不少的輔助類,如

  • RuntimeBeanReference存儲ref信息
  • TypedStringValue存儲value信息
  • ManagedMap存儲<map>信息
  • ManagedList儲存<list>信息
  • 等等

至此就結束了麼?固然不是,咱們在配置bean的時候通常都會使用id來指定beanName,但若是沒有指定id呢?

<bean id="myBean" name="aliasBean" class=""></bean>

此時,則會使用BeanNameGenerator(默認DefaultBeanNameGenerator)生成默認的beanName(參見BeanDefinitionParserDelegate#parseBeanDefinitionElement

隨後,會將beanDefinition、beanName及別名alias一同包裝進BeanDefinitionHolder

BeanDefinitionHolder的定義只是簡單捆綁了以上三者的關係

public class BeanDefinitionHolder implements BeanMetadataElement {
    // bean definition
    private final BeanDefinition beanDefinition;

    // bean name
    private final String beanName;

    // 別名alias
    private final String[] aliases;
}

最後,將該BeanDefinition進行註冊(BeanDefinitionRegistry

// DefaultBeanDefinitionDocumentReader.java
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    // 解析BeanDefinitionHolder
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        // 註冊BeanDefinition
        BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
        // 發送事件
        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }
}

自定義命名空間(標籤)解析

Spring提供了一種能力,來擴展並自定義xml配置文件中的標籤

自定義標籤的實現須要依賴:

  • XSD文件,用來描述(驗證)自定義標籤
  • BeanDefinitionParser實現,用來解析自定義標籤
  • NamespaceHandlerSupport實現,用來註冊自定義標籤解析器
  • spring.handlersspring.schemas,用來發現自定義標籤註冊器(NamespaceHandlerSupport)及XSD文件

BeanDefinitionParser的定義一樣很簡單,將一個Document下的Element解析爲BeanDefinition(s),並註冊

public interface BeanDefinitionParser {
    BeanDefinition parse(Element element, ParserContext parserContext);
}

component-scan爲例,其命名空間爲context

BeanDefinitionParser的實現類爲ComponentScanBeanDefinitionParser

public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
    @Override
    @Nullable
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        String basePackage = /* 解析basePackage */;
        // 掃描BeanDefinition
        ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
        Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
        // 註冊BeanDefinition
        registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

        return null;
    }
}

其中定義瞭如何掃描指定package下的類,並根據註解註冊bean的邏輯

這裏有一個有趣的發現,經過配置文件註冊bean的實現類一般爲xxxBeanDefinitionLoader,而經過註解註冊bean的實現類一般爲xxxBeanDefinitionScanner

NamespaceHandlerSupport的實現類爲ContextNamespaceHandler

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // ...
        // 註冊<component-scan>的解析器
        registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
        // ...
    }
}

spring.handlers文件部份內容爲

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler

spring.schemas文件部份內容爲

http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd

同理,能夠找到<tx:advice><aop:config>等標籤的解析邏輯

回到BeanDefinitionParserDelegate,細心的會發現,對於自定義標籤的解析邏輯,並無出現註冊的地方

// BeanDefinitionParserDelegate.java
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // 獲取命名空間
    String namespaceUri = getNamespaceURI(ele);
    // 獲取解析器
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    // 解析(Q: 並註冊?)
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

對於大多數自定義標籤而言,其都是自注冊的,即在BeanDefinitionParser.parse中完成註冊的邏輯,如<component-scan>,那對於<tx:advice>之類的標籤是如何完成自注冊的?AbstractBeanDefinitionParser!

// AbstractBeanDefinitionParser
public abstract class AbstractBeanDefinitionParser implements BeanDefinitionParser {
    @Override
    @Nullable
    public final BeanDefinition parse(Element element, ParserContext parserContext) {
        // Element解析爲BeanDefinition
        AbstractBeanDefinition definition = parseInternal(element, parserContext);
        // 解析id及aliases,包裝爲BeanDefinitionHolder    
        BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
        // 註冊!!!
        registerBeanDefinition(holder, parserContext.getRegistry());
        // 發送事件
        if (shouldFireEvents()) {
            BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
            postProcessComponentDefinition(componentDefinition);
            parserContext.registerComponent(componentDefinition);
        }
        return definition;
    }
}

AbstractBeanDefinitionParser實現了相似於ComponentScanBeanDefinitionParser的邏輯,在解析出BeanDefinition後隨即將其註冊

註冊

上文中屢次提到了註冊

  • 解析<alias>標籤並註冊
  • 解析<bean>標籤並註冊
  • 解析自定義標籤並註冊
註冊的具體細節但願能和bean的初始化及獲取放到一塊兒來說,本篇簡單介紹並引出幾個概念

bean的註冊由xxxBeanDefinitionRegistry實現,並非註冊bean的實例對象,而是註冊bean的元數據BeanDefinition
別名的註冊由xxxAliasRegistry實現
bean的獲取由xxxBeanFactory實現(包括bean的初始化、依賴注入等)

BeanDefinition的實現有多種,各中關係後續文章詳解

Beans.BeanDefinition

xxxBeanDefinitionRegistryxxxBeanFactory的實現也是錯綜複雜,篇幅有限,統一放到後續文章中講解

Beans.ListableBeanFactory

小結

  • Spring bean的註冊大致分爲兩步:解析;註冊
  • Spring bean的解析方式有不少種,但無外乎三大類:文件配置;註解配置;硬編碼
  • XmlBeanDefinitionReader負責xml配置的bean解析及註冊

    • DocumentLoader負責xml文件的校驗及加載
    • BeanDefinitionDocumentReader負責bean的解析及註冊
  • xml中的標籤分爲兩大類,默認標籤及自定義標籤,其解析邏輯不一樣

    • 全部標籤的解析由BeanDefinitionParserDelegate提供統一入口
    • Spring提供了擴展xml自定義標籤的能力,須要實現BeanDefinitionParserNamespaceHandlerSupport
  • bean的元數據被存放在BeanDefinition中,並同beanName、別名alias封裝在BeanDefinitionHolder

    • bean的註冊由xxxBeanDefinitionRegistry實現
    • 別名的註冊由xxxAliasRegistry實現

問題遺留點

  • bean及別名的註冊是如何實現的
  • bean的獲取(初始化、依賴注入)是如何實現的
  • 使用xml配置文件能夠加載基於註解配置的bean(如<component-scan>),使用註解一樣能夠加載基於xml配置的bean(如@Import({"classpath:xxx.xml"})),二者之間是如何相融合的
  • bean的註冊都伴隨事件發送(ReaderContext#fireXXX),事件在哪裏使用
寫在最後~

2019結束了,但願2020能有新的突破!


訂閱號


  1. https://github.com/spring-pro...
  2. https://github.com/spring-pro...
  3. https://github.com/spring-pro...
  4. https://github.com/spring-pro...
相關文章
相關標籤/搜索