1. 程式人生 > >spring原始碼深度解析— IOC 之 容器的基本實現

spring原始碼深度解析— IOC 之 容器的基本實現

概述

上一篇我們搭建完Spring原始碼閱讀環境,spring原始碼深度解析—Spring的整體架構和環境搭建 這篇我們開始真正的閱讀Spring的原始碼,分析spring的原始碼之前我們先來簡單回顧下spring核心功能的簡單使用

容器的基本用法

bean是spring最核心的東西,spring就像是一個大水桶,而bean就是水桶中的水,水桶脫離了水也就沒有什麼用處了,我們簡單看下bean的定義,程式碼如下:

package com.chenhao.spring;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 10:35 2019/6/19
 * @Modified by:
 */
public class MyTestBean {
    private String name = "ChenHao";
public String getName() {
        return name;
    }

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

原始碼很簡單,bean沒有特別之處,spring的的目的就是讓我們的bean成為一個純粹的的POJO,這就是spring追求的,接下來就是在配置檔案中定義這個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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myTestBean" class="com.chenhao.spring.MyTestBean"/>

</beans>

在上面的配置中我們可以看到bean的宣告方式,在spring中的bean定義有N種屬性,但是我們只要像上面這樣簡單的宣告就可以使用了。 
具體測試程式碼如下:

import com.chenhao.spring.MyTestBean;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author: ChenHao
 * @Description:
 * @Date: Created in 10:36 2019/6/19
 * @Modified by:
 */
public class AppTest {
    @Test
    public void MyTestBeanTest() {
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml"));
        MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean");
        System.out.println(myTestBean.getName());
    }
}

執行上述測試程式碼就可以看到輸出結果如下圖: 

其實直接使用BeanFactory作為容器對於Spring的使用並不多見,因為企業級應用專案中大多會使用的是ApplicationContext(後面我們會講兩者的區別,這裡只是測試)

功能分析

接下來我們分析2中程式碼完成的功能;
- 讀取配置檔案spring-config.xml。
- 根據spring-config.xml中的配置找到對應的類的配置,並例項化。
- 呼叫例項化後的例項
下圖是一個最簡單spring功能架構,如果想完成我們預想的功能,至少需要3個類: 

ConfigReader :用於讀取及驗證自己直檔案 我們妥用配直檔案裡面的東西,當然首先 要做的就是讀取,然後放直在記憶體中.

ReflectionUtil :用於根據配置檔案中的自己直進行反射例項化,比如在上例中 spring-config.xml 出現的<bean id="myTestBean" class="com.chenhao.spring.MyTestBean"/>,我們就可以根據 com.chenhao.spring.MyTestBean 進行例項化。

App :用於完成整個邏輯的串聯。

 工程搭建

spring的原始碼中用於實現上面功能的是spring-bean這個工程,所以我們接下來看這個工程,當然spring-core是必須的。

beans包的層級結構

閱讀原始碼最好的方式是跟著示例操作一遍,我們先看看beans工程的原始碼結構,如下圖所示: 

 

- src/main/java 用於展現Spring的主要邏輯 
- src/main/resources 用於存放系統的配置檔案 
- src/test/java 用於對主要邏輯進行單元測試 
- src/test/resources 用於存放測試用的配置檔案

核心類介紹

接下來我們先了解下spring-bean最核心的兩個類:DefaultListableBeanFactory和XmlBeanDefinitionReader

DefaultListableBeanFactory

XmlBeanFactory繼承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整個bean載入的核心部分,是Spring註冊及載入bean的預設實現,而對於XmlBeanFactory與DefaultListableBeanFactory不同的地方其實是在XmlBeanFactory中使用了自定義的XML讀取器XmlBeanDefinitionReader,實現了個性化的BeanDefinitionReader讀取,DefaultListableBeanFactory繼承了AbstractAutowireCapableBeanFactory並實現了ConfigurableListableBeanFactory以及BeanDefinitionRegistry介面。以下是ConfigurableListableBeanFactory的層次結構圖以下相關類圖

上面類圖中各個類及介面的作用如下:
- AliasRegistry:定義對alias的簡單增刪改等操作
- SimpleAliasRegistry:主要使用map作為alias的快取,並對介面AliasRegistry進行實現
- SingletonBeanRegistry:定義對單例的註冊及獲取
- BeanFactory:定義獲取bean及bean的各種屬性
- DefaultSingletonBeanRegistry:預設對介面SingletonBeanRegistry各函式的實現
- HierarchicalBeanFactory:繼承BeanFactory,也就是在BeanFactory定義的功能的基礎上增加了對parentFactory的支援
- BeanDefinitionRegistry:定義對BeanDefinition的各種增刪改操作
- FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基礎上增加了對FactoryBean的特殊處理功能
- ConfigurableBeanFactory:提供配置Factory的各種方法
- ListableBeanFactory:根據各種條件獲取bean的配置清單
- AbstractBeanFactory:綜合FactoryBeanRegistrySupport和ConfigurationBeanFactory的功能
- AutowireCapableBeanFactory:提供建立bean、自動注入、初始化以及應用bean的後處理器
- AbstractAutowireCapableBeanFactory:綜合AbstractBeanFactory並對介面AutowireCapableBeanFactory進行實現
- ConfigurableListableBeanFactory:BeanFactory配置清單,指定忽略型別及介面等
- DefaultListableBeanFactory:綜合上面所有功能,主要是對Bean註冊後的處理
XmlBeanFactory對DefaultListableBeanFactory類進行了擴充套件,主要用於從XML文件中讀取BeanDefinition,對於註冊及獲取Bean都是使用從父類DefaultListableBeanFactory繼承的方法去實現,而唯獨與父類不同的個性化實現就是增加了XmlBeanDefinitionReader型別的reader屬性。在XmlBeanFactory中主要使用reader屬性對資原始檔進行讀取和註冊

XmlBeanDefinitionReader

XML配置檔案的讀取是Spring中重要的功能,因為Spring的大部分功能都是以配置作為切入點的,可以從XmlBeanDefinitionReader中梳理一下資原始檔讀取、解析及註冊的大致脈絡,首先看看各個類的功能

ResourceLoader:定義資源載入器,主要應用於根據給定的資原始檔地址返回對應的Resource
BeanDefinitionReader:主要定義資原始檔讀取並轉換為BeanDefinition的各個功能
EnvironmentCapable:定義獲取Environment方法
DocumentLoader:定義從資原始檔載入到轉換為Document的功能
AbstractBeanDefinitionReader:對EnvironmentCapable、BeanDefinitionReader類定義的功能進行實現
BeanDefinitionDocumentReader:定義讀取Document並註冊BeanDefinition功能
BeanDefinitionParserDelegate:定義解析Element的各種方法
整個XML配置檔案讀取的大致流程,在XmlBeanDefinitionReader中主要包含以下幾步處理 

(1)通過繼承自AbstractBeanDefinitionReader中的方法,來使用ResourceLoader將資原始檔路徑轉換為對應的Resource檔案
(2)通過DocumentLoader對Resource檔案進行轉換,將Resource檔案轉換為Document檔案
(3)通過實現介面BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader類對Document進行解析,並使用BeanDefinitionParserDelegate對Element進行解析

容器的基礎XmlBeanFactory

 通過上面的內容我們對spring的容器已經有了大致的瞭解,接下來我們詳細探索每個步驟的詳細實現,接下來要分析的功能都是基於如下程式碼:

BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml"));

首先呼叫ClassPathResource的建構函式來構造Resource資原始檔的例項物件,這樣後續的資源處理就可以用Resource提供的各種服務來操作了。有了Resource後就可以對BeanFactory進行初始化操作,那配置檔案是如何封裝的呢? 

配置檔案的封裝 

 Spring的配置檔案讀取是通過ClassPathResource進行封裝的,Spring對其內部使用到的資源實現了自己的抽象結構:Resource介面來封裝底層資源,如下原始碼:

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
    boolean exists();
    default boolean isReadable() {
        return true;
    }
    default boolean isOpen() {
        return false;
    }
    default boolean isFile() {
        return false;
    }
    URL getURL() throws IOException;
    URI getURI() throws IOException;
    File getFile() throws IOException;
    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();
    String getDescription();
}

 InputStreamSource封裝任何能返回InputStream的類,比如File、Classpath下的資源和Byte Array等, 它只有一個方法定義:getInputStream(),該方法返回一個新的InputStream物件 。

Resource介面抽象了所有Spring內部使用到的底層資源:File、URL、Classpath等。首先,它定義了3個判斷當前資源狀態的方法:存在性(exists)、可讀性(isReadable)、是否處於開啟狀態(isOpen)。另外,Resource介面還提供了不同資源到URL、URI、File型別的轉換,以及獲取lastModified屬性、檔名(不帶路徑資訊的檔名,getFilename())的方法,為了便於操作,Resource還提供了基於當前資源建立一個相對資源的方法:createRelative(),還提供了getDescription()方法用於在錯誤處理中的列印資訊。
對不同來源的資原始檔都有相應的Resource實現:檔案(FileSystemResource)、Classpath資源(ClassPathResource)、URL資源(UrlResource)、InputStream資源(InputStreamResource)、Byte陣列(ByteArrayResource)等,相關類圖如下所示: 

在日常開發中我們可以直接使用spring提供的類來載入資原始檔,比如在希望載入資原始檔時可以使用下面的程式碼:

Resource resource = new ClassPathResource("spring-config.xml");
InputStream is = resource.getInputStream();

有了 Resource 介面便可以對所有資原始檔進行統一處理 至於實現,其實是非常簡單的,以 getlnputStream 為例,ClassPathResource 中的實現方式便是通 class 或者 classLoader 提供的底層方法進行呼叫,而對於 FileSystemResource 其實更簡單,直接使用 FileInputStream 對檔案進行例項化。

ClassPathResource.java

InputStream is;
if (this.clazz != null) {
    is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
    is = this.classLoader.getResourceAsStream(this.path);
}
else {
    is = ClassLoader.getSystemResourceAsStream(this.path);
}

FileSystemResource.java

public InputStream getinputStream () throws IOException {
    return new FilelnputStream(this file) ; 
}

當通過Resource相關類完成了對配置檔案進行封裝後,配置檔案的讀取工作就全權交給XmlBeanDefinitionReader來處理了。
接下來就進入到XmlBeanFactory的初始化過程了,XmlBeanFactory的初始化有若干辦法,Spring提供了很多的建構函式,在這裡分析的是使用Resource例項作為建構函式引數的辦法,程式碼如下:

XmlBeanFactory.java

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

上面函式中的程式碼this.reader.loadBeanDefinitions(resource)才是資源載入的真正實現,但是在XmlBeanDefinitionReader載入資料前還有一個呼叫父類建構函式初始化的過程:super(parentBeanFactory),我們按照程式碼層級進行跟蹤,首先跟蹤到如下父類程式碼:

public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
    super(parentBeanFactory);
}

然後繼續跟蹤,跟蹤程式碼到父類AbstractAutowireCapableBeanFactory的建構函式中:

public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
    this();
    setParentBeanFactory(parentBeanFactory);
}
public AbstractAutowireCapableBeanFactory() {
    super();
    ignoreDependencyInterface(BeanNameAware.class);
    ignoreDependencyInterface(BeanFactoryAware.class);
    ignoreDependencyInterface(BeanClassLoaderAware.class);
}

這裡有必要提及 ignoreDependencylnterface方法,ignoreDependencylnterface  的主要功能是 忽略給定介面的向動裝配功能,那麼,這樣做的目的是什麼呢?會產生什麼樣的效果呢?

舉例來說,當 A 中有屬性 B ,那麼當 Spring 在獲取 A的 Bean 的時候如果其屬性 B 還沒有 初始化,那麼 Spring 會自動初始化 B,這也是 Spring 提供的一個重要特性 。但是,某些情況 下, B不會被初始化,其中的一種情況就是B 實現了 BeanNameAware 介面 。Spring 中是這樣介紹的:自動裝配時忽略給定的依賴介面,典型應用是邊過其他方式解析 Application 上下文註冊依賴,類似於 BeanFactor 通過 BeanFactoryAware 進行注入或者 ApplicationContext 通過 ApplicationContextAware 進行注入。

呼叫ignoreDependencyInterface方法後,被忽略的介面會儲存在BeanFactory的名為ignoredDependencyInterfaces的Set集合中:

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {

    private final Set<Class<?>> ignoredDependencyInterfaces = new HashSet<>();
    
    public void ignoreDependencyInterface(Class<?> ifc) {
        this.ignoredDependencyInterfaces.add(ifc);
    }
...
}

ignoredDependencyInterfaces集合在同類中被使用僅在一處——isExcludedFromDependencyCheck方法中:

protected boolean isExcludedFromDependencyCheck(PropertyDescriptor pd) {
    return (AutowireUtils.isExcludedFromDependencyCheck(pd) || this.ignoredDependencyTypes.contains(pd.getPropertyType()) || AutowireUtils.isSetterDefinedInInterface(pd, this.ignoredDependencyInterfaces));
}

而ignoredDependencyInterface的真正作用還得看AutowireUtils類的isSetterDefinedInInterface方法。

public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set<Class<?>> interfaces) {
    //獲取bean中某個屬性物件在bean類中的setter方法
    Method setter = pd.getWriteMethod();
    if (setter != null) {
        // 獲取bean的型別
        Class<?> targetClass = setter.getDeclaringClass();
        for (Class<?> ifc : interfaces) {
            if (ifc.isAssignableFrom(targetClass) && // bean型別是否介面的實現類
                ClassUtils.hasMethod(ifc, setter.getName(), setter.getParameterTypes())) { // 介面是否有入參和bean型別完全相同的setter方法
                return true;
            }
        }
    }
    return false;
}

ignoredDependencyInterface方法並不是讓我們在自動裝配時直接忽略實現了該介面的依賴。這個方法的真正意思是忽略該介面的實現類中和介面setter方法入參型別相同的依賴。
舉個例子。首先定義一個要被忽略的介面。

public interface IgnoreInterface {

    void setList(List<String> list);

    void setSet(Set<String> set);
}

然後需要實現該介面,在實現類中注意要有setter方法入參相同型別的域物件,在例子中就是List<String>和Set<String>。

public class IgnoreInterfaceImpl implements IgnoreInterface {

    private List<String> list;
    private Set<String> set;

    @Override
    public void setList(List<String> list) {
        this.list = list;
    }

    @Override
    public void setSet(Set<String> set) {
        this.set = set;
    }

    public List<String> getList() {
        return list;
    }

    public Set<String> getSet() {
        return set;
    }
}

定義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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd"
default-autowire="byType">


    <bean id="list" class="java.util.ArrayList">
        <constructor-arg>
            <list>
                <value>foo</value>
                <value>bar</value>
            </list>
        </constructor-arg>
    </bean>

    <bean id="set" class="java.util.HashSet">
        <constructor-arg>
            <list>
                <value>foo</value>
                <value>bar</value>
            </list>
        </constructor-arg>
    </bean>

    <bean id="ii" class="com.chenhao.ignoreDependency.IgnoreInterfaceImpl"/>
    <bean class="com.chenhao.autowire.IgnoreAutowiringProcessor"/>
</beans>

最後呼叫ignoreDependencyInterface:

beanFactory.ignoreDependencyInterface(IgnoreInterface.class);

執行結果:
null
null
而如果不呼叫ignoreDependencyInterface,則是:
[foo, bar]
[bar, foo]

我們最初理解是在自動裝配時忽略該介面的實現,實際上是在自動裝配時忽略該介面實現類中和setter方法入參相同的型別,也就是忽略該介面實現類中存在依賴外部的bean屬性注入。

典型應用就是BeanFactoryAware和ApplicationContextAware介面。
首先看該兩個介面的原始碼:

public interface BeanFactoryAware extends Aware {
    void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}

public interface ApplicationContextAware extends Aware {
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

在Spring原始碼中在不同的地方忽略了該兩個介面:

beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);

使得我們的BeanFactoryAware介面實現類在自動裝配時不能被注入BeanFactory物件的依賴:

public class MyBeanFactoryAware implements BeanFactoryAware {
    private BeanFactory beanFactory; // 自動裝配時忽略注入

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public BeanFactory getBeanFactory() {
        return beanFactory;
    }
}

ApplicationContextAware介面實現類中的ApplicationContext物件的依賴同理:

public class MyApplicationContextAware implements ApplicationContextAware {
    private ApplicationContext applicationContext; // 自動裝配時被忽略注入

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}
這樣的做法使得ApplicationContextAware和BeanFactoryAware中的ApplicationContext或BeanFactory依賴在自動裝配時被忽略,而統一由框架設定依賴,如ApplicationContextAware介面的設定會在ApplicationContextAwareProcessor類中完成:
private void invokeAwareInterfaces(Object bean) {
    if (bean instanceof Aware) {
        if (bean instanceof EnvironmentAware) {
            ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
        }
        if (bean instanceof EmbeddedValueResolverAware) {
            ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
        }
        if (bean instanceof ResourceLoaderAware) {
            ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
        }
        if (bean instanceof ApplicationEventPublisherAware) {
            ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
        }
        if (bean instanceof MessageSourceAware) {
            ((MessageSourceAware) bean).setMessageSource(this.applicationContext);
        }
        if (bean instanceof ApplicationContextAware) {
            ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
        }
    }
}

通過這種方式保證了ApplicationContextAware和BeanFactoryAware中的容器保證是生成該bean的容器。

bean載入

在之前XmlBeanFactory建構函式中呼叫了XmlBeanDefinitionReader型別的reader屬性提供的方法this.reader.loadBeanDefinitions(resource),而這句程式碼則是整個資源載入的切入點,這個方法的時序圖如下: 

我們來梳理下上述時序圖的處理過程:
(1)封裝資原始檔。當進入XmlBeanDefinitionReader後首先對引數Resource使用EncodedResource類進行封裝
(2)獲取輸入流。從Resource中獲取對應的InputStream並構造InputSource
(3)通過構造的InputSource例項和Resource例項繼續呼叫函式doLoadBeanDefinitions,loadBeanDefinitions函式具體的實現過程: 

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (logger.isTraceEnabled()) {
        logger.trace("Loading XML bean definitions from " + encodedResource);
    }

    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    if (currentResources == null) {
        currentResources = new HashSet<>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
                "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }
    try {
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            InputSource inputSource = new InputSource(inputStream);
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        finally {
            inputStream.close();
        }
    }
    ...
}

EncodedResource的作用是對資原始檔的編碼進行處理的,其中的主要邏輯體現在getReader()方法中,當設定了編碼屬性的時候Spring會使用相應的編碼作為輸入流的編碼,在構造好了encodeResource物件後,再次轉入了可複用方法loadBeanDefinitions(new EncodedResource(resource)),這個方法內部才是真正的資料準備階段,程式碼如下:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
        throws BeanDefinitionStoreException {
    try {
        // 獲取 Document 例項
        Document doc = doLoadDocument(inputSource, resource);
        // 根據 Document 例項****註冊 Bean資訊
        return registerBeanDefinitions(doc, resource);
    }
    ...
}

核心部分就是 try 塊的兩行程式碼。

  1. 呼叫 doLoadDocument() 方法,根據 xml 檔案獲取 Document 例項。
  2. 根據獲取的 Document 例項註冊 Bean 資訊

其實在doLoadDocument()方法內部還獲取了 xml 檔案的驗證模式。如下:

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

呼叫 getValidationModeForResource() 獲取指定資源(xml)的驗證模式。所以 doLoadBeanDefinitions()主要就是做了三件事情。

1. 呼叫 getValidationModeForResource() 獲取 xml 檔案的驗證模式
2. 呼叫 loadDocument() 根據 xml 檔案獲取相應的 Document 例項。
3. 呼叫 registerBeanDefinitions() 註冊 Bean 例項。

獲取XML的驗證模式 

DTD和XSD區別 

DTD(Document Type Definition)即文件型別定義,是一種XML約束模式語言,是XML檔案的驗證機制,屬於XML檔案組成的一部分。DTD是一種保證XML文件格式正確的有效方法,可以通過比較XML文件和DTD檔案來看文件是否符合規範,元素和標籤使用是否正確。一個DTD文件包含:元素的定義規則,元素間關係的定義規則,元素可使用的屬性,可使用的實體或符合規則。
使用DTD驗證模式的時候需要在XML檔案的頭部宣告,以下是在Spring中使用DTD宣告方式的程式碼:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">

XML Schema語言就是XSD(XML Schemas Definition)。XML Schema描述了XML文件的結構,可以用一個指定的XML Schema來驗證某個XML文件,以檢查該XML文件是否符合其要求,文件設計者可以通過XML Schema指定一個XML文件所允許的結構和內容,並可據此檢查一個XML文件是否是有效的。

在使用XML Schema文件對XML例項文件進行檢驗,除了要宣告名稱空間外(xmlns=http://www.Springframework.org/schema/beans),還必須指定該名稱空間所對應的XML Schema文件的儲存位置,通過schemaLocation屬性來指定名稱空間所對應的XML Schema文件的儲存位置,它包含兩個部分,一部分是名稱空間的URI,另一部分就該名稱空間所標識的XML Schema檔案位置或URL地址(xsi:schemaLocation=”http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd“),程式碼如下:

<?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="myTestBean" class="com.chenhao.spring.MyTestBean"/>

</beans>

驗證模式的讀取 

在spring中,是通過getValidationModeForResource方法來獲取對應資源的驗證模式,其原始碼如下:

protected int getValidationModeForResource(Resource resource) {
    int validationModeToUse = getValidationMode();
    if (validationModeToUse != VALIDATION_AUTO) {
        return validationModeToUse;
    }
    int detectedMode = detectValidationMode(resource);
    if (detectedMode != VALIDATION_AUTO) {
        return detectedMode;
    }
    // Hmm, we didn't get a clear indication... Let's assume XSD,
    // since apparently no DTD declaration has been found up until
    // detection stopped (before finding the document's root tag).
    return VALIDATION_XSD;
}

方法的實現還是很簡單的,如果設定了驗證模式則使用設定的驗證模式(可以通過使用XmlBeanDefinitionReader中的setValidationMode方法進行設定),否則使用自動檢測的方式。而自動檢測驗證模式的功能是在函式detectValidationMode方法中,而在此方法中又將自動檢測驗證模式的工作委託給了專門處理類XmlValidationModeDetector的validationModeDetector方法,具體程式碼如下:

public int detectValidationMode(InputStream inputStream) throws IOException {
    // Peek into the file to look for DOCTYPE.
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    try {
        boolean isDtdValidated = false;
        String content;
        while ((content = reader.readLine()) != null) {
            content = consumeCommentTokens(content);
            if (this.inComment || !StringUtils.hasText(content)) {
                continue;
            }
            if (hasDoctype(content)) {
                isDtdValidated = true;
                break;
            }
            if (hasOpeningTag(content)) {
                // End of meaningful data...
                break;
            }
        }
        return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
    }
    catch (CharConversionException ex) {
        // Choked on some character encoding...
        // Leave the decision up to the caller.
        return VALIDATION_AUTO;
    }
    finally {
        reader.close();
    }
}

從程式碼中看,主要是通過讀取 XML 檔案的內容,判斷內容中是否包含有 DOCTYPE ,如果是 則為 DTD,否則為 XSD,當然只會讀取到 第一個 “<” 處,因為 驗證模式一定會在第一個 “<” 之前。如果當中出現了 CharConversionException 異常,則為 XSD模式。

獲取Document

經過了驗證模式準備的步驟就可以進行Document載入了,對於文件的讀取委託給了DocumentLoader去執行,這裡的DocumentLoader是個介面,而真正呼叫的是DefaultDocumentLoader,解析程式碼如下:

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
        ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    if (logger.isDebugEnabled()) {
        logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
    }
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    return builder.parse(inputSource);
}

分析程式碼,首選建立DocumentBuildFactory,再通過DocumentBuilderFactory建立DocumentBuilder,進而解析InputSource來返回Document物件。對於引數entityResolver,傳入的是通過getEntityResolver()函式獲取的返回值,程式碼如下:

protected EntityResolver getEntityResolver() {
    if (this.entityResolver == null) {
        // Determine default EntityResolver to use.
        ResourceLoader resourceLoader = getResourceLoader();
        if (resourceLoader != null) {
            this.entityResolver = new ResourceEntityResolver(resourceLoader);
        }
        else {
            this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
        }
    }
    return this.entityResolver;
}

這個entityResolver是做什麼用的呢,接下來我們詳細分析下。 

EntityResolver 的用法 

對於解析一個XML,SAX首先讀取該XML文件上的宣告,根據宣告去尋找相應的DTD定義,以便對文件進行一個驗證,預設的尋找規則,即通過網路(實現上就是宣告DTD的URI地址)來下載相應的DTD宣告,並進行認證。下載的過程是一個漫長的過程,而且當網路中斷或不可用時,這裡會報錯,就是因為相應的DTD宣告沒有被找到的原因.

EntityResolver的作用是專案本身就可以提供一個如何尋找DTD宣告的方法,即由程式來實現尋找DTD宣告的過程,比如將DTD檔案放到專案中某處,在實現時直接將此文件讀取並返回給SAX即可,在EntityResolver的介面只有一個方法宣告:

public abstract InputSource resolveEntity (String publicId, String systemId)
    throws SAXException, IOException;

它接收兩個引數publicId和systemId,並返回一個InputSource物件,以特定配置檔案來進行講解 
(1)如果在解析驗證模式為XSD的配置檔案,程式碼如下:

<?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">
....
</beans>

則會讀取到以下兩個引數 
- publicId:null 
- systemId:http://www.Springframework.org/schema/beans/Spring-beans.xsd 

(2)如果解析驗證模式為DTD的配置檔案,程式碼如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">
....
</beans>

讀取到以下兩個引數
- publicId:-//Spring//DTD BEAN 2.0//EN
- systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd

一般都會把驗證檔案放置在自己的工程裡,如果把URL轉換為自己工程裡對應的地址檔案呢?以載入DTD檔案為例來看看Spring是如何實現的。根據之前Spring中通過getEntityResolver()方法對EntityResolver的獲取,我們知道,Spring中使用DelegatingEntityResolver類為EntityResolver的實現類,resolveEntity實現方法如下:

@Override
@Nullable
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
    if (systemId != null) {
        if (systemId.endsWith(DTD_SUFFIX)) {
            return this.dtdResolver.resolveEntity(publicId, systemId);
        }
        else if (systemId.endsWith(XSD_SUFFIX)) {
            return this.schemaResolver.resolveEntity(publicId, systemId);
        }
    }
    return null;
}

不同的驗證模式使用不同的解析器解析,比如載入DTD型別的BeansDtdResolver的resolveEntity是直接擷取systemId最後的xx.dtd然後去當前路徑下尋找,而載入XSD型別的PluggableSchemaResolver類的resolveEntity是預設到META-INF/Spring.schemas檔案中找到systemId所對應的XSD檔案並載入。 BeansDtdResolver 的解析過程如下:

public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException {
    if (logger.isTraceEnabled()) {
        logger.trace("Trying to resolve XML entity with public ID [" + publicId +
                "] and system ID [" + systemId + "]");
    }
    if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
        int lastPathSeparator = systemId.lastIndexOf('/');
        int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
        if (dtdNameStart != -1) {
            String dtdFile = DTD_NAME + DTD_EXTENSION;
            if (logger.isTraceEnabled()) {
                logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
            }
            try {
                Resource resource = new ClassPathResource(dtdFile, getClass());
                InputSource source = new InputSource(resource.getInputStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
                if (logger.isDebugEnabled()) {
                    logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
                }
                return source;
            }
            catch (IOException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
                }
            }
        }
    }
    return null;
}

從上面的程式碼中我們可以看到載入 DTD 型別的 BeansDtdResolver.resolveEntity() 只是對 systemId 進行了簡單的校驗(從最後一個 / 開始,內容中是否包含 spring-beans),然後構造一個 InputSource 並設定 publicId、systemId,然後返回。 PluggableSchemaResolver 的解析過程如下:

public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException {
    if (logger.isTraceEnabled()) {
        logger.trace("Trying to resolve XML entity with public id [" + publicId +
                "] and system id [" + systemId + "]");
    }

    if (systemId != null) {
        String resourceLocation = getSchemaMappings().get(systemId);
        if (resourceLocation != null) {
            Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
            try {
                InputSource source = new InputSource(resource.getInputStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
                if (logger.isDebugEnabled()) {
                    logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
                }
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex);
                }
            }
        }
    }
    return null;
}

首先呼叫 getSchemaMappings() 獲取一個對映表(systemId 與其在本地的對照關係),然後根據傳入的 systemId 獲取該 systemId 在本地的路徑 resourceLocation,最後根據 resourceLocation 構造 InputSource 物件。 對映表如下(部分):

 

解析及註冊BeanDefinitions

當把檔案轉換成Document後,接下來就是對bean的提取及註冊,當程式已經擁有了XML文件檔案的Document例項物件時,就會被引入到XmlBeanDefinitionReader.registerBeanDefinitions這個方法:

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

其中的doc引數即為上節讀取的document,而BeanDefinitionDocumentReader是一個介面,而例項化的工作是在createBeanDefinitionDocumentReader()中完成的,而通過此方法,BeanDefinitionDocumentReader真正的型別其實已經是DefaultBeanDefinitionDocumentReader了,進入DefaultBeanDefinitionDocumentReader後,發現這個方法的重要目的之一就是提取root,以便於再次將root作為引數繼續BeanDefinition的註冊,如下程式碼:

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    logger.debug("Loading bean definitions");
    Element root = doc.getDocumentElement();
    doRegisterBeanDefinitions(root);
}

通過這裡我們看到終於到了解析邏輯的核心方法doRegisterBeanDefinitions,接著跟蹤原始碼如下:

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)) {
                if (logger.isInfoEnabled()) {
                    logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                            "] not matching: " + getReaderContext().getResource());
                }
                return;
            }
        }
    }
    preProcessXml(root);
    parseBeanDefinitions(root, this.delegate);
    postProcessXml(root);
    this.delegate = parent;
}

我們看到首先要解析profile屬性,然後才開始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) {
                Element ele = (Element) node;
                if (delegate.isDefaultNamespace(ele)) {
                    parseDefaultElement(ele, delegate);
                }
                else {
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        delegate.parseCustomElement(root);
    }
}

最終解析動作落地在兩個方法處:parseDefaultElement(ele, delegate) 和 delegate.parseCustomElement(root)。我們知道在 Spring 有兩種 Bean 宣告方式:

  • 配置檔案式宣告:<bean id="myTestBean" class="com.chenhao.spring.MyTestBean"/>
  • 自定義註解方式:<tx:annotation-driven>

兩種方式的讀取和解析都存在較大的差異,所以採用不同的解析方法,如果根節點或者子節點採用預設名稱空間的話,則呼叫 parseDefaultElement() 進行解析,否則呼叫 delegate.parseCustomElement() 方法進行自定義解析。

而判斷是否預設名稱空間還是自定義名稱空間的辦法其實是使用node.getNamespaceURI()獲取名稱空間,並與Spring中固定的名稱空間http://www.springframework.org/schema/beans進行對比,如果一致則認為是預設,否則就認為是自定義。 

profile的用法 

通過profile標記不同的環境,可以通過設定spring.profiles.active和spring.profiles.default啟用指定profile環境。如果設定了active,default便失去了作用。如果兩個都沒有設定,那麼帶有profiles的bean都不會生成。 

配置spring配置檔案最下面配置如下beans

<!-- 開發環境配置檔案 -->
<beans profile="development">
    <context:property-placeholder
            location="classpath*:config_common/*.properties, classpath*:config_development/*.properties"/>
</beans>

<!-- 測試環境配置檔案 -->
<beans profile="test">
    <context:property-placeholder
            location="classpath*:config_common/*.properties, classpath*:config_test/*.properties"/>
</beans>

<!-- 生產環境配置檔案 -->
<beans profile="production">
    <context:property-placeholder
            location="classpath*:config_common/*.properties, classpath*:config_production/*.properties"/>
</beans>

配置web.xml

<!-- 多環境配置 在上下文context-param中設定profile.default的預設值 -->
<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>production</param-value>
</context-param>

<!-- 多環境配置 在上下文context-param中設定profile.active的預設值 -->
<!-- 設定active後default失效,web啟動時會載入對應的環境資訊 -->
<context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>test</param-value>
</context-param>

這樣啟動的時候就可以按照切換spring.profiles.active的屬性值來進行切換了。