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

一、外部化配置详解

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中的命令行上提供:

# 最终会得到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属性,来改变配置文件的名称:

# 查找myproject.yaml,而不是查找application.yaml
$ java -jar myproject.jar --spring.config.name=myproject

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

$ 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)导入附加数据

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

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

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

(5)使用配置树

一般是用在K8s中。

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)属性占位符

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

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文件支持多文档:

spring:
  application:
    name: "MyApp"
---
spring:
  application:
    name: "MyCloudApp"
  config:
    activate:
      on-cloud-platform: "kubernetes"

(8)属性激活

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

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

5、配置随机值

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

# 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)属性绑定

/*
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)构造器绑定

/*
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)可以自动注入

// 可以像普通的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)第三方配置

@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)。

/**
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

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

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

}
// 扫描要配置的包
@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配置类上的约束注释

@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的配置。

@Component
public class MyBean {

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

    // ...

}

三、源码分析

1、PropertySource

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

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

// 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配置

// 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获取配置

// 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;
}

5、总结

在这里插入图片描述

参考资料

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

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃了也弱了。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值