溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

SpringBoot外部化配置如何使用Plus版

發(fā)布時間:2020-07-21 16:05:45 來源:億速云 閱讀:116 作者:小豬 欄目:編程語言

小編這次要給大家分享的是SpringBoot外部化配置如何使用Plus版,文章內(nèi)容豐富,感興趣的小伙伴可以來了解一下,希望大家閱讀完這篇文章之后能夠有所收獲。

序言

上一篇博客記錄,主要集中在具體的配置內(nèi)容,也就是使用 @ConfigurationProperties 這個注解來進行配置與結(jié)構(gòu)化對象的綁定,雖然也順帶說了下 @Value 的使用以及其區(qū)別。

在這篇記錄中,打算從總覽,鳥瞰的俯視視角,來從整體上對 SpringBoot ,乃至 Spring Framework 對于外部化配置文件處理,以及配置參數(shù)的綁定操作,是如果處理的、怎么設計的。

這里其實主要說的是 SpringBoot ,雖然 @Value 屬于 Spring Framework 的注解,不過在 SpringBoot 中也被頻繁使用。

SpringBoot 版本: 2.2.6.RELEASE

SpringBoot啟動流程簡介

在 SpringBoot 的啟動過程中,大體上分為三步

第一步: prepareEnvironment ,準備 SpringBoot 執(zhí)行時所有的配置。

第二步: prepareContext ,根據(jù)啟動時的傳入的配置類,創(chuàng)建其 BeanDefinition 。

第三步: refreshContext ,真正啟動上下文。

在這上面三步中,第一步結(jié)束后,我們所需要的或者配置文件配置的內(nèi)容,大部分已經(jīng)被加載進來,然后在第三步中進行配置的注入或者綁定操作。

至于為什么是大部分,后面會有解釋。

將配置從配置文件加載到Environment中,使用的是事件通知的方式。

本篇博客記錄僅僅聚焦第一步中如何讀取配置文件的分析,順帶介紹下第三步的注入和綁定。

受限于技術(shù)水平,僅能達到這個程度

外部化配置方式

如果有看到 SpringBoot 官網(wǎng)關(guān)于外部化配置的說明,就會驚訝的發(fā)現(xiàn),原來 SpringBoot 有那么多的配置來源。

SpringBoot 關(guān)于外部化配置特性的文檔說明,直達 地址 。

而實際使用中,通??赡軙褂玫谋容^多的是通過以下這些 方式

commandLine

通過在啟動jar時,加上 -DconfigKey=configValue 或者 --configKey=configValue 的方式,來進行配置,多個配置項用空格分隔。

這種使用場景也多,只是一般用于一些配置內(nèi)容很少且比較關(guān)鍵的配置,比如說可以決定運行環(huán)境的配置。

不易進行比較多的或者配置內(nèi)容比較冗長的配置,容易出錯,且不便于維護管理。

application

這種是 SpringBoot 提供的,用于簡便配置的一種方式,只要我們將應用程序所用到的配置,直接寫到 application.properties 中,并將文件放置于以下四個位置即可 。

  1. 位于 jar 同目錄的 config 目錄下的 application.properties
  2. 位于 jar 同目錄的 application.properties
  3. classpath 下的 config 內(nèi) application.properties
  4. classpath 下的 application.properties

以上配置文件類型也都可以使用yml

默認情況下,這種方式是 SpringBoot 約定好的一種方式,文件名必須為 application ,文件內(nèi)容格式可以為 Yaml 或者 Properties ,也許支持 XML ,因為看源碼是支持的,沒有實踐。

好處就是簡單,省心省事,我們只需關(guān)注文件本身的內(nèi)容就可,其他的無需關(guān)心,這也是 SpringBoot 要追求的結(jié)果。

缺點也很明顯,如果配置內(nèi)容比較冗長,為了便于管理維護,增加可讀性,必須要對配置文件進行切分,通過功能等維度進行分類分組,使用多個配置文件來進行存放配置數(shù)據(jù)。

SpringBoot 也想到了這些問題,因此提供了下面兩個比較方便的使用方式,來應對這種情況

profiles

profiles 本身是也是一個配置項,它提供一種方式將部分應用程序配置進行隔離,并且使得它僅在具體某一個環(huán)境中可用。

具體實踐中常用的主要是針對不同的環(huán)境,有開發(fā)環(huán)境用到的特有配置值,有測試環(huán)境特有的配置,有生產(chǎn)環(huán)境特有的配置,包括有些 Bean 根據(jù)環(huán)境選擇決定是否進行實例化,這些都是通過 profiles 來實現(xiàn)的。不過這里只關(guān)注配置這一塊內(nèi)容。

它的使用方式通常是 spring.profiles.active=dev,dev1 或者 spring.profiles.include=db1,db2

這里可以看到有兩種不同的用法,這兩種方式是有區(qū)別的。

如果在 application.properties 中定義了一個 spring.profiles.active=dev ,而后在啟動時通過 命令行又寫了個 --spring.profiles.active=test ,那么最終使用的是 test ,而不是 dev 。

如果同樣的場景下,使用 spring.profiles.include 來替換 spring.profiles.active ,那么結(jié)果會是 dev 和 test 都會存在,而不是替換的行為 。

這就是兩個之間的差別,這種差別也使得他們使用的場景并不一樣, active 更適合那些需要互斥的環(huán)境,而 include 則是多個并存的配置。

僅僅配置了 profiles 是沒有意義的,必須要有相應的配置文件配合一起使用,而且這些配置文件的命名要符合一定的規(guī)則,否則配置文件不會被加載進 Environment 的。

profiles 文件的命名規(guī)則為 application-*.properties ,同樣的, application.properties 能放置的位置它也可以,不能的,它也不可以。

propery source

注解 @PropertySource 可以寫在配置類上,并且指定要讀取的配置文件路徑,這個路徑可以是絕對路徑,也可以是相對路徑。

它可以有以下幾種配置

  • @PropertySource("/config.properties")
  • @PropertySource("config.properties")
  • @PropertySource("file:/usr/local/config.properties")
  • @PropertySource("file:./config.properties")
  • @PropertySource("${pathPrefix}/config.properties")

其中1和2兩種方式是一樣的,都是從 classpath 去開始查找的

3和4是使用文件系統(tǒng)的絕對和相對路徑的方式,這里絕對路徑比較好理解 ,相對路徑則是從項目的根目錄作為相對目錄的

5是結(jié)合 SpEL 的表達式來使用的,可以直接從環(huán)境中獲取配置好的路徑。

以上幾種方式在實際開發(fā)中遇到和 SpringBoot 相關(guān)的配置,基本都能應付過來了。

不過對于上面配置的一些原理性的內(nèi)容,還沒有提到 ,下面會簡單說一下 SpringBoot 關(guān)于配置更詳細的處理,以及配置的優(yōu)先級的問題。

原理淺入淺出

帶著問題去找原因,比較有目的性和針對性,效果也相對好一些。

所以這里描述幾個會引起疑問的現(xiàn)象

默認情況下自動加載的配置文件命名必須要是 application

在使用 application.properties 時,可以同時在四個位置放置配置,配置的優(yōu)先級就是上面羅列時顯示的優(yōu)先級。同樣的配置,優(yōu)先級高的生效,優(yōu)先級低的忽略。

profiles 引入的配置,也準守同樣的優(yōu)先級規(guī)則

命令行配置具有最高優(yōu)先級

有些配置不能使用 @PropertySource 的方式進行注入,比如日志的配置。

如果一個配置類使用了 @ConfigurationProperties ,然后字段使用了 @Value , @ConfigurationProperties 先被處理, @Value 后被處理。

源碼簡讀

SpringBoot 讀取 application.properties 配置

查看 org.springframework.boot.context.config.ConfigFileApplicationListener 的源碼

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
 // Note the order is from least to most specific (last one wins)
 // 默認檢索配置文件的路徑,優(yōu)先級越來越高,
 // 可以通過 spring.config.location重新指定,要早于當前類執(zhí)行時配置好
 private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
 // 默認的配置名,可以通過命令行配置--spring.config.name=xxx來重新指定
 // 不通過命令行也可以通過其他方式,環(huán)境變量這些。
 private static final String DEFAULT_NAMES = "application";
 
 private class Loader {
 // 找到配置的路徑
 private Set<String> getSearchLocations() {
  if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
  return getSearchLocations(CONFIG_LOCATION_PROPERTY);
  }
  Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
  locations.addAll(
  asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
  return locations;
 }
 // 解析成Set
 private Set<String> asResolvedSet(String value, String fallback) {
 List<String> list = Arrays.asList(StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(
 (value != null) &#63; this.environment.resolvePlaceholders(value) : fallback)));
 // 這里會做一個反轉(zhuǎn),也就是配置的路徑中,放在后面的優(yōu)先級越高
  Collections.reverse(list);
 return new LinkedHashSet<>(list);
 }
 private Set<String> getSearchNames() {
 if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
 String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
 return asResolvedSet(property, null);
 }
 return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
 }
 } 
}

命令行的配置具有最高優(yōu)先級

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
 MutablePropertySources sources = environment.getPropertySources();
 if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
 sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
 }
 // 支持從命令行添加屬性以及存在參數(shù)時
 if (this.addCommandLineProperties && args.length > 0) {
 String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
 // 這里是看下是不是存在同名的配置了
 if (sources.contains(name)) {
  PropertySource<&#63;> source = sources.get(name);
  CompositePropertySource composite = new CompositePropertySource(name);
  composite.addPropertySource(
  new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
  composite.addPropertySource(source);
  sources.replace(name, composite);
 }
 else {
  // 直接添加,并且是添加到第一個位置,具有最高優(yōu)先級
  sources.addFirst(new SimpleCommandLinePropertySource(args));
 }
 }
}

@PropertySource 是在 refreshContext 階段,執(zhí)行 BeanDefinitionRegistryPostProcessor 時處理的

// org.springframework.context.annotation.ConfigurationClassParser#processPropertySource
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
 String name = propertySource.getString("name");
 if (!StringUtils.hasLength(name)) {
 name = null;
 }
 String encoding = propertySource.getString("encoding");
 if (!StringUtils.hasLength(encoding)) {
 encoding = null;
 }
 // 獲取配置的文件路徑
 String[] locations = propertySource.getStringArray("value");
 Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
 boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
 // 指定的讀取配置文件的工廠
 Class<&#63; extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
 // 沒有就用默認的 
 PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class &#63;
  DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
 // 循環(huán)加載
 for (String location : locations) {
 try {
  // 會解析存在占位符的情況
  String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
  // 使用DefaultResourceLoader來加載資源
  Resource resource = this.resourceLoader.getResource(resolvedLocation);
  // 創(chuàng)建PropertySource對象
  addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
 }
 catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
  // Placeholders not resolvable or resource not found when trying to open it
  if (ignoreResourceNotFound) {
  if (logger.isInfoEnabled()) {
   logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
  }
  }
  else {
  throw ex;
  }
 }
 }
}

因為執(zhí)行時機的問題,有些配置不能使用 @PropertySource ,因為這個時候?qū)τ行┡渲脕碚f,如果使用這種配置方式,黃花菜都涼了。同時這個注解要配合 @Configuration 注解一起使用才能生效,使用 @Component 是不行的。

處理 @ConfigurationProperty 的處理器是一個 BeanPostProcessor ,處理 @Value 的也是一個 BeanPostProcessor ,不過他倆的優(yōu)先級并不一樣,

// @ConfigurationProperty
public class ConfigurationPropertiesBindingPostProcessor
 implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {
 @Override
 public int getOrder() {
 return Ordered.HIGHEST_PRECEDENCE + 1;
 }

}
// @Value
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
 implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
 private int order = Ordered.LOWEST_PRECEDENCE - 2;
 @Override
 public int getOrder() {
 return this.order;
 }
}

從上面可以看出處理 @ConfigurationProperty 的 BeanPostProcessor 優(yōu)先級很高,而 @Value 的 BeanPostProcessor 優(yōu)先級很低。

使用 @Value 注入時,要求配置的 key 必須存在于 Environment 中的,否則會終止啟動,而 @ConfigurationProperties 則不會。

@Value 可以支持 SpEL 表達式,也支持占位符的方式。

自定義配置讀取

org.springframework.boot.context.config.ConfigFileApplicationListener 是一個監(jiān)聽器,同時也是一個 EnvironmentPostProcessor ,在有 ApplicationEnvironmentPreparedEvent 事件觸發(fā)時,會去處理所有的 EnvironmentPostProcessor 的實現(xiàn)類,同時這些個實現(xiàn)也是使用 SpringFactoriesLoader 的方式來加載的。

對于配置文件的讀取,就是使用的這種方式。

@Override
public void onApplicationEvent(ApplicationEvent event) {
 if (event instanceof ApplicationEnvironmentPreparedEvent) {
 onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
 }
 if (event instanceof ApplicationPreparedEvent) {
 onApplicationPreparedEvent(event);
 }
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
 List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
 postProcessors.add(this);
 AnnotationAwareOrderComparator.sort(postProcessors);
 for (EnvironmentPostProcessor postProcessor : postProcessors) {
 postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
 }
}

有了這個擴展點后,我們就能自己定義讀取任何配置,從任何地方。

只要實現(xiàn)了 EnvironmentPostProcessor 接口,并且在 META-INF/spring.factories 中配置一下

org.springframework.boot.env.EnvironmentPostProcessor=com.example.configuration.ConfigurationFileLoader

附一個自己寫的例子

public class ConfigurationFileLoader implements EnvironmentPostProcessor {

 private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
 private static final String DEFAULT_NAMES = "download";
 private static final String DEFAULT_FILE_EXTENSION = ".yml";


 @Override
 public void postProcessEnvironment (ConfigurableEnvironment environment,
     SpringApplication application) {

 List<String> list = Arrays.asList(StringUtils.trimArrayElements(
  StringUtils.commaDelimitedListToStringArray(DEFAULT_SEARCH_LOCATIONS)));
 Collections.reverse(list);
 Set<String> reversedLocationSet = new LinkedHashSet(list);
 ResourceLoader defaultResourceLoader = new DefaultResourceLoader();
 YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
 List<Properties> loadedProperties = new ArrayList<>(2);
 reversedLocationSet.forEach(location->{
  Resource resource = defaultResourceLoader.getResource(location + DEFAULT_NAMES+DEFAULT_FILE_EXTENSION);
  if (resource == null || !resource.exists()) {
  return;
  }
  yamlPropertiesFactoryBean.setResources(resource);
  Properties properties = yamlPropertiesFactoryBean.getObject();
  loadedProperties.add(properties);
 });

 Properties filteredProperties = new Properties();
 Set<Object> addedKeys = new LinkedHashSet<>();
 for (Properties propertySource : loadedProperties) {
  for (Object key : propertySource.keySet()) {
  String stringKey = (String) key;
  if (addedKeys.add(key)) {
   filteredProperties.setProperty(stringKey, propertySource.getProperty(stringKey));
  }

  }
 }
 PropertiesPropertySource propertySources = new PropertiesPropertySource(DEFAULT_NAMES, filteredProperties);
 environment.getPropertySources().addLast(propertySources);
 }
}

基本上都是 參考 ConfigFileApplicationListener 寫的 ,不過這里實現(xiàn)的功能,其實可以通過 @PropertySource 來 解決,只是當時不知道。

使用 @PropertySource 的話,這么寫 @PropertySource("file:./download.properties") 即可。

個人猜測 SpringBoot 從配置中心加載配置就是使用的這個方式,不過由于沒有實際看過相關(guān)源碼確認,不敢說一定是的 ,但是應該是八九不離十 的 。

總結(jié)

這篇記錄寫的有點亂,一個是涉及到東西感覺也不少,還有就是本身有些地方不怎么了解,花費的時間不夠。

不過對 SpringBoot 的外部化配置來說,就是將各個途徑加載進來的配置,統(tǒng)一收歸 Environment 的 MutablePropertySources 字段,這個字段是一個 ArrayList ,保持添加進來時的順序,因此查找也是按照這個順序查找,查找時查到即返回,不會完全遍歷所有的配置,除非遇到不存在的。

整個設計思想就是使用集中所有的配置,進行優(yōu)先級排序,最后在有需要獲取配置的地方,從 Environment 對象中查找配置項。

對一般使用來說,關(guān)注點就是配置文件的位置,配置文件的名,以及優(yōu)先級,這三個方面比較關(guān)心。

看完這篇關(guān)于SpringBoot外部化配置如何使用Plus版的文章,如果覺得文章內(nèi)容寫得不錯的話,可以把它分享出去給更多人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI