Spring5原始碼解析_IOC之容器的基本實現

碼上遇見你發表於2022-01-05

前言:

在分析原始碼之前,我們簡單回顧一下SPring核心功能的簡單使用:

容器的基本用法

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

package com.vipbbo.spring.Bean;

/**
 * @author paidaxing
 */
public class MyTestBean {
    private String name = "paidaxing";

    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.vipbbo.spring.Bean.MyTestBean"/>
</beans>

在上面的配置中我們可以看到Bean的宣告方式,在Spring中的Bean定義有N種屬性,但是隻要像上面這樣簡單的宣告就可以使用了。

測試程式碼如下:

public class AppTest {
    @Test
    public void MyTestBean(){
//        ApplicationContext ac = new ClassPathXmlApplicationContext("spring-beans.xml");
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-beans.xml"));
        MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean");
        System.out.println(myTestBean.getName());
    }
}

測試結果截圖

image

注意:

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


功能分析

接下來我們分析測試類程式碼中完成的功能:

  • 讀取配置檔案spring-beans.xml
  • 根據spring-beans.xml中的配置找到對應的類的配置,並且例項化
  • 呼叫例項化後的例項

下圖是一個最簡單spring功能架構,如果想完成我們預想的功能,至少需要3個類:

image

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

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

APP:用於完成整個邏輯的關聯。

原始碼工程檢視

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

spring-beans包的層次結構

我們先看看beans工程的原始碼結構,如下圖所示:

image

說明:

  • src/main/java 用於展現Spring的主要邏輯
  • src/main/resource 用於存放系統的配置檔案
  • src/test/java 用於對主要邏輯進行單元測試
  • src/test/resource 用於存放測試用的配置檔案
核心類的介紹

接下來我們先來了解一下spring-beans最核心的兩個類:DefaultListableBeanFactoryXmlBeanDefinitionReader

DefaultListableBeanFactory

首先XMLBeanFactory繼承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整個Bean載入的核心部分,是Spring註冊以及載入Bean的預設實現;而對於XMLBeanFactory與DefaultBeanFactory不同的地方其實是在XmlBeanFactory中使用了自定義XML讀取器XmlBeanDefinitionReader,實現了個性化的BeanDefinitionReader讀取

DefaultListableBeanFactory繼承自AbstractAutowireCapableBeanFactory並實現了ConfigurableListableBeanFactory以及BeanDefinitionRegistry幾口。以下是ConfigurableListableBeanFactory的層次結構圖如下相關類圖


![image](https://img2020.cnblogs.com/blog/1416303/202201/1416303-20220103201745515-1357533824.png) **上面類圖中各個類及介面的作用如下:**
  • AliasRegistry:定義對alias的簡單增刪改操作
  • SimpleAlisaRegistry:主要使用map作為alias的快取,並對介面AliasRegistry進行實現
  • SingletonBeanRegistry:定義對單例的註冊及獲取
  • BeanFactory:定義Bean及Bean的各種屬性
  • BeanDefinitionRegistry:定義對BeanDefinition的各種增刪改操作
  • DefaultSingletonBeanRegistry:預設對幾口SingletonBeanRegistry各個函式的實現
  • HierarchicalBeanFactory:繼承BeanFactory,也就是在BeanFactory定義的功能的基礎上增加了對parentFactory的支援
  • ListableBeanFactory:根據各種條件獲取Bean的配置清單
  • FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基礎上增加了對FactoryBean的特殊處理功能
  • ConfigurableBeanFactory:提供Factory的各種方法
  • AbstractBeanFactory:綜合FactoryBeanRegistrySupport和ConfiguableBeanFactory的功能
  • AutowireCapableBeanFactory:提供建立Bean、自動注入、初始化以及應用Bean的後處理器
  • AbstractAutowireCapableBeanFactory:綜合AbstractBeanFactory並對介面AutowireCapableBeanFactory進行實現
  • ConfiurableListableBeanFactory:BeanFactory配置清單,指定忽略型別及介面等
  • DefaultListableBeanFactory:綜合上面所有的功能,主要是對Bean註冊後的處理

XmlBeanFactory對DefaultListableBeanFactory類進行了擴充套件,主要用於從XML文件中讀取BeanDefinition,對於註冊及獲取Bean都是使用從父類DefaultListableBeanFactory繼承的方法去實現,而唯獨與父類不同的個性化實現就是增加了XmlBeanDefinitionReader型別的reader屬性。在XmlBeanFactory中主要使用reader屬性對資原始檔進行讀取和註冊。

XmlBeanDefinitionReader

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

ResourceLoader:定義資源載入器,主要應用於根據給定的資原始檔地址返回對應的Resource

BeanDefinitonReader:主要定義資原始檔讀取並轉換為BeanDefinition的各個功能

EnvironmentCapable:定義獲取Environment方法

DocumentLoader:定義從資原始檔載入到轉換為Documen的功能

AbstractBeanDefinitonReader:對EnvironmentCapable、BeanDefinitonReader類定義的功能進行實現

BeanDefinitionDocumentReader:定義讀取Document並註冊BeanDefinition功能

BeanDefinitionParserDelegate:定義解析Element的各種方法

整個XML配置檔案讀取的大致流程,在XmlBeanDefinitionReader中主要包含以下幾步處理:

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


image

容器的基礎XmlBeanFactory

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

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

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

配置檔案的封裝

Spring的配置檔案讀取是通過ClassPathResource進行封裝的,Spring對其內部使用到的資源實現了自己的抽象結構:

Resource介面來封裝底層資源,原始碼如下:

public interface Resource extends InputStreamSource {
    Boolean exists();
    default Boolean isReadable() {
        return exists();
    }
    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;
    @Nullable
        String getFilename();
    String getDescription();
}

InputStreamSource原始碼:

public interface InputStreamSource {

    /**
     * Return an {@link InputStream} for the content of an underlying resource.
     * <p>It is expected that each call creates a <i>fresh</i> stream.
     * <p>This requirement is particularly important when you consider an API such
     * as JavaMail, which needs to be able to read the stream multiple times when
     * creating mail attachments. For such a use case, it is <i>required</i>
     * that each {@code getInputStream()} call returns a fresh stream.
     * @return the input stream for the underlying resource (must not be {@code null})
     * @throws java.io.FileNotFoundException if the underlying resource doesn't exist
     * @throws IOException if the content stream could not be opened
     */
    InputStream getInputStream() throws IOException;

}

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

Resource介面抽象了所有Spring內部使用到的底層資源:File、URL、ClassPath等。首先,它先定義了3個判斷當前資源狀態的方法:存在性(exits)、可讀性(isReadable)、是否處於開啟狀態(isOpen),另外,Resource介面還提供了不同資源到URL、URI、File型別的轉換,以及獲取lastModified屬性,檔名(不帶路徑資訊的檔名,getFileName())的方法,為了便於操作,Resource還提供了基於當前資源建立一個相對資源的方法:createRelative(),還提供了getDescription()方法用於在錯誤處理中列印資訊。

對於不同來源的資原始檔都有相對應的Resource實現:檔案(FileSystemResource)、Calsspath資源(ClassPathResource)、URL資源(UrlResource)、InputStream資源(InputStreamResource)、Byte陣列(ByteArrayResource)等,相關類圖如下:

image

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

Resource resource = new ClassPathResource("spring-beans.xml");
try {
    InputStream inputStream = resource.getInputStream();
}
catch (IOException e) {
    e.printStackTrace();
}

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

  • 看原始碼(ClassPathResource.java)- 方法getInputStream
@Override
public InputStream getInputStream() throws IOException {
    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);
    }
    if (is == null) {
        throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
    }
    return is;
}
  • 看原始碼(FileSystemResource.java)- 方法getInputStream
@Override
public InputStream getInputStream() throws IOException {
    try {
        return Files.newInputStream(this.filePath);
    }
    catch (NoSuchFileException ex) {
        throw new FileNotFoundException(ex.getMessage());
    }
}

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

public class XmlBeanFactory extends DefaultListableBeanFactory {
    // 提供了XmlBeanDefinitionReader型別的 reader屬性
    private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
    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),我們按照程式碼層級進行跟蹤,首先跟蹤到如下父類程式碼:

  • 看原始碼(DefaultListableBeanFactory.java)
public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
    super(parentBeanFactory);
}

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

  • 看原始碼(AbstractAutowireCapableBeanFactory.java)
public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
    this();
    setParentBeanFactory(parentBeanFactory);
}
public AbstractAutowireCapableBeanFactory() {
    super();
    ignoreDependencyInterface(BeanNameAware.class);
    ignoreDependencyInterface(BeanFactoryAware.class);
    ignoreDependencyInterface(BeanClassLoaderAware.class);
    if (NativeDetector.inNativeImage()) {
        this.instantiationStrategy = new SimpleInstantiationStrategy();
    } else {
        this.instantiationStrategy = new CglibSubclassingInstantiationStrategy();
    }
}

這裡需要特別注意一下ignoreDependencyInterface方法,ignoreDependencyInterface的主要功能就是指自動裝配(autowiring)的時候忽略的類。那麼這樣做的目的是什麼?會產生什麼樣的結果呢?

舉例來說:

當A中有屬性B,那麼當Spring在獲取A的Bean的時候如果其屬性B還沒有初始化,那麼Spring會自動初始化B;這也是Spring提供的一個重要特性

但是在某些情況下,B不會被初始化,其中的一種情況就是B實現二零BeanNameAware介面。Spring中是這樣介紹的:自動裝配時忽略給定的依賴介面,典型應用是通過其它方式解析Application上下文註冊依賴,類似於BeanFactory通過BeanFactoryAware進行注入或者ApplicationContext通過ApplicationContextAware進行注入。

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

  • 看原始碼(AbstractAutowireCapableBeanFactory.java)
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {
    /**
     * Dependency interfaces to ignore on dependency check and autowire, as Set of
     * Class objects. By default, only the BeanFactory interface is ignored.
     */
    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方法

  • 看原始碼(AutowireUtils.java)
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) {
            // ifc.isAssignableFrom(targetClass) Bean型別是否是介面的實現類,
            // ClassUtils.hasMethod(ifc, setter) 介面中是否有入參和Bean型別完全相同的setter方法
            if (ifc.isAssignableFrom(targetClass) && ClassUtils.hasMethod(ifc, setter)) {
                return true;
            }
        }
    }
    return false;
}

ignoreDependencyInterface方法並不是讓我們在自動裝配時直接忽略實現了該介面的依賴。這個方法的真正意思是忽略該介面的實現類中和介面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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.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="ignoreInterface" class="com.vipbbo.spring.bean.IgnoreInterfaceImpl"/>
    <!--如果要使用後置處理器來進行忽略自動裝配的類 不可以使用 XmlBeanFactory 方式載入配置檔案 如果使用 XmlBeanFactory 可忽略本行程式碼 -->
    <!--關於mlBeanFactory 和 ApplicationContext 區別我在文章 【spring原始碼閱讀一】有提過-->
    <bean id="customBeanFactoryPostProcessor" class="com.vipbbo.spring.bean.IgnoreAutowiringProcessor"/>
</beans>

步驟四:最後呼叫ignoreDependencyInterface()方法

public class AppTestIgnoredepencytype {
    @Test
    public void TestIgnore(){
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-ignore.xml"));
        ((ConfigurableListableBeanFactory) bf ).ignoreDependencyInterface(IgnoreInterface.class);
        IgnoreInterfaceImpl bean = (IgnoreInterfaceImpl) bf.getBean(IgnoreInterface.class);
        System.out.println(bean.getList());
        System.out.println(bean.getSet());
    }
}

使用了ignoreDependencyInterface()方法執行截圖:

image
未使用ignoreDependencyInterface()方法執行截圖:

((ConfigurableListableBeanFactory) bf ).ignoreDependencyInterface(IgnoreInterface.class);注掉即可

image


關於這個ignoreDependencyInterface()方法也可參考以下篇文章:

https://www.jianshu.com/p/3c7e0608ff1f


https://blog.csdn.net/xyjy11/article/details/113776587?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.no_search_link&spm=1001.2101.3001.4242.1

default-autowire和@Autowired在這裡解釋的相對完整

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

典型應用

就是BeanFactoryAwareApplicationContextAware介面。接下來我們看一下它們原始碼:

  • 看原始碼(BeanFactoryAware.java)
public interface BeanFactoryAware extends Aware {
    void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}

繼續追蹤ApplicationContextAware介面:

  • 看原始碼(ApplicationContextAware.java)
public interface ApplicationContextAware extends Aware {
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

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

例如在類AbstractApplicationContext

beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

image

在類AbstractAutowireCapableBeanFactory

ignoreDependencyInterface(BeanFactoryAware.class);

image

由此使我們的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;
    }
}

Spring這樣做的目的其實很簡單,就是使ApplicationContextAware和BeanFactoryAware中的ApplicationContext或BeanFactory依賴在自動裝配時被忽略,而統一由框架設定依賴,如ApplicationContextAware介面設定會在ApplicationContextAwareProcessor類中完成,原始碼如下:

  • 看原始碼(ApplicationContextAwareProcessor.java)
private void invokeAwareInterfaces(Object bean) {
    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 ApplicationStartupAware) {
        ((ApplicationStartupAware) bean).setApplicationStartup(this.applicationContext.getApplicationStartup());
    }
    if (bean instanceof ApplicationContextAware) {
        ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
    }
}

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

Bean載入

在之前XmlBeanFactory建構函式中呼叫了XmlBeanDefinitionReader型別的reader屬性提供的方法

this.reader.loadBeanDefinitions(resource),而這句程式碼則是整個資源載入的切入點,這個方法的時序圖如下:

image


我們來簡單梳理一下上述時序圖的處理過程:

  • 封裝資原始檔,當進入XmlBeanDefinitionReader後首先對引數Resource使用EncodedResource類進行封裝
  • 獲取輸入流,從Resource中獲取對應的InputStream並構造InputSource
  • 通過構造的InputSource例項和Resource例項繼續呼叫函式doLoadBeanDefinitions,loadBeanDefinitons函式具體實現過程原始碼如下:
  • 看原始碼(XmlBeanDefinitionReader.java)
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.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();
        }
    }
}

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

  • 看原始碼(XmlBeanDefinitionReader.java)
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {
    try {
        // 獲取 Document 例項
        Document doc = doLoadDocument(inputSource, resource);
        // 根據 Document 例項註冊 Bean 資訊
        int count = registerBeanDefinitions(doc, resource);
        if (logger.isDebugEnabled()) {
            logger.debug("Loaded " + count + " bean definitions from " + resource);
        }
        return count;
    }
    // 省略......
}

核心部分就是Try中的兩行程式碼,如上圖所示。

  • 呼叫doLoadDocument()方法,根據xml檔案獲取Document例項
  • 根據Document例項註冊Bean資訊

其實咋doLoadDocument()方法中還獲取了xml檔案的驗證方式,如下:

  • 看原始碼(XmlBeanDefinitionReader.java)
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
                    getValidationModeForResource(resource), isNamespaceAware());
}
  • 原始碼解析:

從上述程式碼中我們看到通過getValidationModeForResource()獲取了指定資源的xml的驗證模式,所以doLoadDocument()主要做了三件事

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

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

使用DTO驗證模式的時候需要在XML檔案的頭部宣告,以下是在Spring中使用DTO宣告方式的程式碼:

<?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,它包含兩個部分:

  • 一部分是名稱空間的URL

  • 另外一部分就是該名稱空間所標識的XML Schema檔案位置或URL地址(xsi:schemaLocation="http://www.springframework.org/schema/beans")

    如下案例:

    <?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.vipbbo.spring.bean.MyTestBean"/>
    </beans>
    
驗證模式的獲取

在Spring中,是通過上面所提到的getValidationModeForResource()方法來獲取對應資源的驗證模式,其原始碼如下:

  • 看原始碼(XmlBeanDefinitionReader.java)
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;
}
  • 原始碼解析

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

  • 看原始碼(XmlValidationModeDetector.java)
public int detectValidationMode(InputStream inputStream) throws IOException {
    // Peek into the file to look for DOCTYPE.
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
        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;
    }
}
  • 原始碼分析

從原始碼中我們可以看到,主要是通過讀取XML檔案的內容,判斷內容中是否包含有DOCTYPE

,如果是則為DTD,否則為XSD,當然只會讀取到第一個<處,因為驗證模式一定會在第一個<之前。如果當中出現了CharConversionException 異常,則為XSD模式

獲取Document

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

  • 看原始碼(DefaultDocumentLoader)
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
            ErrorHandler errorHandler, int validationMode, Boolean namespaceAware) throws Exception {
    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    if (logger.isTraceEnabled()) {
        logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
    }
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    return builder.parse(inputSource);
}
  • 原始碼分析

首先我們看到先建立了DocumentBuilderFactory,再通過DocumentBuilderFactory建立DocumentBuilder,進而解析InputSource來返回的Document物件,對於引數entityResolver,傳入是通過XmlBeanDefinitionReader類的doLoadDocument函式中的getEntityResolver()方法獲取的返回值,原始碼如下:

  • 看原始碼(XmlBeanDefinitionReader)
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(simple API for XML:簡單的XML的應用程式介面)首先讀取該XML文件上的宣告,根據宣告去尋找相應的DTD定義,以便對文件進行一個驗證,預設的尋找規則,即通過網路(實現上就是宣告DTD的URI地址來下載相應的DTO宣告,並進行認證。下載的過程是一個漫長的過程,而且當網路中斷或不可用時,這裡會報錯,就是因為相應的DTD宣告沒有找到的原因。)

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

  • 看原始碼(EntityResolver.java)
public interface EntityResolver {
    public abstract InputSource resolveEntity (String publicId,
                                                   String systemId)
            throws SAXException, IOException;
}
  • 原始碼分析

從上面原始碼中我們可以看到它接收兩個引數publicIdsystemId,並返回一個InputSource物件,以特定配置檔案來進行講解:

  • 如果在解析驗證模式為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>

執行測試類我們再ResourceEntityResolver類中Debugger一下

測試類如下:

public class AppTest {
    @Test
    public void MyTestBean(){
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-beans.xml"));
    }
}

Debugger執行可以看到它讀取到了兩個引數

  • publicId : null
  • systemId :http://www.springframework.org/schema/beans/spring-beans.xsd

image

  • 如果解析驗證模式為DTD的配置檔案,程式碼如下

xml:

<?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>

</beans>

測試類:

public class AppTest {
    @Test
    public void MyTestBean(){
        //        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-beans.xml"));
        BeanFactory bf = new XmlBeanFactory( new ClassPathResource("xml-DTD.xml"));
    }
}

執行測試類獲得到如下兩個引數:

  • publicId:-//SPRING//DTD BEAN 2.0//EN

  • systemId:http://www.springframework.org/dtd/spring-beans-2.0.dtd

執行結果截圖:

image

Tips:關於xml解析的DTD方式等相關知識點可以檢視這兩篇文章:

https://cloud.tencent.com/developer/article/1023647

https://www.cnblogs.com/cb0327/p/4967782.html#_label0

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

  • 看原始碼(DelegatingEntityResolver.java)
@Override
@Nullable
public InputSource resolveEntity(@Nullable 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);
        }
    }
    // Fall back to the parser's default behavior.
    return null;
}
  • 原始碼分析

從上面的原始碼中我們可以看到不同的驗證模式是由不同的解析器解析,比如載入DTD型別的BeansDtdResolver的resolveEntity是直接擷取systemId最後的xx.dtd然後去當前路徑下尋找,而載入XSD型別的PluggableSchemaResolver類的resolveEntity是預設到META-INF/spring.schemas檔案中找到systemI對應的XSD檔案並載入。如下圖所示:

image

BeanDtdResolver的解析過程如下:

  • 看原始碼(BeansDtdResolver.java)
@Override
@Nullable
public InputSource resolveEntity(@Nullable 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.isTraceEnabled()) {
                    logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
                }
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
                }
            }
        }
    }
    // Fall back to the parser's default behavior.
    return null;
}
  • 原始碼分析

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

  • 看原版嗎(PluggableSchemaResolver.java)
@Override
@Nullable
public InputSource resolveEntity(@Nullable 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 && systemId.startsWith("https:")) {
            // Retrieve canonical http schema mapping even for https declaration
            resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
        }
        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.isTraceEnabled()) {
                    logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
                }
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
                }
            }
        }
    }
    // Fall back to the parser's default behavior.
    return null;
}
  • 原始碼分析

首先我們從上面可以看到先呼叫了方法getSchemaMappings()獲取了一個對映表(systemId 與其在本地的對照關係),然後根據傳入的systemId獲取該systemId在本地路徑下的resourceLocation,最後根據resourceLocation構造InputSource物件,對映表如下圖所示:(部分)

image

解析及註冊BeanDefinitions

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

  • 看原始碼(XmlBeanDefinitonReader.java)
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;
}
  • 原始碼分析

從上面原始碼中我們可以看到有兩個引數Document doc, Resource resource,其中的doc引數即是上節說的document,而BeanDefinitionDocumentReader是一個介面,而例項化的工作是在createBeanDefinitionDocumentReader方法中完成的。而通過此方法,BeanDefinitionDocumentReader真正的型別已經是DefaultBeanDefinitionDocumentReader,進入到DefaultBeanDefinitionDocumentReader後,發現這個方法的重要目的之一就是提取doc.getDocumentElement(),以便於再次將doc.getDocumentElement()作為引數繼續BeanDefinition的註冊,原始碼如下:

  • 看原始碼(DefaultBeanDefinitionDocumentReader.java)
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);
            // We cannot use Profiles.of(...) since profile expressions are not supported
            // in XML config. See SPR-12458 for details.
            if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("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的讀取,具體的程式碼如下:

  • 看原始碼(DefaultBeanDefinitionDocumentReader.java)
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:

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

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

那麼是如何判斷是否是預設空間還是自定義名稱空間呢?Spring其實使用的node.getNamespaceURI()獲取名稱空間(這個方法是在isDefaultNamespace函式裡面),並與Spring中固定的名稱空間http://www.springframework.org/schema/beans進行對比,如果一致則認為是預設,否則就認為是自定義。

Profile的用法

通過profile標記不同的環境,可以通過設定spring.profiles.acticespring.profiles.default啟用指定的profile環境。

如果設定了activedefault便失去了作用。如果兩個都沒有設定,那麼帶有profiles的Bean都不會生成。

<!-- 開發環境配置檔案 -->
<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的屬性值來進行切換了。

整理不易,如果對你有所幫助歡迎點贊關注

微信搜尋【碼上遇見你】獲取更多精彩內容

相關文章