採用Spring標準的事件/監聽器模型,經過Spring SPI的方式,在Spring Boot啓動時,自動讀取遠端「遠程服務器、本地硬盤等」Environment配置,方便在Spring Boot啓動前,對配置進行靈活調整,增長靈活性,減小硬編碼。html
本文先從原理進行分析,代表其可行性,下一篇文章再展現具體的代碼實現。首先從SPI的基礎開始講起。java
注:此小節內容描述主要參考此文章 spring.factories
在Spring Boot中有一種很是解耦的擴展機制:Spring Factories。這種擴展機制其實是仿照Java中的SPI擴展機制來實現的。web
系統中各個模塊,每每有不少不一樣的實現方案,如日誌組件、JDBC驅動、XML解析組件等。面向對象的程序設計中,推薦使用面向接口編程,業務程序中如需使用某項功能,須依賴通用的標準接口。基於可拔插的設計原則,此時如需更換功能模塊的底層實現,直接予以替換便可「如替換Jar、替換maven依賴等」,業務代碼無需任何改動。上述想法很美好,可是程序使用依賴的功能模塊時,必須進行指明,否則程序運行時可能找不到相應的實現類,可是爲了解耦,咱們不想在業務代碼中聲明具體的實現,有什麼解決方法嗎?spring
這就須要一種服務發現機制。Java SPI就是提供這樣的一個機制。Java SPI機制「Service Provider Interface」主要用於插件等,如需詳細瞭解可參考java.util.ServiceLoader的文檔。編程
Java SPI的具體約定爲:當服務的提供者,提供了服務接口的一種實現以後,在jar包的META-INF/services/目錄裏同時建立一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能經過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。 基於這樣一個約定就能很好的找到服務接口的實現類,而不須要再在代碼裏指定。JDK中提供了服務實現查找的一個工具類:java.util.ServiceLoader緩存
在Spring中也有一種相似與Java SPI的加載機制。它在META-INF/spring.factories文件中配置接口的實現類名稱,而後在程序中讀取這些配置文件並實例化。這種自定義的SPI機制是Spring Boot Starter實現的基礎。服務器
下面就根據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); } }
在實例化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"
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
對象的結構以下:
一顆多叉樹。將spring.factories
中配置的全部接口和其實現類的全名都讀取了出來。此接口將接口org.springframework.context.ApplicationListener
的實現類的類名的集合做爲結果返回,然後org.springframework.boot.SpringApplication#createSpringFactoriesInstances
方法將上述實現類均進行實例化,此時監聽器就都建立好並註冊了。
spring.factories是經過Properties解析獲得的,咱們能夠按照以下規則編寫:
com.xxx.interface=com.xxx.classname
key是接口,value是實現類。系統會自動將其初始化爲如圖所示的結構,方便使用。
調用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。具體作法下節再講述。