SpringBoot外部化配置使用Plus版的方法示例

PS: 之前写过一篇关于 SpringBoo 中使用配置文件的一些姿势,不过嘛,有句话(我)说的好:曾见小桥流水,未睹观音坐莲!所以再写一篇增强版,以便记录。

序言

上一篇博客记录,主要集中在具体的配置内容,也就是使用 @ConfigurationProperties 这个注解来进行配置与结构化对象的绑定,虽然也顺带说了下 @Value 的使用以及其区别。

在这篇记录中,打算从总览,鸟瞰的俯视视角,来从整体上对 SpringBoot ,乃至 Spring Framework 对于外部化配置文件处理,以及配置参数的绑定操作,是如果处理的、怎么设计的。

这里其实主要说的是 SpringBoot ,虽然 @Value 属于 Spring Framework 的注解,不过在 SpringBoot 中也被频繁使用。

SpringBoot 版本: 2.2.6.RELEASE

SpringBoot启动流程简介

在 SpringBoot 的启动过程中,大体上分为三步

第一步: prepareEnvironment ,准备 SpringBoot 执行时所有的配置。

第二步: prepareContext ,根据启动时的传入的配置类,创建其 BeanDefinition 。

第三步: refreshContext ,真正启动上下文。

在这上面三步中,第一步结束后,我们所需要的或者配置文件配置的内容,大部分已经被加载进来,然后在第三步中进行配置的注入或者绑定操作。

至于为什么是大部分,后面会有解释。

将配置从配置文件加载到Environment中,使用的是事件通知的方式。

本篇博客记录仅仅聚焦第一步中如何读取配置文件的分析,顺带介绍下第三步的注入和绑定。

受限于技术水平,仅能达到这个程度

外部化配置方式

如果有看到 SpringBoot 官网关于外部化配置的说明,就会惊讶的发现,原来 SpringBoot 有那么多的配置来源。

SpringBoot 关于外部化配置特性的文档说明,直达 地址

而实际使用中,通常可能会使用的比较多的是通过以下这些 方式

commandLine

通过在启动jar时,加上 -DconfigKey=configValue 或者 --configKey=configValue 的方式,来进行配置,多个配置项用空格分隔。

这种使用场景也多,只是一般用于一些配置内容很少且比较关键的配置,比如说可以决定运行环境的配置。

不易进行比较多的或者配置内容比较冗长的配置,容易出错,且不便于维护管理。

application

这种是 SpringBoot 提供的,用于简便配置的一种方式,只要我们将应用程序所用到的配置,直接写到 application.properties 中,并将文件放置于以下四个位置即可 。

  1. 位于 jar 同目录的 config 目录下的 application.properties
  2. 位于 jar 同目录的 application.properties
  3. classpath 下的 config 内 application.properties
  4. classpath 下的 application.properties

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

默认情况下,这种方式是 SpringBoot 约定好的一种方式,文件名必须为 application ,文件内容格式可以为 Yaml 或者 Properties ,也许支持 XML ,因为看源码是支持的,没有实践。

好处就是简单,省心省事,我们只需关注文件本身的内容就可,其他的无需关心,这也是 SpringBoot 要追求的结果。

缺点也很明显,如果配置内容比较冗长,为了便于管理维护,增加可读性,必须要对配置文件进行切分,通过功能等维度进行分类分组,使用多个配置文件来进行存放配置数据。

SpringBoot 也想到了这些问题,因此提供了下面两个比较方便的使用方式,来应对这种情况

profiles

profiles 本身是也是一个配置项,它提供一种方式将部分应用程序配置进行隔离,并且使得它仅在具体某一个环境中可用。

具体实践中常用的主要是针对不同的环境,有开发环境用到的特有配置值,有测试环境特有的配置,有生产环境特有的配置,包括有些 Bean 根据环境选择决定是否进行实例化,这些都是通过 profiles 来实现的。不过这里只关注配置这一块内容。

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

这里可以看到有两种不同的用法,这两种方式是有区别的。

如果在 application.properties 中定义了一个 spring.profiles.active=dev ,而后在启动时通过 命令行又写了个 --spring.profiles.active=test ,那么最终使用的是 test ,而不是 dev 。

如果同样的场景下,使用 spring.profiles.include 来替换 spring.profiles.active ,那么结果会是 dev 和 test 都会存在,而不是替换的行为 。

这就是两个之间的差别,这种差别也使得他们使用的场景并不一样, active 更适合那些需要互斥的环境,而 include 则是多个并存的配置。

仅仅配置了 profiles 是没有意义的,必须要有相应的配置文件配合一起使用,而且这些配置文件的命名要符合一定的规则,否则配置文件不会被加载进 Environment 的。

profiles 文件的命名规则为 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是使用文件系统的绝对和相对路径的方式,这里绝对路径比较好理解 ,相对路径则是从项目的根目录作为相对目录的

5是结合 SpEL 的表达式来使用的,可以直接从环境中获取配置好的路径。

以上几种方式在实际开发中遇到和 SpringBoot 相关的配置,基本都能应付过来了。

不过对于上面配置的一些原理性的内容,还没有提到 ,下面会简单说一下 SpringBoot 关于配置更详细的处理,以及配置的优先级的问题。

原理浅入浅出

带着问题去找原因,比较有目的性和针对性,效果也相对好一些。

所以这里描述几个会引起疑问的现象

默认情况下自动加载的配置文件命名必须要是 application

在使用 application.properties 时,可以同时在四个位置放置配置,配置的优先级就是上面罗列时显示的优先级。同样的配置,优先级高的生效,优先级低的忽略。

profiles 引入的配置,也准守同样的优先级规则

命令行配置具有最高优先级

有些配置不能使用 @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)
 // 默认检索配置文件的路径,优先级越来越高,
 // 可以通过 spring.config.location重新指定,要早于当前类执行时配置好
 private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
 // 默认的配置名,可以通过命令行配置--spring.config.name=xxx来重新指定
 // 不通过命令行也可以通过其他方式,环境变量这些。
 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) ? this.environment.resolvePlaceholders(value) : fallback)));
 // 这里会做一个反转,也就是配置的路径中,放在后面的优先级越高
  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);
 }
 }
}

命令行的配置具有最高优先级

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));
 }
 // 支持从命令行添加属性以及存在参数时
 if (this.addCommandLineProperties && args.length > 0) {
 String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
 // 这里是看下是不是存在同名的配置了
 if (sources.contains(name)) {
  PropertySource<?> source = sources.get(name);
  CompositePropertySource composite = new CompositePropertySource(name);
  composite.addPropertySource(
  new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
  composite.addPropertySource(source);
  sources.replace(name, composite);
 }
 else {
  // 直接添加,并且是添加到第一个位置,具有最高优先级
  sources.addFirst(new SimpleCommandLinePropertySource(args));
 }
 }
}

@PropertySource 是在 refreshContext 阶段,执行 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<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
 // 没有就用默认的
 PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
  DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
 // 循环加载
 for (String location : locations) {
 try {
  // 会解析存在占位符的情况
  String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
  // 使用DefaultResourceLoader来加载资源
  Resource resource = this.resourceLoader.getResource(resolvedLocation);
  // 创建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;
  }
 }
 }
}

因为执行时机的问题,有些配置不能使用 @PropertySource ,因为这个时候对有些配置来说,如果使用这种配置方式,黄花菜都凉了。同时这个注解要配合 @Configuration 注解一起使用才能生效,使用 @Component 是不行的。

处理 @ConfigurationProperty 的处理器是一个 BeanPostProcessor ,处理 @Value 的也是一个 BeanPostProcessor ,不过他俩的优先级并不一样,

// @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 优先级很高,而 @Value 的 BeanPostProcessor 优先级很低。

使用 @Value 注入时,要求配置的 key 必须存在于 Environment 中的,否则会终止启动,而 @ConfigurationProperties 则不会。

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

自定义配置读取

org.springframework.boot.context.config.ConfigFileApplicationListener 是一个监听器,同时也是一个 EnvironmentPostProcessor ,在有 ApplicationEnvironmentPreparedEvent 事件触发时,会去处理所有的 EnvironmentPostProcessor 的实现类,同时这些个实现也是使用 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());
 }
}

有了这个扩展点后,我们就能自己定义读取任何配置,从任何地方。

只要实现了 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 写的 ,不过这里实现的功能,其实可以通过 @PropertySource 来 解决,只是当时不知道。

使用 @PropertySource 的话,这么写 @PropertySource("file:./download.properties") 即可。

个人猜测 SpringBoot 从配置中心加载配置就是使用的这个方式,不过由于没有实际看过相关源码确认,不敢说一定是的 ,但是应该是八九不离十 的 。

总结

这篇记录写的有点乱,一个是涉及到东西感觉也不少,还有就是本身有些地方不怎么了解,花费的时间不够。

不过对 SpringBoot 的外部化配置来说,就是将各个途径加载进来的配置,统一收归 Environment 的 MutablePropertySources 字段,这个字段是一个 ArrayList ,保持添加进来时的顺序,因此查找也是按照这个顺序查找,查找时查到即返回,不会完全遍历所有的配置,除非遇到不存在的。

整个设计思想就是使用集中所有的配置,进行优先级排序,最后在有需要获取配置的地方,从 Environment 对象中查找配置项。

对一般使用来说,关注点就是配置文件的位置,配置文件的名,以及优先级,这三个方面比较关心。

这篇记录也基本能解答这几个疑问,完成了写这篇记录的初衷。

到此这篇关于SpringBoot外部化配置使用Plus版的方法示例的文章就介绍到这了,更多相关SpringBoot外部化配置Plus版内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot引用外部配置文件的方法步骤

    现在的项目越来越多的都是打包成jar运行尤其是springboot项目,这时候配置文件如果一直放在项目中,每次进行简单的修改时总会有些不方便,这里我们看下打包成jar之后,从外部配置文件中读取配置信息. 首先想到的是通过java代码读取外边某个路径下的文件,但是开始做之后发现好多问题.后来又找其它解决方案,正好搜到一种简单的解决方式: java -jar demo.jar --Dspring.config.location=myapplication.properties 这样就可以通过@val

  • 配置springboot项目使用外部tomcat过程解析

    这篇文章主要介绍了配置springboot项目使用外部tomcat过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在pom文件中添加依赖 <!--使用自带的tomcat--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifac

  • SpringBoot外部化配置使用Plus版的方法示例

    PS: 之前写过一篇关于 SpringBoo 中使用配置文件的一些姿势,不过嘛,有句话(我)说的好:曾见小桥流水,未睹观音坐莲!所以再写一篇增强版,以便记录. 序言 上一篇博客记录,主要集中在具体的配置内容,也就是使用 @ConfigurationProperties 这个注解来进行配置与结构化对象的绑定,虽然也顺带说了下 @Value 的使用以及其区别. 在这篇记录中,打算从总览,鸟瞰的俯视视角,来从整体上对 SpringBoot ,乃至 Spring Framework 对于外部化配置文件处

  • 详解关于SpringBoot的外部化配置使用记录

    更新: 工作中突然想起来,关于Yaml的使用,并不属于Spring的范畴,是org.yaml.snakeyaml处理的.所以yaml的使用应该参考官方,不过貌似打不开... Spring利用snakeyaml将配置解析成PropertySource,然后写入到Environment,就能使用了 记录下使用SpringBoot配置时遇到的一些麻烦,虽然这种麻烦是因为知识匮乏导致的. 记录下避免一段时间后自己又给忘记了,以防万一. 如果放到博客里能帮助到遇到同样问题的同志,自是极好! SpringB

  • SpringBoot扩展外部化配置的原理解析

    Environment实现原理 在基于SpringBoot开发的应用中,我们常常会在application.properties.application-xxx.properties.application.yml.application-xxx.yml等配置文件中设置一些属性值,然后通过@Value.@ConfigurationProperties等注解获取,或者采用编码的方式通过Environment获取. # application.properties my.config.appId=d

  • Spring Boot外部化配置实战解析

    一.流程分析 1.1 入口程序 在 SpringApplication#run(String... args) 方法中,外部化配置关键流程分为以下四步 public ConfigurableApplicationContext run(String... args) { ... SpringApplicationRunListeners listeners = getRunListeners(args); // 1 listeners.starting(); try { ApplicationA

  • Spring外部化配置的几种技巧分享

    目录 正文 Envrionment 获取外部配置 修改Spring默认配置文件名称 Value注解配置来源 外部化配置文件优先级问题 Autowire注入ConfigurableEnvrionment ApplicationInitialiazer 配置 总结 正文 Envrionment 获取外部配置 @Log4j2 @SpringBootApplication public class ConfigurationApplication { public static void main(St

  • springboot 多环境配置 yml文件版的实现方法

    关于 dev.sit.uat.prod多环境切换的配置 最近小伙伴跟杨洋我聊到了多环境配置的问题,网上的大部分教程都是copy的,很多文章根本就没法用,小伙伴很苦恼啊,于是心(yu)地(shu)善(lin)良(feng)的杨洋回去写了个demo给了小伙 , 那么这边文章呢,正好给大家讲解下关于springboot 的多环境配置 科普时间:  dev.sit.uat.prod是什么呢? 首先给刚接触的小伙伴们科普下含义 dev--本地开发环境: sit--测试环境: uat--准生产环境: pro

  • Spring boot读取外部化配置的方法

    目录 1. Properties / YAML 1.1 Environment 1.2 Value注解 2. 自定义Properties文件 3. 其他命令参数 总结 这篇文章我们主要讨论 Spring Boot 的外部化配置功能,该功能主要是通过外部的配置资源实现与代码的相互配合,来避免硬编码,提供应用数据或行为变化的灵活性.本文主要记录读取外部化配置的几种常见的操作方式,相关原理不在此记录. 1. Properties / YAML 我们一般会将相关配置信息写在Properties / YA

  • springboot整合阿里云oss上传的方法示例

    OSS申请和配置 1. 注册登录 输入网址:https://www.aliyun.com/product/oss 如果没有账号点击免费注册,然后登录. 2.开通以及配置 点击立即开通 进入管理控制台 第一次使用会出现引导,按引导点击"我知道了",然后点击创建Bucket. 如果没有存储包或流量包点击购买. 点击确定,返回主页面,出现该页面,点击我知道了 将EndPoint记录下来,方便后期添加到我们项目的配置文件中 创建 AccessKeyID 和 AccessKeySecret 点击

  • springboot集成@DS注解实现数据源切换的方法示例

    目录 启用@DS实现数据源切换 POM内添加核心jar包 yml配置 “核心”-使用@DS注解 最后 启用@DS实现数据源切换 POM内添加核心jar包         <dependency>             <groupId>com.baomidou</groupId>             <artifactId>dynamic-datasource-spring-boot-starter</artifactId>        

  • .NET 中配置从xml转向json方法示例详解

    目录 一.配置概述 二.配置初识 三.选项模式 四.选项依赖注入 五.其它配置 六.托管模式 一.配置概述 在.net framework平台中我们常见的也是最熟悉的就是.config文件作为配置,控制台桌面程序是App.config,Web就是web.config,里面的配置格式为xml格式. 在xml里面有系统生成的配置项,也有我们自己添加的一些配置,最常用的就是appSettings节点,用来配置数据库连接和参数. 使用的话就引用包System.Configuration.Configur

随机推荐