Springboot外部化配置详解与源码分析

@[TOC]

一、外部化配置详解

1、配置优先级

Springboot配置属性加载顺序从上到下依次加载,后加载的同名配置会覆盖之前的。

建议在整个应用程序中坚持使用一种格式。如果有两者的配置文件.properties和YAML格式放在同一个位置,.properties优先考虑。

2、命令行参数

默认情况下,SpringApplication转换任何命令行选项参数(即以--,比如--server.port=9000)property并将它们添加到Spring的Environment中。

命令行属性始终优先于基于文件的属性。

如果不想将命令行属性添加到Environment,可以使用以下命令禁用它们SpringApplication.setAddCommandLineProperties(false)

3、JSON应用程序属性

对应第十条,当应用程序启动时,任何spring.application.json或者SPRING_APPLICATION_JSON属性将被分析并添加到Environment

例如,在SPRING_APPLICATION_JSON属性可以作为环境变量在UN*X shell中的命令行上提供:

shell 复制代码
# 最终会得到my.name=test在Spring的Environment.
$ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar
$ java -Dspring.application.json='{"my":{"name":"test"}}' -jar myapp.jar
$ java -jar myapp.jar --spring.application.json='{"my":{"name":"test"}}'

4、配置文件

Spring Boot会自动找到并加载application.propertiesapplication.yaml应用程序启动时来自以下位置的文件: 1、从类路径的根目录;类路径下/config目录。 2、从当前目录、当前目录下config/目录;当前目录下config/目录下的直接子目录

可以通过指定spring.config.name属性,来改变配置文件的名称:

bash 复制代码
# 查找myproject.yaml,而不是查找application.yaml
$ java -jar myproject.jar --spring.config.name=myproject

还可以通过使用spring.config.location属性来指定配置文件位置:

bash 复制代码
$ java -jar myproject.jar --spring.config.location=\
    optional:classpath:/default.properties,\
    optional:classpath:/override.properties

spring.config.name, spring.config.location,以及spring.config.additional-location很早就被用来确定哪些文件必须被加载。它们必须定义为环境属性(通常是OS环境变量、系统属性或命令行参数)。

(1)可选位置(optional)

默认情况下,当指定的配置数据位置不存在时,Spring Boot将引发ConfigDataLocationNotFoundException,应用程序将不会启动。

如果希望指定一个位置,但允许它不存在,可以使用optional:前缀。可以将此前缀与spring.config.locationspring.config.additional-location属性以及spring.config.import使用。

例如,一个spring.config.import的值optional:file:./myconfig.properties即允许应用程序的myconfig.properties文件缺失。

如果你想忽略所有ConfigDataLocationNotFoundExceptions并始终继续启动应用程序,可以使用SpringApplication.setDefaultProperties(...​)或者使用系统/环境变量spring.config.on-not-found配置,值设置为ignore。。

(2)通配符

使用通配符位置spring.config.locationspring.config.additional-location属性: config/*/会加载/config/redis/application.properties/config/mysql/application.properties

(3)导入附加数据

bash 复制代码
spring.application.name=myapp
# 会导入dev.properties作为配置文件
spring.config.import=optional:file:./dev.properties

(4)导入无扩展名的文件

需要指定文件类型: spring.config.import=file:/etc/config/myconfig[.yaml]

(5)使用配置树

一般是用在K8s中。

bash 复制代码
etc/
  config/
    dbconfig/
      db/
        username
        password
    mqconfig/
      mq/
        username
        password

# 这将增加db.username, db.password, mq.username和mq.password属性。
spring.config.import=optional:configtree:/etc/config/*/

(6)属性占位符

在配置文件中,可以使用属性占位符来获取已经加载的属性:

bash 复制代码
app.name=MyApp
# ${app.name}会被替换为MyApp,${username:Unknown}由于没有username配置,会使用默认值Unknown
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}

${demo.item-price}会取demo.item-pricedemo.itemPrice的配置,但是相反${demo.itemPrice}不会取demo.item-price的配置。

(7)使用多文档文件

多文档属性文件通常与激活属性结合使用,例如spring.config.activate.on-profile。 yaml文件支持多文档:

yaml 复制代码
spring:
  application:
    name: "MyApp"
---
spring:
  application:
    name: "MyCloudApp"
  config:
    activate:
      on-cloud-platform: "kubernetes"

(8)属性激活

以下内容指定第二个文档仅在Kubernetes上运行时活动,并且仅在"prod"或"staging"配置文件活动时活动:

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

5、配置随机值

RandomValuePropertySource对于注入随机值非常有用(例如,注入到秘密或测试用例中)。它可以生成整数、长整型、uuids或字符串,如下例所示:

bash 复制代码
# random.int*语法是OPEN value (,max) CLOSE在哪里OPEN,CLOSE是任何字符和value,max都是整数。如果max那么value是最小值max是最大值(不含)。
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number-less-than-ten=${random.int(10)}
my.number-in-range=${random.int[1024,65536]}

二、JavaBean属性与配置绑定

1、@ConfigurationProperties

(1)属性绑定

java 复制代码
/*
my.service.enabled,其值为false默认情况下。
my.service.remote-address,其类型可以是String.
my.service.security.username,带有嵌套的"安全"对象,该对象的名称由属性的名称决定。特别是,那里根本没有使用该类型,它可能已经被使用了SecurityProperties.
my.service.security.password.
my.service.security.roles,带有一组String默认为USER.

静态属性不会绑定
*/

@ConfigurationProperties("my.service")
public class MyProperties {

    private boolean enabled;

    private InetAddress remoteAddress;

    private final Security security = new Security();

    // getters / setters...

    public static class Security {

        private String username;

        private String password;

        private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

        // getters / setters...
    }
}

(2)构造器绑定

java 复制代码
/*
jdk16以上,如果有多个构造器,需要使用@ConstructorBinding指定构造器
默认值可以使用@DefaultValue在构造函数参数上


若要使用构造函数绑定,必须使用@EnableConfigurationProperties或配置属性扫描。您不能将构造函数绑定用于由常规Spring机制创建的beans(例如@Componentbean,使用@Bean使用加载的方法或beans@Import)
*/
@ConstructorBinding
@ConfigurationProperties("my.service")
public class MyProperties {

    // fields...

    public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) {
        this.enabled = enabled;
        this.remoteAddress = remoteAddress;
        this.security = security;
    }

    // getters...

    public static class Security {

        // fields...

        public Security(String username, String password, @DefaultValue("USER") List<String> roles) {
            this.username = username;
            this.password = password;
            this.roles = roles;
        }

        // getters...

    }

}

(3)可以自动注入

java 复制代码
// 可以像普通的Bean一样自动注入
@Service
public class MyService {

    private final MyProperties properties;

    public MyService(MyProperties properties) {
        this.properties = properties;
    }

    public void openConnection() {
        Server server = new Server(this.properties.getRemoteAddress());
        server.start();
        // ...
    }

    // ...

}

(4)第三方配置

java 复制代码
@Configuration(proxyBeanMethods = false)
public class ThirdPartyConfiguration {

// 属性定义的任何JavaBean属性another前缀被映射到AnotherComponentbean
    @Bean
    @ConfigurationProperties(prefix = "another")
    public AnotherComponent anotherComponent() {
        return new AnotherComponent();
    }

}

(5)宽松配置

例如,context-path绑定到contextPath和大写的环境属性(例如,PORT绑定到port)。

java 复制代码
/**
my.main-project.person.first-name,建议用于.properties和YAML档案。
my.main-project.person.firstName标准的camel case语法。
my.main-project.person.first_name 下划线表示法,这是在中使用的一种替代格式.properties和YAML档案。
MY_MAINPROJECT_PERSON_FIRSTNAME 使用系统环境变量时建议使用大写格式。

建议使用:my.person.first-name=Rod
*/
@ConfigurationProperties(prefix = "my.main-project.person")
public class MyPersonProperties {

    private String firstName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

}

2、@EnableConfigurationProperties

java 复制代码
// 将SomeProperties自动配置
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties.class)
public class MyConfiguration {

}
@ConfigurationProperties("some.properties")
public class SomeProperties {

}
java 复制代码
// 扫描要配置的包
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {

}

我们建议@ConfigurationProperties只处理环境,特别是不从上下文中注入其他beans。对于拐角情况,可以使用setter注入或任何*Aware框架提供的接口(例如EnvironmentAware如果您需要访问Environment).如果您仍然希望使用构造函数注入其他bean,则配置属性bean必须用@Component并使用基于JavaBean的属性绑定。

3、属性验证

可以用JSR-303 javax.validation配置类上的约束注释

java 复制代码
@ConfigurationProperties("my.service")
@Validated
public class MyProperties {

    @NotNull
    private InetAddress remoteAddress;

    // getters/setters...

}


@ConfigurationProperties("my.service")
@Validated
public class MyProperties {

    @NotNull
    private InetAddress remoteAddress;

    @Valid
    private final Security security = new Security();

    // getters/setters...

    public static class Security {

        @NotEmpty
        private String username;

        // getters/setters...

    }

}

4、@Value

同样的${demo.item-price}会取demo.item-pricedemo.itemPrice的配置,但是相反${demo.itemPrice}不会取demo.item-price的配置。

java 复制代码
@Component
public class MyBean {

    @Value("${name}")
    private String name;

    // ...

}

三、源码分析

1、PropertySource

Spring所有外部配置资源,都存在org.springframework.core.env.PropertySource中,它是一个抽象类,有许多实现。

2、默认配置&命令行参数的配置

java 复制代码
// org.springframework.boot.SpringApplication#configurePropertySources
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
	MutablePropertySources sources = environment.getPropertySources(); // 拿出PropertySources
	if (!CollectionUtils.isEmpty(this.defaultProperties)) { // 默认的配置
		DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
	}
	if (this.addCommandLineProperties && args.length > 0) { // 命令行的配置
		String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; // commandLineArgs
		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); // 替换到sources中
		}
		else {
			// 添加
			sources.addFirst(new SimpleCommandLinePropertySource(args));
		}
	}
}

3、jndi&Servlet配置

java 复制代码
// org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor#addJsonPropertySource
private void addJsonPropertySource(ConfigurableEnvironment environment, PropertySource<?> source) {
	MutablePropertySources sources = environment.getPropertySources();
	String name = findPropertySource(sources);
	if (sources.contains(name)) {
		sources.addBefore(name, source);
	}
	else {
		sources.addFirst(source);
	}
}
// org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor#findPropertySource
private String findPropertySource(MutablePropertySources sources) {
	if (ClassUtils.isPresent(SERVLET_ENVIRONMENT_CLASS, null)) {
		PropertySource<?> servletPropertySource = sources.stream()
				.filter((source) -> SERVLET_ENVIRONMENT_PROPERTY_SOURCES.contains(source.getName())).findFirst()
				.orElse(null);
		if (servletPropertySource != null) {
			return servletPropertySource.getName();
		}
	}
	return StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME;
}

4、从Environment获取配置

java 复制代码
// org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String, java.lang.Class<T>, boolean)
@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
	if (this.propertySources != null) {
		// 遍历PropertySource
		for (PropertySource<?> propertySource : this.propertySources) {
			if (logger.isTraceEnabled()) {
				logger.trace("Searching for key '" + key + "' in PropertySource '" +
						propertySource.getName() + "'");
			}
			Object value = propertySource.getProperty(key);
			if (value != null) { // 如果获取到了就直接返回,所以配置加载是是有先后顺序的
				if (resolveNestedPlaceholders && value instanceof String) {
					value = resolveNestedPlaceholders((String) value);
				}
				logKeyFound(key, propertySource, value);
				return convertValueIfNecessary(value, targetValueType);
			}
		}
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Could not find key '" + key + "' in any property source");
	}
	return null;
}

参考资料

https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/features.html#features.external-config

相关推荐
程序员-珍25 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin3344556642 分钟前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端
bug菌2 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
夜月行者3 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
Yvemil73 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
sdg_advance3 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
猿java4 小时前
使用 Kafka面临的挑战
java·后端·kafka
碳苯4 小时前
【rCore OS 开源操作系统】Rust 枚举与模式匹配
开发语言·人工智能·后端·rust·操作系统·os
kylinxjd4 小时前
spring boot发送邮件
java·spring boot·后端·发送email邮件
2401_857439698 小时前
Spring Boot新闻推荐系统:用户体验优化
spring boot·后端·ux