本文如有任何纰漏、错误,请不吝指正!

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下的configapplication.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,那么结果会是devtest都会存在,而不是替换的行为 。

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

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

profiles文件的命名规则为application-*.properties,同样的,application.properties能放置的位置它也可以,不能的,它也不可以。

propery source

注解@PropertySource可以写在配置类上,并且指定要读取的配置文件路径,这个路径可以是绝对路径,也可以是相对路径。

它可以有以下几种配置

  1. @PropertySource("/config.properties")
  2. @PropertySource("config.properties")
  3. @PropertySource("file:/usr/local/config.properties")
  4. @PropertySource("file:./config.properties")
  5. @PropertySource("${pathPrefix}/config.properties")

其中1和2两种方式是一样的,都是从classpath去开始查找的

3和4是使用文件系统的绝对和相对路径的方式,这里绝对路径比较好理解 ,相对路径则是从项目的根目录作为相对目录的,如果是jar包的方式运行,就是jar所在的目录。

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

注意@PropertySource指定的文件一定是要存在的,默认情况下文件类型为Properties,可以通过自定义PropertySourceFactory来读取其他的格式

FBI Warning : 第一种和第二种配置时,直接跑不会有问题,但是跑单元测试时出了问题,建议加上classpath:,原因在这里解释

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

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

原理浅入浅出

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

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

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

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

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

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

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

如果一个配置类使用了@ConfigurationProperties,然后字段使用了@Value@ConfigurationProperties先被处理,@Value后被处理。

源码简读

SpringBoot读取application.properties配置

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

  1. public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
  2. // Note the order is from least to most specific (last one wins)
  3. // 默认检索配置文件的路径,优先级越来越高,
  4. // 可以通过 spring.config.location重新指定,要早于当前类执行时配置好
  5. private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
  6. // 默认的配置名,可以通过命令行配置--spring.config.name=xxx来重新指定
  7. // 不通过命令行也可以通过其他方式,环境变量这些。
  8. private static final String DEFAULT_NAMES = "application";
  9. private class Loader {
  10. // 找到配置的路径
  11. private Set<String> getSearchLocations() {
  12. if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
  13. return getSearchLocations(CONFIG_LOCATION_PROPERTY);
  14. }
  15. Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
  16. locations.addAll(
  17. asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
  18. return locations;
  19. }
  20. // 解析成Set
  21. private Set<String> asResolvedSet(String value, String fallback) {
  22. List<String> list = Arrays.asList(StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(
  23. (value != null) ? this.environment.resolvePlaceholders(value) : fallback)));
  24. // 这里会做一个反转,也就是配置的路径中,放在后面的优先级越高
  25. Collections.reverse(list);
  26. return new LinkedHashSet<>(list);
  27. }
  28. private Set<String> getSearchNames() {
  29. if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
  30. String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
  31. return asResolvedSet(property, null);
  32. }
  33. return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
  34. }
  35. }
  36. }

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

  1. protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
  2. MutablePropertySources sources = environment.getPropertySources();
  3. if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
  4. sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
  5. }
  6. // 支持从命令行添加属性以及存在参数时
  7. if (this.addCommandLineProperties && args.length > 0) {
  8. String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
  9. // 这里是看下是不是存在同名的配置了
  10. if (sources.contains(name)) {
  11. PropertySource<?> source = sources.get(name);
  12. CompositePropertySource composite = new CompositePropertySource(name);
  13. composite.addPropertySource(
  14. new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
  15. composite.addPropertySource(source);
  16. sources.replace(name, composite);
  17. }
  18. else {
  19. // 直接添加,并且是添加到第一个位置,具有最高优先级
  20. sources.addFirst(new SimpleCommandLinePropertySource(args));
  21. }
  22. }
  23. }

@PropertySource是在refreshContext阶段,执行BeanDefinitionRegistryPostProcessor时处理的

  1. // org.springframework.context.annotation.ConfigurationClassParser#processPropertySource
  2. private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
  3. String name = propertySource.getString("name");
  4. if (!StringUtils.hasLength(name)) {
  5. name = null;
  6. }
  7. String encoding = propertySource.getString("encoding");
  8. if (!StringUtils.hasLength(encoding)) {
  9. encoding = null;
  10. }
  11. // 获取配置的文件路径
  12. String[] locations = propertySource.getStringArray("value");
  13. Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
  14. boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
  15. // 指定的读取配置文件的工厂
  16. Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
  17. // 没有就用默认的
  18. PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
  19. DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
  20. // 循环加载
  21. for (String location : locations) {
  22. try {
  23. // 会解析存在占位符的情况
  24. String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
  25. // 使用DefaultResourceLoader来加载资源
  26. Resource resource = this.resourceLoader.getResource(resolvedLocation);
  27. // 创建PropertySource对象
  28. addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
  29. }
  30. catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
  31. // Placeholders not resolvable or resource not found when trying to open it
  32. if (ignoreResourceNotFound) {
  33. if (logger.isInfoEnabled()) {
  34. logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
  35. }
  36. }
  37. else {
  38. throw ex;
  39. }
  40. }
  41. }
  42. }

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

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

  1. // @ConfigurationProperty
  2. public class ConfigurationPropertiesBindingPostProcessor
  3. implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {
  4. @Override
  5. public int getOrder() {
  6. return Ordered.HIGHEST_PRECEDENCE + 1;
  7. }
  8. }
  9. // @Value
  10. public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
  11. implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
  12. private int order = Ordered.LOWEST_PRECEDENCE - 2;
  13. @Override
  14. public int getOrder() {
  15. return this.order;
  16. }
  17. }

从上面可以看出处理@ConfigurationPropertyBeanPostProcessor优先级很高,而@ValueBeanPostProcessor优先级很低。

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

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

自定义配置读取

org.springframework.boot.context.config.ConfigFileApplicationListener是一个监听器,同时也是一个EnvironmentPostProcessor,在有ApplicationEnvironmentPreparedEvent事件触发时,会去处理所有的EnvironmentPostProcessor的实现类,同时这些个实现也是使用SpringFactoriesLoader的方式来加载的。

对于配置文件的读取,就是使用的这种方式。

  1. @Override
  2. public void onApplicationEvent(ApplicationEvent event) {
  3. if (event instanceof ApplicationEnvironmentPreparedEvent) {
  4. onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
  5. }
  6. if (event instanceof ApplicationPreparedEvent) {
  7. onApplicationPreparedEvent(event);
  8. }
  9. }
  10. private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
  11. List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
  12. postProcessors.add(this);
  13. AnnotationAwareOrderComparator.sort(postProcessors);
  14. for (EnvironmentPostProcessor postProcessor : postProcessors) {
  15. postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
  16. }
  17. }

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

只要实现了EnvironmentPostProcessor接口,并且在META-INF/spring.factories中配置一下

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

附一个自己写的例子

  1. public class ConfigurationFileLoader implements EnvironmentPostProcessor {
  2. private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
  3. private static final String DEFAULT_NAMES = "download";
  4. private static final String DEFAULT_FILE_EXTENSION = ".yml";
  5. @Override
  6. public void postProcessEnvironment (ConfigurableEnvironment environment,
  7. SpringApplication application) {
  8. List<String> list = Arrays.asList(StringUtils.trimArrayElements(
  9. StringUtils.commaDelimitedListToStringArray(DEFAULT_SEARCH_LOCATIONS)));
  10. Collections.reverse(list);
  11. Set<String> reversedLocationSet = new LinkedHashSet(list);
  12. ResourceLoader defaultResourceLoader = new DefaultResourceLoader();
  13. YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
  14. List<Properties> loadedProperties = new ArrayList<>(2);
  15. reversedLocationSet.forEach(location->{
  16. Resource resource = defaultResourceLoader.getResource(location + DEFAULT_NAMES+DEFAULT_FILE_EXTENSION);
  17. if (resource == null || !resource.exists()) {
  18. return;
  19. }
  20. yamlPropertiesFactoryBean.setResources(resource);
  21. Properties properties = yamlPropertiesFactoryBean.getObject();
  22. loadedProperties.add(properties);
  23. });
  24. Properties filteredProperties = new Properties();
  25. Set<Object> addedKeys = new LinkedHashSet<>();
  26. for (Properties propertySource : loadedProperties) {
  27. for (Object key : propertySource.keySet()) {
  28. String stringKey = (String) key;
  29. if (addedKeys.add(key)) {
  30. filteredProperties.setProperty(stringKey, propertySource.getProperty(stringKey));
  31. }
  32. }
  33. }
  34. PropertiesPropertySource propertySources = new PropertiesPropertySource(DEFAULT_NAMES, filteredProperties);
  35. environment.getPropertySources().addLast(propertySources);
  36. }
  37. }

基本上都是 参考ConfigFileApplicationListener写的 ,不过这里实现的功能,其实可以通过 @PropertySource来 解决,只是当时不知道。

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

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

总结

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

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

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

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

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

SpringBoot外部化配置使用Plus版的更多相关文章

  1. SpringBoot 正式环境必不可少的外部化配置

    前言 <[源码解析]凭什么?spring boot 一个 jar 就能开发 web 项目> 中有读者反应: 部署后运维很不方便,比较修改一个 IP 配置,需要重新打包. 这一点我是深有体会 ...

  2. SpringBoot官方文档学习(二)Externalized Configuration(外部化配置)

    Spring Boot允许您将配置外部化,以便可以在不同的环境中使用相同的应用程序代码.您可以使用属性文件.YAML文件.环境变量和命令行参数来具体化配置.属性值可以通过使用@Value注释直接注入b ...

  3. 关于SpringBoot的外部化配置使用记录

    关于SpringBoot的外部化配置使用记录 声明: 若有任何纰漏.错误请不吝指出! 记录下使用SpringBoot配置时遇到的一些麻烦,虽然这种麻烦是因为知识匮乏导致的. 记录下避免一段时间后自己又 ...

  4. SpringBoot的外部化配置最全解析!

    目录 SpringBoot中的配置解析[Externalized Configuration] 本篇要点 一.SpringBoot官方文档对于外部化配置的介绍及作用顺序 二.各种外部化配置举例 1.随 ...

  5. Spring Boot外部化配置实战解析

    一.流程分析 1.1 入口程序 在 SpringApplication#run(String... args) 方法中,外部化配置关键流程分为以下四步 public ConfigurableAppli ...

  6. 玩转Spring Boot 自定义配置、导入XML配置与外部化配置

    玩转Spring Boot 自定义配置.导入XML配置与外部化配置       在这里我会全面介绍在Spring Boot里面如何自定义配置,更改Spring Boot默认的配置,以及介绍各配置的优先 ...

  7. 曹工谈Spring Boot:Spring boot中怎么进行外部化配置,一不留神摔一跤;一路debug,原来是我太年轻了

    spring boot中怎么进行外部化配置,一不留神摔一跤:一路debug,原来是我太年轻了 背景 我们公司这边,目前都是spring boot项目,没有引入spring cloud config,也 ...

  8. Spring配置文件外部化配置及.properties的通用方法

    摘要:本文深入探讨了配置化文件(即.properties)的普遍应用方式.包括了Spring.一般的.远程的三种使用方案. 关键词:.properties, Spring, Disconf, Java ...

  9. Dubbo 新编程模型之外部化配置

    外部化配置(External Configuration) 在Dubbo 注解驱动例子中,无论是服务提供方,还是服务消费方,均需要转配相关配置Bean: @Bean public Applicatio ...

随机推荐

  1. 我想solo自己一个人!

    区域赛之后你就该走了,现在你告诉我,没精力不打了,我真谢谢你! 今年就TM的没有一点舒心的地方! 父母分居, 队友出走, 队伍解散, 白天家里两个外甥很吵, 鼻窦炎复发, 喜欢的妹子也追不到, 整夜失 ...

  2. 虚拟 IP 设为静态 IP

    一:虚拟机设置桥接模式 1.进入虚拟机设置中将网络适配器设置成桥接模式 2.编辑--虚拟网络编辑器--选择桥接 二:将虚拟IP设置成静态IP (1)方案一:进入虚拟机系统 System 设置 (2)方 ...

  3. Spring MVC的Controller接受请求方式以及编写请求处理方法

    Controller接受请求参数的常见方法: 1.通过Bean接受请求参数: 创建POJO实体类 创建pojo包,并在该包中创建实体类UserForm,代码: package pojo; public ...

  4. Python基础00 教程

    Python: 简明 Python 教程 廖雪峰Python3教程 Python快速教程 (手册) 爬虫: 汪海的实验室:Python爬虫入门教程 静觅: Python爬虫学习系列教程 Flask: ...

  5. E. Kamil and Making a Stream 区间gcd

    E. Kamil and Making a Stream 这个题目要用到一个结论,就是区间一个区间长度为n的不同的gcd不会超过logn 个, 其实就是知道这个题目可以暴力就好了. 然后就是对于每一个 ...

  6. 步入LTE、多址技术

    LTE系统的主要性能和目标 与3G相比,LTE主要性能特性: 带宽灵活配置:支持1.4MHz, 3MHz, 5MHz, 10Mhz, 15Mhz, 20MHz 峰值速率(20MHz带宽):下行100M ...

  7. (一)Redis介绍

    1 背景 在早期的互联网Web 1.0时代,大部分企业还是采用传统的企业级单体应用架构,而一时间蜂拥而至的巨大用户流量使得这种架构难以支撑,通过对诸多系统架构实施以及对巨大用户流量的分析过程中发现,其 ...

  8. 王颖奇 20171010129《面向对象程序设计(java)》第十三周学习总结

      实验十三  图形界面事件处理技术 实验时间 2018-11-22 1.实验目的与要求 (1) 掌握事件处理的基本原理,理解其用途: (2) 掌握AWT事件模型的工作机制: (3) 掌握事件处理的基 ...

  9. 深入理解JS中的对象(一)

    目录 一切皆是对象吗? 对象 原型与原型链 构造函数 参考 1.一切皆是对象吗? 首先,"在 JavaScript 中,一切皆是对象"这种表述是不完全正确的. JavaScript ...

  10. 【Scala】代码实现Actor多种需求

    文章目录 简单实现Actor并发编程 使用Actor实现发送没有返回值的异步消息 使用Actor实现不间断消息发送 用react方法替代receive方法接收消息 结合case class,通过匹配不 ...