详解SpringBoot外部化配置

4,892 阅读12分钟

我正在参加「掘金·启航计划」

2023/11/09 更新

SpringBoot外部化配置(基于2.4.0以后)

Spring Boot可以让你将配置外部化,这样你就可以在不同的环境中使用相同的应用程序代码。 你可以使用各种外部配置源,包括Java properties 文件、YAML文件、环境变量和命令行参数。

属性值可以通过使用 @Value 注解直接注入你的Bean,也可以通过Spring 的 Environment 访问,或者通过 @ConfigurationProperties 绑定到对象。同时 Spring Boot 也提供了一种非常特殊的 PropertyOrder,来允许用户可以在适当的场景下覆盖某些属性值,该顺序旨在允许合理地覆盖值。

按以下顺序优先级从低到高, 后者的属性值覆盖前者 ,所有的配置会形成互补配置

  1. 默认属性(使用 SpringApplication.setDefaultProperties 指定)

  2. @Configuration类上的@PropertySource注解引入的配置属性

    • 请注意,这样的属性源直到ApplicationContext被刷新时才会被添加到环境中。这对于配置某些属性来说已经太晚了,比如logging.* spring.main.* ,它们在刷新开始前就已经被读取了。
  3. 配置数据(例如application.properties文件)

  4. 对于random.*形式的属性,优先从RandomValuePropertySource获取(指优先于后者)

  5. OS environment variables(操作系统环境变量)

  6. Java System properties(Java 系统属性System.getProperties()

  7. JNDI 属性

  8. ServletContext 的 初始化参数

  9. ServletConfig 的 初始化参数

  10. SPRING_APPLICATION_JSON 属性

  11. 命令行参数

  12. test 模块下的 properties 属性

  13. test 模块下 @TestPropertySource 注解引入的配置文件

  14. 启用 devtools 时 $HOME/.config/spring-boot 路径下的配置

配置数据文件按以下加载顺序考虑:

  • 打包在 jar 中的应用程序属性(application.properties 和 YAML)
  • 打包在 jar 中的特定配置文件的应用程序属性(application-{profile}.properties 和 YAML)
  • 打包 jar 之外的应用程序属性(application.properties 和 YAML)
  • 打包 jar 之外的特定配置文件的应用程序属性(application-{profile}.properties 和 YAML)

SpringBoot 配置文件

springdoc.cn/spring-boot…

Spring 中常见的配置文件类型

  • XML 资源
  • Properties 资源
  • YAML 资源

配置文件加载顺序

Spring Boot 启动时,会自动加载 JAR 包内部及 JAR 包所在目录指定位置的配置文件(Properties 文件、YAML 文件)。列表按优先级排序(较低项目的值覆盖较早项目的值)

  1. classpath( –classpath
    1. classpath 根路径
    2. classpath 下的 /config 包
  1. 当前目录( –file
    1. 当前目录下
    2. 当前目录下的 config/ 子目录
    3. 当前目录下的 config/ 子目录的直接子目录
. project-sample
├── config
│   ├── application.yml (4)
│   └── src/main/resources
|   │   ├── application.yml (1)
|   │   └── config
|   |   │   ├── application.yml (2)
├── application.yml (3

启动时加载配置文件顺序:1 > 2 > 3 > 4

配置覆盖变更(2.4.0以后)

2.4.0以前版本,默认情况的加载顺序如下:

  1. 打包在 jar 中的应用程序属性(application.properties 和 YAML)。
  2. 打包 jar 之外的应用程序属性(application.properties 和 YAML)
  3. 打包在 jar 中的特定于配置文件的应用程序属性(application-{profile}.properties 和 YAML)
  4. 打包 jar 之外的特定于配置文件的应用程序属性(application-{profile}.properties 和 YAML)

注意: 在之前的版本中,JAR 包外部的application.properties配置文件不会覆盖 JAR 包里面的基于 "profile" 的application-{profile} .properties 配置文件。


2.4.0以后版本,默认情况的搜索顺序如下: 保证了 JAR 包外部的应用程序参数应优先于 JAR 包内部的特定激活的配置参数

  1. 打包在 jar 中的应用程序属性(application.properties 和 YAML)。
  2. 打包在 jar 中的特定于配置文件的应用程序属性(application-{profile}.properties 和 YAML)
  3. 打包 jar 之外的应用程序属性(application.properties 和 YAML)
  4. 打包 jar 之外的特定于配置文件的应用程序属性(application-{profile}.properties 和 YAML)

注意:同一位置下, Properties 文件优先级高于 YAML 文件 如果Spring Boot在优先级更高的位置找到了配置,那么它就会无视优先级低的配置。

可选的位置(Optional Locations)

默认情况下,当指定的配置数据位置不存在时,Spring Boot将抛出一个 ConfigDataLocationNotFoundException ,你的应用程序将无法启动。

如果你想指定一个位置,但你不介意它并不总是存在,你可以使用 optional: 前缀。你可以在 spring.config.location和spring.config.extra-location 属性中使用这个前缀,也可以在 spring.config.import 声明中使用。

例如,spring.config.import 值为 optional:file:./myconfig.properties 允许你的应用程序启动,即使 myconfig.properties 文件丢失。

通配符地址

如果一个配置文件的位置在最后一个路径段中包含 * 字符,它就被认为是一个通配符位置。

你可以在 spring.config.location 和 spring.config.extra-location 属性中使用通配符位置。

通配符位置必须只包含一个 * 并以 */ 结尾,用于搜索属于目录的位置,或 */ 用于搜索属于文件的位置。 带有通配符的位置将根据文件名的绝对路径按字母顺序排序。

通配符位置只对外部目录起作用。 你不能在 classpath: 位置中使用通配符。

导入额外的配置文件(2.4.0以后)

可以使用spring.config.import属性从其他地方导入更多的配置数据 。它会将导入文件作为临时文件放在当前配置文件之后处理,因此其属性具有更高的优先级

spring.application.name=myapp
spring.config.import=optional:file:./dev.properties

这将触发导入当前目录下的 dev.properties 文件(如果存在这样的文件)。 导入的 dev.properties 中的值将优先于触发导入的文件。 在上面的例子中,dev.properties 可以将 spring.application.name 重新定义为一个不同的值。

可以使用以下 3 种方式来指定文件路径:

  1. 文件系统路径:可以使用绝对或相对于当前工作目录的文件系统路径来指定配置文件的位置。例如:
spring.config.import=file:/opt/config/application.yml

以上示例将 /opt/config/application.yml 文件作为额外的配置文件导入到应用程序中。

  1. 类路径(classpath)路径:可以使用 classpath: 前缀以及相对于类路径的文件路径来指定配置文件的位置。例如:
spring.config.import=classpath:/config/application.yml

以上示例将 classpath:/config/application.yml 文件作为额外的配置文件导入到应用程序中。

  1. URL:还可以使用 URL 来指定远程服务器上的配置文件。例如:
spring.config.import=http://config-server:8888/appname/profile.yml

以上示例将http://config-server:8888/appname/profile.yml文件作为额外的配置文件导入到应用程序中。

需要注意的是,spring.config.import属性支持多个值,可以通过逗号分隔来同时导入多个配置文件。

激活外部配置文件

在运行Jar包的命令中加入这个参数就可以指定Jar包以外的配置文件的位置了,也可以在application的配置文件中配置该属性

$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

这个参数就是指定外部application.yml配置文件位置的参数,它支持classpathfile路径

java -jar myproject.jar --spring.config.name=myproject

如果您不喜欢application.properties作为配置文件名,您可以通过指定spring.config.name环境属性来切换到另一个文件名

Profile 概述

Profile 本质上代表一种用于组织配置信息的维度,在不同场景下可以代表不同的含义。例如,如果 Profile 代表的是一种状态,我们可以使用 open、halfopen、close 等值来分别代表全开、半开和关闭等。再比如系统需要设置一系列的模板,每个模板中保存着一系列配置项。

配置命名规则:

/{application}.yml
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

文档排序(2.4.0以后)

从 Spring Boot 2.4 开始,加载 Properties 和 YAML 文件时候会遵循, 在文档中声明排序靠前的属性将被靠后的属性覆盖

激活指定配置或文件

Spring Profiles提供了一种方法来隔离你的应用程序配置的一部分,并使其仅在某些环境中可用。 任何 @Component、@Configuration 或 @ConfigurationProperties 都可以用 @Profile 标记,以限制它的加载时机,如下面的例子所示。

@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {

    // ...

}

命令行激活: --spring.profiles.active=prod 

spring:
  profiles:
    active: dev #激活开发环境配置

配置文件激活如上,只需要在application.yml或者properties文件中配置即可

注意:在application.yml或者properties文件存在的情况下,不管激活的是prod还是dev,还是会读取默认的配置文件,只不过指定的配置文件会覆盖默认配置文件中的属性

激活属性(Activation Properties)

有时,只在满足某些条件时激活一组特定的属性是很有用的。 例如,你可能有一些属性只有在特定的配置文件被激活时才相关。

你可以使用 spring.config.activation.* 有条件地激活一个属性文件。

属性说明
on-profile一个必须与之匹配的配置文件表达式,以使文件处于活动状态(激活指定的配置文件时有效)。
on-cloud-platform必须检测到的 CloudPlatform,以使文件处于活动状态。(云平台状态下有效)

例如,下面指定第二个文件只有在Kubernetes上运行时才有效,并且只有在 “prod” 或 “staging” 配置文件处于活动状态时才有效。

myprop=always-set
#---
spring.config.activate.on-cloud-platform=kubernetes
spring.config.activate.on-profile=prod | staging
myotherprop=sometimes-set

配置文件组(Profile Groups)

偶尔,你在你的应用程序中定义和使用的配置文件过于精细,使用起来就会很麻烦。 例如,你可能有 proddb 和 prodmq 配置文件,用来独立启用数据库和消息传递功能。

为了帮助解决这个问题,Spring Boot允许你定义配置文件组。 配置文件组允许你为相关的配置文件组定义一个逻辑名称。

例如,我们可以创建一个 production 组,由 proddb 和 prodmq 配置文件组成。

spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq

现在可以使用 --spring.profiles.active=production 来启动我们的应用程序,一次性激活 production、proddb 和 prodmq 配置文件。

以编程方式设置配置文件(Profile)

你可以在应用运行前通过调用 SpringApplication.setAdditionalProfiles(…) 以编程方式设置激活的配置文件。 也可以通过使用Spring的 ConfigurableEnvironment 接口来激活配置文件。

嵌入系统配置信息

例如,如果想要获取当前应用程序的名称并作为一个配置项进行管理,那么很简单,我们直接通过 ${spring.application.name} 占位符:

myapplication.name : ${spring.application.name}

假设我们使用 Maven 来构建应用程序,那么可以按如下所示的配置项来动态获取与系统构建过程相关的信息:

info: 
  app:
    encoding: @project.build.sourceEncoding@
    java:
      source: @java.version@
      target: @java.version@

# 等同于下述效果
info:
  app:
    encoding: UTF-8
    java:
        source: 1.8.0_31
        target: 1.8.0_31

配置参数提示

additional-spring-configuration-metadata.jsonspring-configuration-metadata.json在springboot-starter官方项目或第三方starter项目中随处可见,那它起的作用是什么?

  • 配置additional-spring-configuration-metadata.json文件后,在开发人员的IDE工具使用个人编写的配置读取很有效的在application.propertiesapplication.yml文件下完成提示

配置处理器

在Maven中,该依赖关系应被声明为可选的,如以下例子所示。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

创建additional-spring-configuration-metadata.json

resources/META-INF目录下创建additional-spring-configuration-metadata.json,分类为 “groups” 或 “properties”,附加值提示分类为 "hints",如以下例子所示:

{
    "groups": [
        {
            "name": "server",
            "type": "org.springframework.boot.autoconfigure.web.ServerProperties",
            "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
        },
        {
            "name": "spring.jpa.hibernate",
            "type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate",
            "sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties",
            "sourceMethod": "getHibernate()"
        }
    ...
    ],
    "properties": [
        {
            "name": "server.port",
            "type": "java.lang.Integer",
            "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
        },
        {
            "name": "server.address",
            "type": "java.net.InetAddress",
            "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
        },
        {
            "name": "spring.jpa.hibernate.ddl-auto",
            "type": "java.lang.String",
            "description": "DDL mode. This is actually a shortcut for the "hibernate.hbm2ddl.auto" property.",
            "sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate"
        }
    ...
    ],
    "hints": [
        {
            "name": "spring.jpa.hibernate.ddl-auto",
            "values": [
                {
                    "value": "none",
                    "description": "Disable DDL handling."
                },
                {
                    "value": "validate",
                    "description": "Validate the schema, make no changes to the database."
                },
                {
                    "value": "update",
                    "description": "Update the schema if necessary."
                },
                {
                    "value": "create",
                    "description": "Create the schema and destroy previous data."
                },
                {
                    "value": "create-drop",
                    "description": "Create and then destroy the schema at the end of the session."
                }
            ]
        }
    ]
}

Property 属性

properties 数组中包含的JSON对象可以包含下表中描述的属性。

Name类型目的
nameString属性的全名。 名称采用小写的句号分隔形式(例如,server.address)。 这个属性是强制性的。
typeString该属性的数据类型的完整签名(例如,java.lang.String),但也有完整的通用类型(例如 java.util.Map<java.lang.String,com.example.MyEnum>)。 您可以使用此属性来指导用户可以输入的值的类型。 为了保持一致性,基元的类型是通过使用其包装类型来指定的(例如,boolean 变成 java.lang.Boolean)。 如果该类型不知道,可以省略。
descriptionString可以显示给用户的该property的简短描述。 如果没有描述,可以省略。 描述中的最后一行应以句号(.)结束。
sourceTypeString贡献此属性的来源的类名。 例如,如果该属性来自于一个用 @ConfigurationProperties 注解的类,该属性将包含该类的完全限定名称。 如果源类型未知,可以省略。
defaultValueObject默认值,如果没有指定该属性,则使用该值。 如果该属性的类型是一个数组,它可以是一个数组的值。 如果默认值是未知的,它可以被省略。
deprecationDeprecation指定该属性是否被废弃。 如果该字段没有被废弃,或者不知道该信息,可以省略。 下表提供了关于 deprecation 属性的更多细节。

Hint 属性

包含在 hints 数组中的JSON对象可以包含下表中的属性。

Name类型目的
nameString此提示所指向的属性的全名。 名称采用小写的句号分隔形式(如 spring.mvc.servlet.path)。 这个属性是强制性的。
valuesValueHint[]由 ValueHint 对象定义的有效值的列表(在下表中描述)。 每个条目都定义了值,并且可以有一个description。

每个 hint 元素的 values 属性中包含的JSON对象可以包含下表中描述的属性。

Name类型目的
valueObject提示所指的元素的一个有效值。 如果该属性的类型是一个数组,它也可以是一个数组的值。 这个属性是强制性的。
descriptionString可以显示给用户的价值的简短描述。 如果没有描述,可以省略。 描述中的最后一行应以句号(.)结束。

SpringBoot命令行参数和系统属性参数

参考:juejin.cn/post/684490…

命令行参数

Java传递命令行参数,有三种参数形式:

  • 选项参数,基本格式为--optName[=optValue]--为连续两个减号)
--foo
--foo=bar
--foo="bar then baz"
--foo=bar,baz,biz
  • 非选项参数:java -jar xxx.jar abc def
  • 系统参数:java -jar -Dserver.port=8081 xxx.jar

其中还能分成两种类型的命令行参数:VM optionsProgram arguments。它们之间的区别如下:

VM options

VM options是传递给Java虚拟机(JVM)的参数。这些参数通常用于控制JVM的行为,例如设置内存大小、GC策略、调试选项等。

可以通过以下方式指定VM options

  • 在命令行中使用-D选项指定系统属性;
  • 通过-XX选项指定JVM参数。

例如,将最大堆大小设置为512 MB:

java -Xmx512m -jar myapp.jar

在上面的示例中,-Xmx选项用于设置最大堆大小,512m指定了堆的最大值为512MB。

Program arguments

Program arguments是传递给应用程序的参数。这些参数通常用于配置应用程序的行为或传递输入数据。

可以将Program arguments作为命令行参数传递给应用程序。例如,以下命令传递一个名为“arg1”的参数和一个名为“arg2”的参数:

java -jar myapp.jar arg1 arg2

然后,在应用程序中,您可以使用以下代码访问这些参数:

public static void main(String[] args) {
    String arg1 = args[0];
    String arg2 = args[1];
    // ...
}

总之,VM optionsProgram arguments是两种不同的命令行参数类型,可以用于控制JVM的行为或传递输入数据。通常,VM options用于配置JVM,而Program arguments用于配置应用程序。

解析原理

SpringBoot 基于 Java 命令行参数中的非选项参数自定义了选项参数的规则,具体可以看解析器SimpleCommandLineArgsParser,它里面调用其parse方法对参数进行解析

class SimpleCommandLineArgsParser {

    public CommandLineArgs parse(String... args) {
        CommandLineArgs commandLineArgs = new CommandLineArgs();
        for (String arg : args) {
            // --开头的选参数解析
            if (arg.startsWith("--")) {
                // 获得key=value或key值
                String optionText = arg.substring(2, arg.length());
                String optionName;
                String optionValue = null;
                // 如果是key=value格式则进行解析
                if (optionText.contains("=")) {
                    optionName = optionText.substring(0, optionText.indexOf('='));
                    optionValue = optionText.substring(optionText.indexOf('=')+1, optionText.length());
                } else {
                    // 如果是仅有key(--foo)则获取其值
                    optionName = optionText;
                }
                // 如果optionName为空或者optionValue不为空但optionName为空则抛出异常
                if (optionName.isEmpty() || (optionValue != null && optionValue.isEmpty())) {
                    throw new IllegalArgumentException("Invalid argument syntax: " + arg);
                }
                // 封装入CommandLineArgs
                commandLineArgs.addOptionArg(optionName, optionValue);
            } else {
                commandLineArgs.addNonOptionArg(arg);
            }
        }
        return commandLineArgs;
    }
}

参数值的获取

如果您需要访问传递给应用程序的参数SpringApplication.run(…),您可以注入一个ApplicationArguments。该ApplicationArguments接口提供对原始String[]参数以及选项参数和非选项参数的访问,如以下示例所示:

@Component
public class MyBean {
    
    @Autowired
    public MyBean(ApplicationArguments args) {
        boolean debug = args.containsOption("debug");
        List<String> files = args.getNonOptionArgs();
        // if run with "--debug logfile.txt" debug=true, files=["logfile.txt"]
    }
    
}
  • 另外,选项参数,也可以直接通过@Value在类中获取
  • 系统参数可以通过java.lang.System提供的方法获取

参数值的区别

关于参数值区别,重点看选项参数和系统参数。通过上面的示例我们已经发现使用选项参数时,参数在命令中是位于xxx.jar之后传递的,而系统参数是紧随java -jar之后。

如果不按照该顺序进行执行,比如使用如下方式使用选项参数:

java -jar --server.port=8081 xxx.jar

则会抛出如下异常:

Unrecognized option: --server.port=8081
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

如果将系统参数放在jar包后面,问题会更严重,会出现可以正常启动,但参数无法生效。这个错误是最坑的,所以一定谨记:通过-D传递系统参数时,务必放置在待执行的jar包之前。

正确示例如下:

# 使用命令行传递参数
java -jar myapp.jar --myParam=value
# 使用系统属性传递参数
java -DmyParam=value -jar myapp.jar

扩展“外部化配置”属性源

SpingBoot怎么支持YAML配置文件解析?

处理@PropertySource注解从ConfigurationClassParser#processPropertySource方法进

Spring中@PropertySource默认不支持YAML格式的解析,但是SpringBoot的配置文件却可以解析YAML,这说明SpringBoot中已经实现了YAML文件的解析,我们只需要复用即可,我们可以看该注解源码

/**
 * Specify a custom {@link PropertySourceFactory}, if any.
 * <p>By default, a default factory for standard resource files will be used.
 * @since 4.3
 * @see org.springframework.core.io.support.DefaultPropertySourceFactory
 * @see org.springframework.core.io.support.ResourcePropertySource
 */
Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

PropertySourceFactory的默认实现是DefaultPropertySourceFactory

public class DefaultPropertySourceFactory implements PropertySourceFactory {

	@Override
	public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
		return (name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource));
	}

}

ResourcePropertySource默认不支持YAML,所以我们可以通过实现PropertySourceFactory接口,然后用@PropertySource的factory属性来实现YAML的解析

public class YamlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
        yamlPropertiesFactoryBean.setResources(resource.getResource());
        Properties yamlProperties = yamlPropertiesFactoryBean.getObject();
        return new PropertiesPropertySource(name, yamlProperties);
    }
}

关于 ApplicationEnvironmentPreparedEvent 没有被执行的原因

官方文档 中有说到:有些事件实际上是在ApplicationContext被创建之前触发的,所以我们不能将这些事件的监听器注册为@Bean

因为这个时候应用上下文还没有被创建,也就是说监听器也还没有被初始化,这个先后顺序不对,会导致这些事件的监听器不会被触发

但可以使用SpringApplication.addListeners(...) 方法或SpringApplicationBuilder.listeners(...) 方法注册它们。

如果您希望这些侦听器自动注册的话,可以通过新建一个META-INF/spring.factories文件,添加类似以下内容,SpringBoot会自动帮你注册。

org.springframework.context.ApplicationListener=com.example.project.MyListener