Spring Boot 外部化配置 - 中篇
文章目录
文章说明
本系列会完整的介绍 Spring Boot 中外部化配置相关应用以及部分源码分析
- 上篇 - 什么是外部化配置,外部化配置有哪些应用,顺序覆盖性
- 中篇 - @Value 注入、Environment 抽象、@ConfigurationProperties、@ConditionalOnProperty 应用
- 下篇 - 如何扩展外部化配置,代码演示覆盖顺序
项目环境
- jdk 1.8
- Spring Boot 2.0.2.RELEASE
- github 地址:https://github.com/huajiexiewenfeng/deep-in-spring-boot
- 本章模块:externalized-configuration
1.@Value 注入
1.1 @Value 字段注入(Field Injection)
改造 User 类
- 新增 age 属性,采用字段注入的方式
- 添加
@Value("${user.age}")
public class User {
private Long id;
private String name;
@Value("${user.age}")
private Integer age;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
同时在 application.properties 文件中增加对应的属性配置
user.id = 1
user.name = 小仙2020
user.age = 18
执行结果:
User{id=1, name='xwf', age=18}
不过 @Value 注解,在属性没有的时候会报错 java.lang.IllegalArgumentException: Could not resolve placeholder 'user.age' in value "${user.age}"
所以我们可以增加一个默认值来避免这种情况
@Value("${user.age:30}")
private Integer age;
1.2 @Value 构造器注入(Constructor Injection)
@EnableAutoConfiguration
public class ValueAnnotationBootstrap { // 因为是引导类的原因所以作为了 Configuration.class,一般配置类需要 @Configuration 注解标注
private final Long id;
private final String name;
private final Integer age;
public ValueAnnotationBootstrap(@Value("${user.id}") Long id,
@Value("${user.name}") String name,
@Value("${user.age}") Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
@Bean
public User user() {
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
return user;
}
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ValueAnnotationBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
User user = context.getBean("user", User.class);
System.out.println("用户对象 : " + user);
// 关闭上下文
context.close();
}
}
执行结果:
用户对象 : User{id=1, name='Administrator', age=18}
1.3 @Value 方法注入(Method Injection)
从上面的例子可以看到 构造器注入 相对比较复杂,我们可以改造成 方法注入
- 只需要将注入的参数放到
user
方法中即可
@EnableAutoConfiguration
public class ValueAnnotationBootstrap { // 因为是引导类的原因所以作为了 Configuration.class,一般配置类需要 @Configuration 注解标注
@Bean
public User user(@Value("${user.id}") Long id,
@Value("${user.name}") String name,
@Value("${user.age}") Integer age) {
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
return user;
}
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ValueAnnotationBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
User user = context.getBean("user", User.class);
System.out.println("用户对象 : " + user);
// 关闭上下文
context.close();
}
}
执行结果:
用户对象 : User{id=1, name='Administrator', age=18}
1.4 @Value 默认值嵌套
默认值的设置也支持 ${xxx.xxx}
的方式
@Bean
public User user(@Value("${user.id}") Long id,
@Value("${user.name}") String name,
@Value("${user.age:${my.user.age:19}}") Integer age) {
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
return user;
}
可以多层嵌套
@Value("${user.age:${my.user.age:${your.user.age:88}}}") Integer age
2.Environment 抽象
Environment 是什么?
源码位置:org.springframework.core.env.Environment
public interface Environment extends PropertyResolver {
...
从源码可以看到它继承了 PropertyResolver,我们继续看 PropertyResolver 中的方法
- getProperty(“user.id”) 类似于 @Value("${user.id}")
- getProperty(“user.id”,“30”) 类似于 @Value("${user.id:30}")
- getProperty(“user.id”,Long.class) 会将返回值转换成 Long 类型
- … 其他的就不做介绍了
/**
* Return the property value associated with the given key,
* or {@code null} if the key cannot be resolved.
* @param key the property name to resolve
* @see #getProperty(String, String)
* @see #getProperty(String, Class)
* @see #getRequiredProperty(String)
*/
@Nullable
String getProperty(String key);
/**
* Return the property value associated with the given key, or
* {@code defaultValue} if the key cannot be resolved.
* @param key the property name to resolve
* @param defaultValue the default value to return if no value is found
* @see #getRequiredProperty(String)
* @see #getProperty(String, Class)
*/
String getProperty(String key, String defaultValue);
/**
* Return the property value associated with the given key,
* or {@code null} if the key cannot be resolved.
* @param key the property name to resolve
* @param targetType the expected type of the property value
* @see #getRequiredProperty(String, Class)
*/
@Nullable
<T> T getProperty(String key, Class<T> targetType);
通过上面的接口分析,可以知道如何通过 Environment 获取我们外部化配置的相关属性,下面我们来演示如何获取 Environment 对象。
2.1 Environment 方法注入
- 这里的 Environment 对象是通过 @Bean 的方法注入进行获取的
@EnableAutoConfiguration
public class ValueAnnotationBootstrap { // 因为是引导类的原因所以作为了 Configuration.class,一般配置类需要 @Configuration 注解标注
@Bean
public User user2(Environment environment) {
Long id = environment.getProperty("user.id",Long.class);
String name = environment.getProperty("user.name",String.class);
Integer age = environment.getProperty("user.age",Integer.class,88);
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
return user;
}
@Bean
public User user(@Value("${user.id}") Long id,
@Value("${user.name}") String name,
@Value("${user.age:${my.user.age:${your.user.age:88}}}") Integer age) {
User user = new User();
user.setId(id);
user.setName(name);
user.setAge(age);
return user;
}
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ValueAnnotationBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
User user = context.getBean("user", User.class);
User user2 = context.getBean("user2", User.class);
System.out.println("用户对象1 : " + user);
System.out.println("用户对象2 : " + user);
// 关闭上下文
context.close();
}
}
执行结果
用户对象1 : User{id=1, name='Administrator', age=88}
用户对象2 : User{id=1, name='Administrator', age=88}
2.2 Environment 构造器注入
@EnableAutoConfiguration
public class ValueAnnotationBootstrap {
private final Environment environment;
@Autowired//可写 可不写
public ValueAnnotationBootstrap(Environment environment) {
this.environment = environment;
}
...
2.3 Environment 字段注入
@EnableAutoConfiguration
public class ValueAnnotationBootstrap {
@Autowired
private Environment environment;
...
2.4 通过 Aware 回调接口注入
@EnableAutoConfiguration
public class ValueAnnotationBootstrap implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment
}
...
2.5 通过 BeanFactory#getBean 依赖查找的方式
- BeanFactory 对象同样也可通过 Aware 接口回调的方式获取
- 然后通过
beanFactory.getBean
依赖查找的方式,根据 Environment.class 类型进行查找获取 Environment 对象实例
Environment environment = beanFactory.getBean(Environment.class);
2.6 Environment 对象何时创建?
Spring Boot 场景中 org.springframework.boot.SpringApplication#run(java.lang.String…)
在 SpringApplication.run() 启动过程中,调用了 prepareEnvironment 方法,表示预处理 Environment 对象,在此方法中 getOrCreateEnvironment 方法来创建 Environmen对象;在 Web Servlet 场景中创建的是 StandardServletEnvironment 对象,其它场景创建 StandardEnvironment 对象。
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
if (this.webApplicationType == WebApplicationType.SERVLET) {
return new StandardServletEnvironment();
}
return new StandardEnvironment();
}
而在传统 Spring Framewrok 场景中,相关的创建代码在
AbstractApplicationContext#refresh() 启动应用上下文中的 prepareBeanFactory() 666 行,相关代码如下:
// Register default environment beans.
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
最终创建代码
@Override
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
this.environment = createEnvironment();
}
return this.environment;
}
...
protected ConfigurableEnvironment createEnvironment() {
return new StandardEnvironment();
}
由此可见,在 Spring Boot 和 Spring 场景中,Environment 对象的创建并不相同。
继续分析
在 Spring Boot 场景中,SpringApplication#run 继续分析
- prepareEnvironment 处理完之后获得 ConfigurableEnvironment 对象
- 在 SpringApplication#prepareContext 中通过
context.setEnvironment(environment);
的方式设置到 ApplicationContext 应用上下文中 - Spring Fremawork 中 getEnvironment 如果判断 environment 存在,就会直接返回,不再自己进行创建
由此可知,在 Spring Boot 场景中,当前的应用上下文中只存在一个 Environment 对象,而且是由 Spring Boot 生命周期进行创建。
prepareContext 方法
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
...
3. @ConfigurationProperties Bean 绑定
3.1 类级别标注
只需要在 User 类上加 @ConfigurationProperties 注解即可
- 其中 “user” 表示的是 properties 文件中的前缀
user
.id = 1
@ConfigurationProperties("user")
public class User {
...
测试引导类
/**
* {@link ConfigurationProperties}注解引导类
*
* @see ConfigurationProperties
*/
@EnableAutoConfiguration
public class ConfigurationPropertiesBootstrap {
@Bean
private User user(){
return new User();
}
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ConfigurationPropertiesBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
User user = context.getBean("user", User.class);
System.out.println("用户对象1 : " + user);
// 关闭上下文
context.close();
}
}
执行结果:
用户对象1 : User{id=1, name='Administrator', age=null}
可以看到 user 确实也被赋值了,但是 age = null ,表示值可以为空,也不会报错,这点和 @Value
不一样。
官方还有一种方式,可以在引导类上加上 @EnableConfigurationProperties(User.class)
,这样我们就不用自己来注册一个 User Bean 对象,但是只能通过类型的方式获取 Bean 对象。
@EnableAutoConfiguration
@EnableConfigurationProperties(User.class)
public class ConfigurationPropertiesBootstrap {
// @Bean
// private User user(){
// return new User();
// }
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ConfigurationPropertiesBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
// User user = context.getBean("user", User.class);
User user = context.getBean(User.class);
System.out.println("用户对象1 : " + user);
// 关闭上下文
context.close();
}
}
3.2 @Bean 方法声明
同样的效果, @ConfigurationProperties 可以标准在对应的方法上
@EnableAutoConfiguration
public class ConfigurationPropertiesBootstrap {
@Bean
@ConfigurationProperties("user")
private User user(){
return new User();
}
...
3.3 嵌套类型绑定
Spring Boot 官网原文档地址:
https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/html/boot-features-external-config.html
24.7 Type-safe Configuration Properties
改造 User 类型,增加 City 的属性,同样增加 setter/getter 方法
@ConfigurationProperties("user")
public class User {
private Long id;
private String name;
// @Value("${user.age:30}")
private Integer age;
private City city;
private static class City{
private String postCode;
private String name;
public String getPostCode() {
return postCode;
}
public void setPostCode(String postCode) {
this.postCode = postCode;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "City{" +
"postCode='" + postCode + '\'' +
", name='" + name + '\'' +
'}';
}
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", city=" + city +
'}';
}
}
application.properties
user.id = 1
user.name = 小仙2020
#my.user.age = 18
# 增加 User.City 外部化配置
user.city.postCode = 4000
user.city.name = wuhan
执行结果:
用户对象1 : User{id=1, name='Administrator', age=null, city=City{postCode='4000', name='wuhan'}}
3.4 松散绑定(Relaxed Binding)
Spring Boot 官网原文档地址:
https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/html/boot-features-external-config.html
24.7.2 Relaxed Binding
Property Note acme.my-project.person.first-name
Kebab case, which is recommended for use in .properties
and.yml
files.acme.myProject.person.firstName
Standard camel case syntax. acme.my_project.person.first_name
Underscore notation, which is an alternative format for use in .properties
and.yml
files.ACME_MYPROJECT_PERSON_FIRSTNAME
Upper case format, which is recommended when using system environment variables.
比如我们将 application.properties 中的 postCode 属性修改为其他几种方式效果都一样
user.city.post-code = 4001
user.city.postCode = 4002
user.city.post_code = 4003
#USER_CITY_POST_CODE = 4004
USER_CITY_POST_CODE = 4004 这种方式必须是 system environment variables
系统环境变量,
所以我们需要修改 Idea Run 引导类启动配置,才能看到效果,增加一个system environment variables
执行结果:
用户对象1 : User{id=1, name='Administrator', age=null, city=City{postCode='4004', name='wuhan'}}
3.5 效验
Spring Boot 官网原文档地址:
https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/html/boot-features-external-config.html
24.7.5 @ConfigurationProperties Validation
具体的使用方式可以参考 Spring Bean Validation 的使用
这里只做简单的演示
这里需要引入 javax.validation
的相关实现依赖
pom.xml 中加入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
修改 User 类
- 类上标注 @Validated
- 对应需要效验的字段上标注 @NotNull(当然这里有很多注解,详情可以看 JSR-303 规范)或者定位到
javax.validation.constraints
包下查看相应的源代码文件
@Validated
public class User {
private Long id;
private String name;
// @Value("${user.age:30}")
private Integer age;
private City city;
private static class City{
private String postCode;
@NotNull
private String name;
...
注释掉 properties 中相应的属性
user.id = 1
user.name = 小仙2020
# 增加 User.City 外部化配置
#user.city.name = wuhan
执行结果:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'user' to com.huajie.deepinspringboot.externlized.configuration.domain.User failed:
Property: user.city.name
Value: null
Reason: 不能为null
4.@ConditionalOnProperty
这个条件注解表示如果当前环境中存在此 Property 属性,加载当前 Bean,如果不存在则不会加载。
@EnableAutoConfiguration
//@EnableConfigurationProperties(User.class)
public class ConfigurationPropertiesBootstrap {
@Bean
@ConfigurationProperties("user")
@ConditionalOnProperty("user.city.post_code")
private User user(){
return new User();
}
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ConfigurationPropertiesBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
User user = context.getBean(User.class);
System.out.println("用户对象1 : " + user);
// 关闭上下文
context.close();
}
}
如果注释掉 application.properties 中的 user.city.post_code
属性。
执行结果:
NoSuchBeanDefinitionException 异常
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.huajie.deepinspringboot.externlized.configuration.domain.User' available
如果配置这个属性那么正常执行
用户对象1 : User{id=1, name='Administrator', age=null, city=City{postCode='4004', name='wuhan'}}
这个注解还有两个方法可以配置
/**
* The string representation of the expected value for the properties. If not
* specified, the property must <strong>not</strong> be equals to {@code false}.
* @return the expected value
*/
String havingValue() default "";
/**
* Specify if the condition should match if the property is not set. Defaults to
* {@code false}.
* @return if should match if the property is missing
*/
boolean matchIfMissing() default false;
- havingValue 表示如果当前的属性值等于我们配置的这个值,才会生效
- matchIfMissing 如果属性不存在,是否生效
@Bean
@ConfigurationProperties("user")
@ConditionalOnProperty(value = "user.city.post_code",matchIfMissing = false,havingValue = "4004")
private User user(){
return new User();
}
5.参考
- 慕课网-小马哥《Spring Boot2.0深度实践之核心技术篇》