「原理分析」Spring Boot啓動時基於spring.factories自動讀取遠端Environment實現的原理源碼分析

採用Spring標準的事件/監聽器模型,經過Spring SPI的方式,在Spring Boot啓動時,自動讀取遠端「遠程服務器、本地硬盤等」Environment配置,方便在Spring Boot啓動前,對配置進行靈活調整,增長靈活性,減小硬編碼。html

本文先從原理進行分析,代表其可行性,下一篇文章再展現具體的代碼實現。首先從SPI的基礎開始講起。java

1. 服務發現的基礎:SPI

注:此小節內容描述主要參考此文章 spring.factories
在Spring Boot中有一種很是解耦的擴展機制:Spring Factories。這種擴展機制其實是仿照Java中的SPI擴展機制來實現的。web

1.1 背景描述

系統中各個模塊,每每有不少不一樣的實現方案,如日誌組件、JDBC驅動、XML解析組件等。面向對象的程序設計中,推薦使用面向接口編程,業務程序中如需使用某項功能,須依賴通用的標準接口。基於可拔插的設計原則,此時如需更換功能模塊的底層實現,直接予以替換便可「如替換Jar、替換maven依賴等」,業務代碼無需任何改動。上述想法很美好,可是程序使用依賴的功能模塊時,必須進行指明,否則程序運行時可能找不到相應的實現類,可是爲了解耦,咱們不想在業務代碼中聲明具體的實現,有什麼解決方法嗎?spring

這就須要一種服務發現機制。Java SPI就是提供這樣的一個機制。Java SPI機制「Service Provider Interface」主要用於插件等,如需詳細瞭解可參考java.util.ServiceLoader的文檔。編程

1.2 Java SPI約定

Java SPI的具體約定爲:當服務的提供者,提供了服務接口的一種實現以後,在jar包的META-INF/services/目錄裏同時建立一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能經過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。 基於這樣一個約定就能很好的找到服務接口的實現類,而不須要再在代碼裏指定。JDK中提供了服務實現查找的一個工具類:java.util.ServiceLoader緩存

1.3 Spring Boot中的SPI機制

在Spring中也有一種相似與Java SPI的加載機制。它在META-INF/spring.factories文件中配置接口的實現類名稱,而後在程序中讀取這些配置文件並實例化。這種自定義的SPI機制是Spring Boot Starter實現的基礎。服務器

2. Spring Boot實現SPI的源碼分析

下面就根據Spring Boot應用的啓動過程,對源碼進行簡要分析。固然Spring Boot本質是對Spring的再封裝,故如下內容適用於Spring,只是部分源碼是Spring Boot專屬的。要注意的是,爲了節省篇幅,避免喧賓奪主,會對實際源碼進行精簡,以突出要表述的內容。app

首先展現最經典的Spring Boot啓動代碼,本節今後處講起,以下:maven

public class Application {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Application.class);
        application.run(args);
    }
}

2.1 實例化SpringApplication對象

在實例化SpringApplication對象時,能夠看到程序調用了以下構造方法。在執行到setInitializers((Collection)getSpringFactoriesInstances(ApplicationContextInitializer.class));時,即觸發了Spring實現的SPI。ide

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

繼續深刻看該方法的具體實現,定位到該方法:org.springframework.boot.SpringApplication#getSpringFactoriesInstances,該方法的源碼以下:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

Spring-Core包裏定義了SpringFactoriesLoader類,該類實現了檢索META-INF/spring.factories文件,並獲取指定接口的配置的功能。在這個類中定義了兩個對外的方法:
loadFactories:根據接口類獲取其實現類的實例,這個方法返回的是對象列表。
loadFactoryNames:根據接口獲取其接口類的名稱,這個方法返回的是類名的列表。
上面的兩個方法的關鍵都是從指定的ClassLoader中獲取spring.factories文件,並解析獲得類名列表,

此處使用的是loadFactoryNames方法。繼續深刻發現實際調用的是loadSpringFactories方法:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }
    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                List<String> factoryClassNames = Arrays.asList(
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
                result.addAll((String) entry.getKey(), factoryClassNames);
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

其中靜態常量FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"

2.2 加載factories文件

java.lang.ClassLoader#getResources方法會遍歷整個項目「包含依賴」的META-INF/spring.factories文件,取得其絕對路徑,如/Users/bitkylin/.m2/repository/cn/bitkylin/test/1.0.0/test.jar/META-INF/spring.factories,使用PropertiesLoaderUtils#loadProperties方法從路徑加載,並最終將接口和其實現類的全名緩存在cache對象中。cache對象的結構以下:

loadProperties讀取.png

一顆多叉樹。將spring.factories中配置的全部接口和其實現類的全名都讀取了出來。此接口將接口org.springframework.context.ApplicationListener的實現類的類名的集合做爲結果返回,然後org.springframework.boot.SpringApplication#createSpringFactoriesInstances方法將上述實現類均進行實例化,此時監聽器就都建立好並註冊了。

spring.factories是經過Properties解析獲得的,咱們能夠按照以下規則編寫:

com.xxx.interface=com.xxx.classname

key是接口,value是實現類。系統會自動將其初始化爲如圖所示的結構,方便使用。

2.3 Spring Boot啓動

調用org.springframework.boot.SpringApplication#run方法,開始啓動Spring Boot。在啓動最開始階段,程序就會調用到org.springframework.boot.SpringApplication#prepareEnvironment方法,並最終調用到經典的org.springframework.context.event.SimpleApplicationEventMulticaster#invokeListener方法「典型的觀察者模式,標準的Spring事件/監聽器模型」,源碼以下:

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
        try {
            doInvokeListener(listener, event);
        }
        catch (Throwable err) {
            errorHandler.handleError(err);
        }
    }
    else {
        doInvokeListener(listener, event);
    }
}

經過該方法,將事件ApplicationEnvironmentPreparedEvent傳遞到全部已註冊的監聽器,能夠藉此實現Spring Boot啓動時自動讀取遠端Environment。具體作法下節再講述。

參考連接

spring.factories

相關文章
相關標籤/搜索