Spring IoC BeanDefinition 的加載和註冊

前言

本系列所有基於 Spring 5.2.2.BUILD-SNAPSHOT 版本。由於 Spring 整個體系太過於龐大,因此只會進行關鍵部分的源碼解析。java

本篇文章主要介紹 Spring IoC 容器怎麼加載 bean 的定義元信息。node

下圖是一個大體的流程圖:git

正文

首先定義兩個簡單的 POJO,以下:github

public class User {

    private Long id;
    private String name;
	
    private City city;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public City getCity() {
        return city;
    }

    public void setCity(City city) {
        this.city = city;
    }

    @Override
    public String toString() {
        return "User{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", city=" + city +
            '}';
    }
    
}
public class City {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "City{" +
            "id=" + id +
            ", name='" + name + '\'' +
            '}';
    }
}

再編寫一個 XML 文件。web

<?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="user" class="com.leisurexi.ioc.domain.User">
        <property name="id" value="1"/>
        <property name="name" value="leisurexi"/>
        <property name="city" ref="city"/>
    </bean>
	
    <bean id="city" class="com.leisurexi.ioc.domain.City">
        <property name="id" value="1"/>
        <property name="name" value="beijing"/>
    </bean>

</beans>

最後再來一個測試類。spring

public class BeanDefinitionDemo {

    public static void main(String[] args) {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions("META-INF/bean-definition.xml");
        User user = beanFactory.getBean("user", User.class);
        System.err.println(user);
    }

}

上面這段代碼比較簡單,無非就是聲明 bean 工廠,而後經過指定的 XML 文件加載 bean 的定義元信息,最後經過 bean 工廠獲取 bean。接下來介紹上面代碼中的2個核心類 DefaultListableBeanFactoryXmlBeanDefinitionReader緩存

DefaultListableBeanFactory

下面是該類的類圖及層次結構:dom

  • AliasRegistry:定義對 alias 的簡單增刪改等操做。
  • SimpleAliasRegistry:主要使用 map 做爲 alias 的緩存,並對接口 AliasRegistry 進行實現。
  • SingletonBeanRegistry:定義了對單例 bean 的註冊及獲取。
  • BeanFactory:定義獲取單個 beanbean 的各類屬性。
  • DefaultSingletonBeanRegistry:對接口 SingletonBeanRegistry 各函數的實現。
  • HierarchicalBeanFactory:繼承 BeanFactory,也就是在 BeanFactory 定義的功能的基礎上增長了對 parentBeanFactory 的支持。
  • BeanDefinitionRegistry:定義了對 BeanDefinition 的各類增刪改操做。
  • FactoryBeanRegistrySupport:DefaultSingletonBeanRegistry 基礎上增長了對 FactoryBean 的特殊處理功能。
  • ConfigurableBeanFactory:提供配置 BeanFactory 的各類方法。
  • ListableBeanFactory:繼承 BeanFactory 提供了獲取多個 bean 的各類方法。
  • AbstractBeanFactory:綜合 FactoryBeanRegistrySupportConfigurableBeanFactory 的功能。
  • AutowireCapableBeanFactory:提供建立 bean、自動注入、初始化以及應用 bean 的後處理器。
  • AbstractAutowireCapableBeanFactory:綜合 AbstractBeanFactory 並對接口 AutowireCapableBeanFactory 進行實現。
  • ConfigurableListableBeanFactory:BeanFactory 配置清單,指定忽略類型及接口等。
  • DefaultListableBeanFactory:綜合上面全部功能,主要是對 bean 註冊後的處理。

能夠看到上面的接口大多數是定義了一些功能或在父接口上擴展了一些功能,DefaultListableBeanFactory 實現了全部接口,大多數默認狀況下咱們所使用的 beanFactory 就是 DefaultListableBeanFactoryide

下面咱們就開始分析 Spring 是如何解析 XML 文件,並讀取其中的內容的。函數

AbstractBeanDefinitionReader#loadBeanDefinitions

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
    // 獲取resourceLoader,這邊是PathMatchingResourcePatternResolver
    ResourceLoader resourceLoader = getResourceLoader();
    if (resourceLoader == null) {
        throw new BeanDefinitionStoreException("Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
    }
    // 判斷resourceLoader是不是ResourcePatternResolver,咱們這邊是符合的
    if (resourceLoader instanceof ResourcePatternResolver) {
        try {
            // 根據路徑獲取所欲符合的配置文件並封裝成Resource對象
            Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
            // 根據Resource加載bean定義,並返回數量
            int count = loadBeanDefinitions(resources);
            if (actualResources != null) {
                Collections.addAll(actualResources, resources);
            }
            return count;
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                "Could not resolve bean definition resource pattern [" + location + "]", ex);
        }
    }
    else {
        // 只能經過絕對路徑加載單個資源
        Resource resource = resourceLoader.getResource(location);
        // 根據Resource加載bean的定義,並返回數量
        int count = loadBeanDefinitions(resource);
        if (actualResources != null) {
            actualResources.add(resource);
        }
        return count;
    }
}

public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
    Assert.notNull(resources, "Resource array must not be null");
    int count = 0;
    for (Resource resource : resources) {
        // 調用具體實現類的方法,加載 BeanDefinition,並返回數量,見下文詳解
        count += loadBeanDefinitions(resource);
    }
    return count;
}

上面方法主要是將資源文件轉換爲 Resource 對象,而後調用 loadBeanDefinitions(Resource...) 加載 BeanDefinition

XmlBeanDefinitionReader#loadBeanDefinitions

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    // 將Resource封裝成EncodedResource,也就是對資源指定編碼和字符集
    return loadBeanDefinitions(new EncodedResource(resource));
}

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    // 當前正在加載的EncodedResource,第一次加載的話這裏是空的
    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    if (currentResources == null) {
        currentResources = new HashSet<>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    // 若是當前encodedResource已經存在,表明出現了循環加載,拋出異常
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }
    try {
        // 獲取Resource的輸入流
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            // 將inputStream封裝成org.xml.sax.InputSource
            InputSource inputSource = new InputSource(inputStream);
            // 若是encodedResource的編碼不爲空,設置inputSource的編碼
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            // 加載bean定義(方法以do開頭,真正處理的方法),見下文詳解
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        finally {
            // 關閉流
            inputStream.close();
        }
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException(
            "IOException parsing XML document from " + encodedResource.getResource(), ex);
    }
    finally {
        // 當前資源以及加載完畢,從currentResources中移除
        currentResources.remove(encodedResource);
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}

上面方法主要將 Resource 封裝成 EncodedResource ,也就是制定資源的編碼和字符集。而後獲取 Resource 的輸入流 InputStream ,並封裝成 InputSource 設置其編碼,最終調用 doLoadBeanDefinitions 開始真正的加載流程。

XmlBeanDefinitionReader#doLoadBeanDefinitions

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
    try {
        // 根據 inputSource 和 resource 加載 XML 文件,並封裝成 Document
        Document doc = doLoadDocument(inputSource, resource);
        // 用 doc 去解析和註冊 bean definition,見下文詳解
        int count = registerBeanDefinitions(doc, resource);
        return count;
    }
    // 省略異常處理
}

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

protected int getValidationModeForResource(Resource resource) {
    // 若是手動指定了驗證模式則使用指定的驗證模式
    int validationModeToUse = getValidationMode();
    if (validationModeToUse != VALIDATION_AUTO) {
        return validationModeToUse;
    }
    // 若是未指定則使用自動檢測,其實就是判斷文件是否包含 DOCTYPE
    int detectedMode = detectValidationMode(resource);
	if (detectedMode != VALIDATION_AUTO) {
        return detectedMode;
    }
    // 若是沒有找到驗證,默認使用 XSD 模式,由於 DTD 已經不維護了
    return VALIDATION_XSD;
}

// DefaultDocumentLoader.java
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
    // 建立DocumentBuilderFactory
    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    // 建立DocumentBuilder
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    // 解析inputSource並返回Document對象
    return builder.parse(inputSource);
}

detectValidationMode() 方法其實就是讀取文件內容,判斷是否包含 DOCTYPE,若是包含就是 DTD 不然就是 XSD。

獲取 XML 配置文件的驗證模式。XML 文件的驗證模式是用來保證 XML 文件的正確性,常見的驗證模式有 DTD 和 XSD。

DTD XML 格式示例:

XSD XML 格式示例:

XmlBeanDefinitionReader#registerBeanDefinitions

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // 獲取DefaultBeanDefinitionDocumentReader
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // 獲取註冊中心,再靠註冊中心獲取註冊以前以及註冊過的BeanDefinition數量
    int countBefore = getRegistry().getBeanDefinitionCount();
    // 解析並註冊BeanDefinition,見下文詳解
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // 獲取註冊事後BeanDefinition數量減去註冊以前的數量,獲得的就是本次註冊的數量
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

這裏的 getRegistry() 方法返回的就是咱們經過構造函數傳入的 DefaultListableBeanFactory ,基本上這裏都是 DefaultListableBeanFactory,由於就只有它實現了 BeanDefinitionRegistry 接口。

DefaultListableBeanFactory 中定義了存放 BeanDefinition 的緩存,以下:

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
		implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
    // 存放BeanDefinition的緩存,key爲 bean的名稱,value就是其BeanDefinition
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
}

因此 getBeanDefinitionCount() 方法返回的就是 beanDefinitionMap 中元素的數量。

DefaultBeanDefinitionDoucumentReader#registerBeanDefinitions

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    // 提取 root,註冊 BeanDefinition (理論上 Spring 的配置文件,root 都應該是 beans 標籤)
    doRegisterBeanDefinitions(doc.getDocumentElement());
}

protected void doRegisterBeanDefinitions(Element root) {
    BeanDefinitionParserDelegate parent = this.delegate;
    // 專門處理解析
    this.delegate = createDelegate(getReaderContext(), root, parent);
    // 校驗root節點的命名空間是否爲默認的命名空間(默認命名空間http://www.springframework.org/schema/beans)
    if (this.delegate.isDefaultNamespace(root)) {
        // 處理 profile 屬性
        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);

    this.delegate = parent;
}

profile 主要是用於多環境開發,例如:

集成到 Web 環境時,在 web.xml 中加入如下代碼:

<coontext-param>
    <param-name>Spring.profiles.active</param-name>
    <param-value>dev</param-value>
</coontext-param>

preProcessXml()postProcessXml() 採用的 模板方法模式,子類能夠DefaultBeanDefinitionDoucumentReader 來重寫這兩個方法,這也是解析先後的擴展點。

DefaultBeanDefinitionDoucumentReader#parseBeanDefinitions

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // 校驗root節點的命名空間是否爲默認的命名空間,這裏爲何再次效驗,由於調用解析前調用了preProcessXml()方法,可能會對節點作修改
    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)) {
                    // 默認命名空間節點的處理,例如<bean id="..." class="..."/>
                    parseDefaultElement(ele, delegate);
                }
                else {
                    // 自定義命名空間節點的處理,例如<context:compoent-scan/>、<aop:aspectj-autoproxy>
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        // 自定義命名空間節點的處理
        delegate.parseCustomElement(root);
    }
}

關於 默認命名空間節點 的處理 和 自定義命名空間節點 的處理,會在後續文章一一分析。

總結

本篇文章對 Spring 解析 XML 文件具體節點前的準備工做作了簡要分析,可是對 Spring 的資源管理(Resource)沒有作過多介紹,有興趣的小夥伴能夠自行去研究一下。

我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring

參考

相關文章
相關標籤/搜索