1. 什么是外部化配置导犹?
個人理解是spring提供的屬性配置和環(huán)境切換功能贯要。核心Api為Environment抽象睛藻,而springboot的配置文件(proepreties/yaml)的加載和其密不可分怀樟,springboot會從默認(rèn)的location位置加載數(shù)據(jù)源并設(shè)置到Environment中薛训。根據(jù)配置環(huán)境來進(jìn)行屬性源的優(yōu)先級調(diào)整
Environment相關(guān)類圖
2. 加載springboot外部化配置文件在2.4.0和之前版本有較大改動,下面分析會根據(jù)不同版本進(jìn)行不同分析
2.1 springboot2.3以及之前版本
在SpringApplication啟動的時候在prepareEnvironment階段會發(fā)送
ApplicationEnvironmentPreparedEvent
事件-
EnvironmentPostProcessorApplicationListener
接受到事件會將spring.factories
文件中所有的EnvironmentPostProcessor
加載并回調(diào)其postProcessEnvironment()
方法辈毯,此時很重要的ConfigFileApplicationListener#postProcessEnvironment()
會被回調(diào)- 添加
Random PropertySource
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { //1. 添加Random PropertySource到Environment中 RandomValuePropertySource.addToEnvironment(environment); //2. 創(chuàng)建Loader內(nèi)置類坝疼,傳入Environment執(zhí)行l(wèi)oad方法 new Loader(environment, resourceLoader).load(); }
- 創(chuàng)建
Load
對象
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { //1. 傳入外部化配置環(huán)境對象 this.environment = environment; //2. 實例化占位符解析器 this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment); //3. 創(chuàng)建資源加載對象 this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null); //4. 以及最重要的加載spring.factories文件中所有的PropertySourceLoader(內(nèi)置兩種:properteies/yaml) this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); }
-
正式進(jìn)行l(wèi)oad外部化配置資源
void load() { //調(diào)用靜態(tài)方法進(jìn)行加載 //1. 環(huán)境對象 //2. defaultProperties PropertySource 加載的profiles位置 spring.profiles.active / include //3. 處理加載邏輯 FilteredPropertySource.apply(this.environment, DefaultPropertiesPropertySource.NAME, LOAD_FILTERED_PROPERTY, this::loadWithFilteredProperties); }
-
替換如果
defaultProperties
存在的話,這個屬性是SpringApplication構(gòu)造的時候傳入的屬性源static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties, Consumer<PropertySource<?>> operation) { MutablePropertySources propertySources = environment.getPropertySources(); PropertySource<?> original = propertySources.get(propertySourceName); //1. 查看defaultProperties是否存在 if (original == null) { operation.accept(null); return; } //構(gòu)造成FilteredPropertySource谆沃,然后加載并替換 propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties)); try { operation.accept(original); } finally { propertySources.replace(propertySourceName, original); } }
loadWithFilteredProperties(PropertySource<?> defaultProperties)
加載
private void loadWithFilteredProperties(PropertySource<?> defaultProperties) { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); initializeProfiles(); //1. 初始化profiles相關(guān)參數(shù) while (!this.profiles.isEmpty()) { //2. 將獲取到的profile參數(shù)依次出棧钝凶,進(jìn)行加載 Profile profile = this.profiles.poll(); if (isDefaultProfile(profile)) { //3. 這里判斷是否是默認(rèn)的profile,其實這里方法名有奇異唁影,其實應(yīng)該是不是默認(rèn)的profile會被添加到Envrionment中 addProfileToEnvironment(profile.getName()); } //4. 加載符合當(dāng)前profile的配置文件耕陷,并添加到Environment最后面 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } //5. 加載 spring.config.name.fileExtension中剩余未加載的(翻閱多種情況,發(fā)現(xiàn)只可能在執(zhí)行該方法的時候外部修改了envrionment的activeProfiles方法才可能進(jìn)入) load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); //6. 將load到的PropertySource應(yīng)用到Environment對象中 addLoadedPropertySources(); //7. 應(yīng)用profile到Environment中 applyActiveProfiles(defaultProperties); }
- 這里面核心只需要觀察
load(profile, this::getPositiveProfileFilter,addToLoaded(MutablePropertySources::addLast, false));
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { //1. 依次獲取 spring.cofnig.addtional-location / location / default localtioni 的路徑進(jìn)行迭代 getSearchLocations().forEach((location) -> { //2. 封裝成ConfigDataLocation String nonOptionalLocation = ConfigDataLocation.of(location).getValue(); boolean isDirectory = location.endsWith("/"); //3. 如果是目錄則獲取spring.config.name作為文件名稱進(jìn)行加載据沈,不是則傳遞null Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES; //4. load資源 names.forEach((name) -> load(nonOptionalLocation, name, profile, filterFactory, consumer)); }); } // -------------- private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { //....省略部分非核心方法 Set<String> processed = new HashSet<>(); //1. 會迭代我們從spring.factories中獲取到的PropertySourceLoader(porperties/yaml) for (PropertySourceLoader loader : this.propertySourceLoaders) { for (String fileExtension : loader.getFileExtensions()) { if (processed.add(fileExtension)) { //2. 進(jìn)行加載哟沫,并傳入profile,拼接的路徑名稱.. loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory, consumer); } } } } // -------------- //加載核心流程 profile -> null -> (若沒有profile)default -> include -> active private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null); // positive: if document.profiles.isEmpty() return true DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile); // positive: if document.profiles contains profile return true | profile = true and document.profiles.isEmptry() if (profile != null) { // Try profile-specific file & profile section in profile file (gh-340) String profileSpecificFile = prefix + "-" + profile + fileExtension; //1. 優(yōu)先加載 spring.config.name-{profile}.fileExtension 并通過defaultFilter過濾的(沒有spring.profiles的) load(loader, profileSpecificFile, profile, defaultFilter, consumer); //2. 然后加載 匹配spring.profiles和當(dāng)前 profile匹配的特有屬性 load(loader, profileSpecificFile, profile, profileFilter, consumer); // Try profile specific sections in files we've already processed for (Profile processedProfile : this.processedProfiles) { if (processedProfile != null) { String previouslyLoaded = prefix + "-" + processedProfile + fileExtension; //3. 加載之前加載過的profile中包含 spring.profiles的特有屬性 load(loader, previouslyLoaded, profile, profileFilter, consumer); } } } // Also try the profile-specific section (if any) of the normal file //4. 最后加載spring.config.name.fileExtension 中spring.profiles不為空的 load(loader, prefix + fileExtension, profile, profileFilter, consumer); }
這步進(jìn)行完畢之后會將所有的外部配置問價加載到
org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loaded
屬性中private Map<Profile, MutablePropertySources> loaded; //保存所有已經(jīng)加載的PropertySource
- 最后應(yīng)用propertySource和Profile就大功告成了
//1. 應(yīng)用PropertySource private void addLoadedPropertySources() { MutablePropertySources destination = this.environment.getPropertySources(); List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values()); Collections.reverse(loaded); String lastAdded = null; Set<String> added = new HashSet<>(); for (MutablePropertySources sources : loaded) { for (PropertySource<?> source : sources) { if (added.add(source.getName())) { //依次添加到Environment中 addLoadedPropertySource(destination, lastAdded, source); lastAdded = source.getName(); } } } } //2. 應(yīng)用profile private void applyActiveProfiles(PropertySource<?> defaultProperties) { List<String> activeProfiles = new ArrayList<>(); if (defaultProperties != null) { Binder binder = new Binder(ConfigurationPropertySources.from(defaultProperties), new PropertySourcesPlaceholdersResolver(this.environment)); activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.include")); if (!this.activatedProfiles) { activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.active")); } } this.processedProfiles.stream().filter(this::isDefaultProfile).map(Profile::getName) .forEach(activeProfiles::add); this.environment.setActiveProfiles(activeProfiles.toArray(new String[0])); }
-
- 添加
最后上一下自己的實驗配置屬性圖锌介,和最后加載的Environment對象結(jié)果
//classpath:/applicaiton.properties
name=default
spring.profiles.active=dev,prod
spring.profiles.include=config,test
#---
spring.profiles=test
name=default#test
#---
spring.profiles=negative
name=default#negative
Environment#propertySource
2.2 springboot 2.4版本配置加載
這個版本springboot重構(gòu)了之前的外部化文件加載方式嗜诀,并且添加了對各大元計算平臺的支持,如Kubernetes的ConfigMap等. 重構(gòu)了之前使用PropertySourceLoader進(jìn)行外部化配置地址 -> propertySource的轉(zhuǎn)變孔祸,其中核心Api類圖如下
核心步驟
- 通過SpringApplication的啟動生命周期回調(diào)到
ConfigDataEnvironmentPostProcessor
的回調(diào) - 從
spring.factories
中獲取ConfigDataLoader
,ConfigDataLocationResolver
加載解析核心組件,并構(gòu)造成ConfigDataEnvironment
對象 - 通過
ConfigDataEnvironment#processAndApply()
開始加載配置文件邏輯 - 核心加載架構(gòu)個人總結(jié)為三大步和三個階段
- 三大步
- 通過
ConfigDataLocationResolver
將相關(guān)spring.config.import
,spring.config.addtional-location
,spring.config.location
等資源定位路徑下的spring.config.name-{profile}.fileExtension
資源解析成ConfigDataResource
- 通過
ConfigDataLoader
將ConfigDataLocationResolver
解析好的資源進(jìn)行加載筑公,將ConfigDataResource
->ConfigData
尊浪, 其中ConfigData
是一組ProeprtySource
- 將加載好的
ConfigData
添加到Environment中
- 通過
- 三大階段匣屡,核心對象為
ConfigDataEnvironmentContributors
捣作,其中分了三個大階段對外部化資源進(jìn)行加載- 無profile無CloudPlatform階段 , 這個階段會使用三大步中前兩步構(gòu)造出ConfigData
- 根據(jù)環(huán)境參數(shù)
spring.main.cloud-platform
或者環(huán)境變量參數(shù)來自動探測云計算廠商環(huán)境鹅士,從而進(jìn)行二階段加載 - 設(shè)置profiles,使用Binder Api從綁定的
ConfigurationPropertySource
中獲取spring.profiles
/spring.config
等資源,進(jìn)行第三階段的加載
- 三大步
詳細(xì)步驟如下
-
ConfigDataEnvironmentPostProcessor
回調(diào)
void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Collection<String> additionalProfiles) {
try {
this.logger.trace("Post-processing environment to add config data");
resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
//執(zhí)行ConfigData加載和應(yīng)用
getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
}
catch (UseLegacyConfigProcessingException ex) {
this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]",
ex.getConfigurationProperty()));
//這里兼容了springboot2.4之前版本的實現(xiàn),可以通過spring.config.use-legacy-processing=true來調(diào)整為之前的實現(xiàn)
//若拋出UseLegacyConfigProcessingException異常則使用老的方式(ConfigFileApplicationListener)進(jìn)行外部化文件配置加載
postProcessUsingLegacyApplicationListener(environment, resourceLoader);
}
}
ConfigDataEnvironment對象的構(gòu)造
ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
Binder binder = Binder.get(environment); //1. 綁定當(dāng)前Environment對象
UseLegacyConfigProcessingException.throwIfRequested(binder);
this.logFactory = logFactory;
this.logger = logFactory.getLog(getClass());
//2. 從屬性spring.config.on-not-found中獲取文件找不到的執(zhí)行邏輯
this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
.orElse(ConfigDataNotFoundAction.FAIL);
this.bootstrapContext = bootstrapContext;
this.environment = environment;
//3. 從spring.factories中獲取ConfigDataLocationResolver實現(xiàn)以舒。(可以自己實現(xiàn)蔓钟,擴(kuò)展點之一)
//4. 同時這里面會傳入boostrapper/resourceLoader/Binder等參數(shù)用于構(gòu)造參數(shù)反射
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
this.additionalProfiles = additionalProfiles;
//5. 從spring.factories中獲取所有的ConfigDataLoader并用反射進(jìn)行實例化
this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext);
//6. 創(chuàng)建ConfigDataEnvironmentContributors對象滥沫,里面會根據(jù)spring.config.import / location等默認(rèn)定位參數(shù)初始化Contributor
this.contributors = createContributors(binder);
}
-
解析并加載
processAndApply()
,整個外部化配置解析的核心框架兰绣,這里能明顯看到我上面說明的三大階段
void processAndApply() {
//1. 封裝ConfigDataImporter對象缀辩,里面有解析ConfigDataLocation -> ConfigDataResource 和load ConfigDataResource -> ConfigData之類的操作
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
this.loaders);
this.bootstrapContext.register(Binder.class, InstanceSupplier
.from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)));
//1. 加載和解析ConfigDataLocation -> ConfigDataResource -> ConfigData ,此時還沒有導(dǎo)入到Environment中雌澄,執(zhí)行完畢之后應(yīng)該都是BOUND_IMPORT,且此時綁定了spring.config / spring.profiles相關(guān)的配置屬性信息
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
//2. 獲取包含Root Contributor中 所有ConfigurationPropertySource的Binder
Binder initialBinder = contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
//3. 重新注冊Binder到Bootstrapper中
this.bootstrapContext.register(Binder.class, InstanceSupplier.of(initialBinder));
ConfigDataActivationContext activationContext = createActivationContext(initialBinder); //構(gòu)建激活的上下文對象,此時對元計算平臺進(jìn)行設(shè)置
//4. 帶云計算平臺參數(shù)上下文進(jìn)行二次迭代
contributors = processWithoutProfiles(contributors, importer, activationContext);
//5. 構(gòu)建profile
activationContext = withProfiles(contributors, activationContext);
//6. 帶profile參數(shù)進(jìn)行第三次迭代
contributors = processWithProfiles(contributors, importer, activationContext);
//7. 應(yīng)用到Environment對象中
applyToEnvironment(contributors, activationContext);
}
內(nèi)容比較復(fù)雜,核心為ConfigDataEnvironmrntContributor
的幾個階段的處理旗唁,可以看其中的內(nèi)部類Kind
enum Kind {
//包含了所有的Contributors
ROOT,
//上面我們剛創(chuàng)建就屬于這個狀態(tài)
INITIAL_IMPORT,
//已經(jīng)將內(nèi)部PropertySource應(yīng)用到Environment中的Contributors
EXISTING,
//剛解析構(gòu)造好ConfigData,還沒有綁定spring.config / spring.profiles等環(huán)境參數(shù)
UNBOUND_IMPORT,
//已經(jīng)綁定好環(huán)境參數(shù)階段
BOUND_IMPORT;
}
接下來繼續(xù)跟processAndApply()
方法
-
processInitial
: 處理Kind為INITIAL_IMPORT
類型的Contributros 检疫,這里面也是主要的解析配置的地方
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
ConfigDataActivationContext activationContext) {
//1. 獲取Import階段屎媳,分導(dǎo)入前導(dǎo)入后
ImportPhase importPhase = ImportPhase.get(activationContext);
this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
(activationContext != null) ? activationContext : "no activation context"));
ConfigDataEnvironmentContributors result = this;
int processed = 0;
while (true) {
//1階段. 初始化為null
//2階段. 設(shè)置好ActivationContext(相關(guān)云計算平臺參數(shù)進(jìn)行第二輪的迭代),進(jìn)行相關(guān)云平臺過濾
//3階段. 進(jìn)行profile文件的解析和加載
ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
if (contributor == null) {
this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
return result;
}
if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
//從UNBOUND_IMPORT Contributor中獲取配置屬性源
Iterable<ConfigurationPropertySource> sources = Collections
.singleton(contributor.getConfigurationPropertySource());
// 進(jìn)行占位符解析
PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
result, activationContext, true);
Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
// 綁定ConfigDataProperties 并進(jìn)行替換
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
result.getRoot().withReplacement(contributor, bound));
continue;
}
//2.封裝Resolver,Loader等相關(guān)操作上下文對象
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
result, contributor, activationContext);
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
//3. 從ConfigDataLocationContributor(ConfigDataProperties)中獲取ConfigDataLocation(資源路徑對象)
List<ConfigDataLocation> imports = contributor.getImports();
this.logger.trace(LogMessage.format("Processing imports %s", imports));
//4. 解析到Map<ConfigDataResource, ConfigData>
Map<ConfigDataResource, ConfigData> imported = importer.resolveAndLoad(activationContext,
locationResolverContext, loaderContext, imports);
this.logger.trace(LogMessage.of(() -> imported.isEmpty() ? "Nothing imported" : "Imported "
+ imported.size() + " resource " + ((imported.size() != 1) ? "s" : "") + imported.keySet()));
ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
asContributors(imported));
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext, //返回設(shè)置好Child Contributor的結(jié)果集,然后繼續(xù)下一次迭代
result.getRoot().withReplacement(contributor, contributorAndChildren));
processed++;
}
}
-
getNextToProcess()
: 用來獲取ConfigDataEnvironmentContributes
中下次個滿足解析的Contributor
private ConfigDataEnvironmentContributor getNextToProcess(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, ImportPhase importPhase) { for (ConfigDataEnvironmentContributor contributor : contributors.getRoot()) { /** * 1. 剛進(jìn)來是INITIAL_IMPORT * 2. activationContext = null * 3. importPhase = BEFORE_PROFILE_ACTIVATION */ if (contributor.getKind() == Kind.UNBOUND_IMPORT || isActiveWithUnprocessedImports(activationContext, importPhase, contributor)) { return contributor; } } return null; } private boolean isActiveWithUnprocessedImports(ConfigDataActivationContext activationContext, ImportPhase importPhase, ConfigDataEnvironmentContributor contributor) { //ConfigDataProperties -> ConfigDataActivationContext (前兩者為null為true) ( onCloudPlatform -> Profiles) (為null為true/匹配當(dāng)前環(huán)境) return contributor.isActive(activationContext) && contributor.hasUnprocessedImports(importPhase); } //下面是一些列的判斷方法,依次遞進(jìn)。返回true表示當(dāng)前為激活環(huán)境双泪,現(xiàn)在階段Kind為INITIAL_IMPORT焙矛,且activation為null //所以會返回true boolean isActive(ConfigDataActivationContext activationContext) { return this.properties == null || this.properties.isActive(activationContext); } boolean isActive(ConfigDataActivationContext activationContext) { return this.activate == null || this.activate.isActive(activationContext); } boolean isActive(ConfigDataActivationContext activationContext) { if (activationContext == null) { return false; } boolean activate = true; activate = activate && isActive(activationContext.getCloudPlatform()); activate = activate && isActive(activationContext.getProfiles()); return activate; }
- 構(gòu)造
ConfigDataLocationResolver
薄扁,ConfigDataLoader
等上下文對象
//2.封裝Resolver,Loader等相關(guān)操作上下文對象 ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext( result, contributor, activationContext); ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
- 獲取默認(rèn)的定位資源并進(jìn)行解析加載
//3. 從ConfigDataLocationContributor(ConfigDataProperties)中獲取ConfigDataLocation(資源路徑對象) List<ConfigDataLocation> imports = contributor.getImports(); this.logger.trace(LogMessage.format("Processing imports %s", imports)); //4. 解析到Map<ConfigDataResource, ConfigData> Map<ConfigDataResource, ConfigData> imported = importer.resolveAndLoad(activationContext, locationResolverContext, loaderContext, imports);
-
resolveAndLoad
: 解析configDataLocation
為ConfigDataResource
,隨后ConfingDataLoader#load
為ConfigData
脱盲,并返回Map<ConfigDataResource, ConfigData>
映射關(guān)系钱反,具體解析流程匣距,我直接截取了最核心的解析和加載代碼毅待,如下
Map<ConfigDataResource, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext, ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext, List<ConfigDataLocation> locations) { try { //1. 初始化import階段profile為空 , 這個第三階段會派上用場 Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null; //2. 使用ConfigDateLocationResolver進(jìn)行加載和解析 // ConfigDataResolutionResult 包含了ConfigDataLocation 和ConfigDataResource(解析結(jié)果) List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations); //3. 使用ConfigDataLoader將ConfigDataResource -> ConfigData -> (PropertySource) return load(loaderContext, resolved); } catch (IOException ex) { throw new IllegalStateException("IO error on loading imports from " + locations, ex); } } // resolver() 核心 ,根這上面resolve方法一直跟就找到了,ConfigDataLocationResolvers#resolve() private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver, ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) { //進(jìn)行解析 List<ConfigDataResolutionResult> resolved = resolve(location, () -> resolver.resolve(context, location)); if (profiles == null) { return resolved; } //下面是第三階段用來進(jìn)行profile環(huán)境加載 List<ConfigDataResolutionResult> profileSpecific = resolve(location, () -> resolver.resolveProfileSpecific(context, location, profiles)); return merge(resolved, profileSpecific); } //ConfigDataImport#load() , 核心使用剛才加載到的ConfigDataResource列表進(jìn)行ConfigDataLoader#load加載 //我們可以通過在META-INF/spring.factories中配置我們自己實現(xiàn)的ConfigDataLoader進(jìn)行擴(kuò)展加載其他格式的外部化環(huán)境吱涉, // 比如最后我會演示擴(kuò)展實現(xiàn)一個加載json文件的Loader private Map<ConfigDataResource, ConfigData> load(ConfigDataLoaderContext loaderContext, List<ConfigDataResolutionResult> candidates) throws IOException { Map<ConfigDataResource, ConfigData> result = new LinkedHashMap<>(); //1. 從后向前迭代ConfigDataResolutionResult(包含ConfigDataLocation,ConfigDataResource) //2. 這里有個細(xì)節(jié)怎爵,為什么是從后往前遍歷鳖链?因為之前解析profile的時候是從優(yōu)先級低 -> 高 for (int i = candidates.size() - 1; i >= 0; i--) { ConfigDataResolutionResult candidate = candidates.get(i); ConfigDataLocation location = candidate.getLocation(); ConfigDataResource resource = candidate.getResource(); if (this.loaded.add(resource)) { //set緩存并去重 try { //2. ConfigDataLoader加載將ConfigDataResource -> ConfigData (PropetySource)又是一個擴(kuò)展點 ConfigData loaded = this.loaders.load(loaderContext, resource); if (loaded != null) { result.put(resource, loaded); } } catch (ConfigDataNotFoundException ex) { handle(ex, location); } } } return Collections.unmodifiableMap(result); }
這邊核心的三步我們就完成了兩步芙委,解析和加載题山,隨后就是一些重復(fù)邏輯顶瞳,加載另外兩階段的配置慨菱,這邊挑一些細(xì)節(jié)來展示,我們回到
ConfigDataEnvironment#processAndApply()
符喝,剛剛執(zhí)行完processInitia()
方法邏輯协饲,解析和加載了第一階段,隨便進(jìn)行云計算廠商的配置整合描馅,核心在createActivationContext()
void processAndApply() { //1. 封裝ConfigDataImporter對象铭污,里面有解析ConfigDataLocation -> ConfigDataResource 和load ConfigDataResource -> ConfigData之類的操作 ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers, this.loaders); this.bootstrapContext.register(Binder.class, InstanceSupplier .from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE))); //1. 加載和解析ConfigDataLocation -> ConfigDataResource -> ConfigData ,此時還沒有導(dǎo)入到Environment中嘹狞,執(zhí)行完畢之后應(yīng)該都是BOUND_IMPORT,且此時綁定了spring.config / spring.profiles相關(guān)的配置屬性信息 ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer); //2. 獲取包含Root Contributor中 所有ConfigurationPropertySource的Binder Binder initialBinder = contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE); //3. 重新注冊Binder到Bootstrapper中 this.bootstrapContext.register(Binder.class, InstanceSupplier.of(initialBinder)); ConfigDataActivationContext activationContext = createActivationContext(initialBinder); //構(gòu)建激活的上下文對象,此時對元計算平臺進(jìn)行設(shè)置 //4. 帶云計算平臺參數(shù)上下文進(jìn)行二次迭代 contributors = processWithoutProfiles(contributors, importer, activationContext); //5. 構(gòu)建profile activationContext = withProfiles(contributors, activationContext); //6. 帶profile參數(shù)進(jìn)行第三次迭代 contributors = processWithProfiles(contributors, importer, activationContext); //7. 應(yīng)用到Environment對象中 applyToEnvironment(contributors, activationContext); }
- 自動探測和整合第三場云廠商 知市,如k8s等.詳細(xì)可以參考
CloudPlatform
這個類,里面有自動探測和通過配置相關(guān)環(huán)境變量的方法來進(jìn)行設(shè)置
private CloudPlatform deduceCloudPlatform(Environment environment, Binder binder) { for (CloudPlatform candidate : CloudPlatform.values()) { //嘗試從Environment上下文中獲取spring.main.cloud-platform,若有指定對應(yīng)的云計算廠商則直接返回對應(yīng)的CloudPlatform if (candidate.isEnforced(binder)) { return candidate; } } //從環(huán)境變量中尋找是否有對應(yīng)云平臺的環(huán)境變量參數(shù),比如k8s(svc相關(guān)環(huán)境參數(shù)): KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT return CloudPlatform.getActive(environment); }
- 獲取Environment中的profile屬性
private ConfigDataActivationContext withProfiles(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext) { this.logger.trace("Deducing profiles from current config data environment contributors"); Binder binder = contributors.getBinder(activationContext, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE); try { //優(yōu)先設(shè)置構(gòu)造SpringApplication的addtionalProfile Set<String> additionalProfiles = new LinkedHashSet<>(this.additionalProfiles); //設(shè)置include profile additionalProfiles.addAll(getIncludedProfiles(contributors, activationContext)); //設(shè)置active profile 规哲、 default profile Profiles profiles = new Profiles(this.environment, binder, additionalProfiles); return activationContext.withProfiles(profiles); } catch (BindException ex) { if (ex.getCause() instanceof InactiveConfigDataAccessException) { throw (InactiveConfigDataAccessException) ex.getCause(); } throw ex; } }
- 隨后完成第三階段的解析和加載以及最后應(yīng)用到Environment中
//6. 帶profile參數(shù)進(jìn)行第三次迭代 contributors = processWithProfiles(contributors, importer, activationContext); //7. 應(yīng)用到Environment對象中 applyToEnvironment(contributors, activationContext);
2.3 兩種版本不同的加載優(yōu)先級如下
//springboot2.4之前 //location優(yōu)先級為: spring.config.addtional-location > spring.config.location or default //這里的default指springboot默認(rèn)加載位置 classpath:/ classpath:/config/ ... //profile優(yōu)先級: //spring.profiles.active > spring.profiles.include //且這里如果有spring.profiles指定的多環(huán)境格式,如下,此時加載test環(huán)境的時候袄简,spring.profiles=test也會隨后加載 name=default spring.profiles.active=dev,prod spring.profiles.include=config,test #--- spring.profiles=test name=default#test //springboot2.4之后 //location優(yōu)先級為: spring.config.import > addtionial-location > location //profile優(yōu)先級 //spring.profiles.include > active(之間還多了一個spring.config.group)
總結(jié):
springboot2.4和之前版本實現(xiàn)有較大差距绿语,前者擴(kuò)展了通過spring.config.import導(dǎo)入資源吕粹,并且資源加載來源更加寬廣了匹耕,springboot內(nèi)建的實現(xiàn)甚至可以從svn中加載配置。而下面也將進(jìn)行簡單的兩個版擴(kuò)展配置的方式
spring boot 2.4之前, 只需要實現(xiàn)
PropertySourceLoader
接口然后添加到META-INF/spring.factories
即可- 自定義
CustomPropertySourceLoader
//自定義json后綴資源加載器 public class CustomPropertySourceLoader implements PropertySourceLoader { public static final String CUSTOM_PREFIX = "json"; @Override public String[] getFileExtensions() { return new String[]{CUSTOM_PREFIX}; } @Override public List<PropertySource<?>> load(String name, Resource resource) throws IOException { Map<String, Object> result = new ObjectMapper().readValue(resource.getURL(),new TypeReference<Map<String,Object>>(){}); return Collections.singletonList(new MapPropertySource("JSON_PROPERTY_SOURCE", result)); } }
- 配置文件
#自定義ConfigDataLocationResolver -> ConfigDataLocation -> ConfigDataResource org.springframework.boot.context.config.ConfigDataLocationResolver=\ boot.in.action.bootsourcelearning.configdata.CustomConfigDataLocationResolver
spring boot2.4擴(kuò)展
- 實現(xiàn)
ConfigDataLocationResolver
, 和ConfigDataResource
, 這種自定義實現(xiàn)將可以解析custom:
前綴的資源煤傍,實現(xiàn)參考了ConfigTreeDataLocationResolver
public class CustomConfigDataLocationResolver implements ConfigDataLocationResolver<CustomConfigDataResource> { public static final String CUSTOM_CONFIG_PREFIX = "custom:"; @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { return location.hasPrefix(CUSTOM_CONFIG_PREFIX) && location.getValue().endsWith(".properties"); } @Override public List<CustomConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) { List<CustomConfigDataResource> result = new ArrayList<>(); try { Resource[] resources = new PathMatchingResourcePatternResolver().getResources(location.getValue().substring(CUSTOM_CONFIG_PREFIX.length())); for (Resource resource : resources) { result.add(new CustomConfigDataResource(PropertiesLoaderUtils.loadProperties(resource))); } } catch (IOException e) { if (location.isOptional()) { log.warn("not found resource :{}", location.getValue()); } else { ReflectionUtils.rethrowRuntimeException(e); } } return result; } }
- 實現(xiàn)
ConfigDataLoader
public class CustomConfigDataLoader implements ConfigDataLoader<CustomConfigDataResource> { @Override public ConfigData load(ConfigDataLoaderContext context, CustomConfigDataResource resource) throws ConfigDataResourceNotFoundException { Properties properties = resource.getProperties(); return new ConfigData(Collections.singleton(new PropertiesPropertySource("FILE_PROPERTY_SOURCE", properties))); } }
- 配置文件
#自定義ConfigDataLocation ConfigDataResource->ConfigData org.springframework.boot.context.config.ConfigDataLoader=\ boot.in.action.bootsourcelearning.configdata.CustomConfigDataLoader org.springframework.boot.env.PropertySourceLoader=\ boot.in.action.bootsourcelearning.configdata.CustomPropertySourceLoader
- 測試輸入程序如下
public static void main(String[] args) { ConfigurableApplicationContext context = new SpringApplicationBuilder(BootSourceLearningApplication.class) // .properties("spring.config.use-legacy-processing=true") .properties("spring.config.additional-location=classpath:/custom/") .properties("spring.config.import=classpath:/custom/custom.json,optional:custom:/custom/custom.properties") .applicationStartup(new BufferingApplicationStartup(2048)) .web(WebApplicationType.SERVLET) .run(args); context.getEnvironment().getPropertySources().forEach(System.out::println); } //輸出結(jié)果如下,成功加載custom前綴和 .json后綴的PropertySource MapPropertySource {name='server.ports'} ConfigurationPropertySourcesPropertySource {name='configurationProperties'} StubPropertySource {name='servletConfigInitParams'} ServletContextPropertySource {name='servletContextInitParams'} PropertiesPropertySource {name='systemProperties'} OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'} RandomValuePropertySource {name='random'} PropertiesPropertySource {name='FILE_PROPERTY_SOURCE'} //custom前綴 MapPropertySource {name='JSON_PROPERTY_SOURCE'} //.json后綴
- 構(gòu)造