@[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.properties
和application.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.location
和spring.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.location
和spring.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-price
和demo.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-price
和demo.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