Spring Boot 外部化配置 - 中篇


文章说明

本系列会完整的介绍 Spring Boot 中外部化配置相关应用以及部分源码分析

  • 上篇 - 什么是外部化配置,外部化配置有哪些应用,顺序覆盖性
  • 中篇 - 外部化配置 @Value 注入、Environment 抽象、@ConfigurationProperties、@ConditionalOnProperty 应用场景
  • 下篇 - 如何扩展外部化配置,代码演示覆盖顺序

项目环境

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 = 张三-application

@Value注解,在属性没有的时候会报错 java.lang.IllegalArgumentException: Could not resolve placeholder 'user.age' in value "${user.age}",所以我们可以增加一个默认值来避免这种情况

@Value("${user.age:31}")
private Integer age;

运行 XmlPlaceHolderExternalizedConfigurationBootstrap,输出:

User{id=1, name='zhangquan', age=31}
系统变量:zhangquan

1.2 @Value 构造器注入(Constructor Injection)

application.properties

user.id = 1
user.name = 张三-application
user.age = 32
public class ValueAnnotationBootstrap {

	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) {
		// 因为 new SpringApplicationBuilder(ValueAnnotationBootstrap.class),指定了是引导类,
		// 所以框架帮我们配置了 Configuration Class,不然需要加上 @Configuration 注解
		ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(ValueAnnotationBootstrap.class)
				.web(WebApplicationType.NONE)
				.run(args);

		User user = applicationContext.getBean("user", User.class);
		System.out.println("用户对象:" + user);

		// 关闭 Spring 上下文
		applicationContext.close();
	}
}

执行结果:

用户对象 @Value 构造器注入:User{id=1, name='zhangquan', age=32}

id = 1:是因为 resources/application.properties 文件中 user.id=1

name=zhangquan,是因为外部化配置顺序覆盖问题,系统变量 user.name 覆盖了 resources/application.properties 文件中 user.name = 张三-application

1.3 @Value 方法注入(Method Injection)

从上面的例子可以看到 构造器注入 相对比较复杂,我们可以改造成 方法注入

  • 只需要将注入的参数放到 user 方法中即可

    public class ValueAnnotationBootstrap {
    
    	@Bean
    	public User user2(@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) {
    		// 因为 new SpringApplicationBuilder(ValueAnnotationBootstrap.class),指定了是引导类,
    		// 所以框架帮我们配置了 Configuration Class,不然需要加上 @Configuration 注解
    		ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(ValueAnnotationBootstrap.class)
    				.web(WebApplicationType.NONE)
    				.run(args);
    
    		User user2 = applicationContext.getBean("user2", User.class);
    		System.out.println("用户对象 @Value 方法注入 user2: " + user2);
    
    
    		// 关闭 Spring 上下文
    		applicationContext.close();
    	}
    }

    执行结果:

    用户对象 @Value 方法注入 user2: User{id=1, name='zhangquan', age=32}

1.4 @Value 默认值嵌套

默认值的设置也支持 ${xxx.xxx} 的方式

application.properties 文件中

my.user.age = 33

User.java 注解

//@Value("${user.age}")
private Integer age;
@Bean
public User user2(@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;
}

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 的方法注入进行获取的
public class ValueAnnotationBootstrap {	
  
	@Bean
	public User user3(Environment environment) {
		Long id = environment.getProperty("user.id", Long.class);
		String name = environment.getProperty("user.name");
		Integer age = environment.getProperty("user.age", Integer.class, 20);

		User user = new User();
		user.setId(id);
		user.setName(name);
		user.setAge(age);
		return user;
	}
User user3 = applicationContext.getBean("user3", User.class);
System.out.println("用户对象 Environment 方法注入 user3: " + user3);

运行结果:

用户对象 Environment 方法注入 user3: User{id=1, name='zhangquan', age=32}

2.2 Environment 构造器注入

public class ValueAnnotationBootstrap {

  private  Environment environment;

	@Autowired
	public ValueAnnotationBootstrap(Environment environment) {
		this.environment = environment;
	}
  
  @Bean
	public User user4() {
		Long id = this.environment.getProperty("user.id", Long.class);
		String name = this.environment.getProperty("user.name");
		Integer age = this.environment.getProperty("user.age", Integer.class, 20);

		User user = new User();
		user.setId(id);
		user.setName(name);
		user.setAge(age);
		return user;
	}
User user4 = applicationContext.getBean("user4", User.class);
System.out.println("用户对象 Environment 构造器注入 user4: " + user4);

运行结果:

用户对象 Environment 构造器注入 user4: User{id=1, name='zhangquan', age=32}

2.3 Environment 字段注入

public class ValueAnnotationBootstrap {
  
  @Autowired
	private Environment environment1;
  
  @Bean
	public User user5() {
		Long id = this.environment1.getProperty("user.id", Long.class);
		String name = this.environment1.getProperty("user.name");
		Integer age = this.environment1.getProperty("user.age", Integer.class, 20);

		User user = new User();
		user.setId(id);
		user.setName(name);
		user.setAge(age);
		return user;
	}
User user5 = applicationContext.getBean("user5", User.class);
System.out.println("用户对象 Environment 字段注入 user5: " + user5);

运行结果:

用户对象 Environment 字段注入 user5: User{id=1, name='zhangquan', age=32}

2.4 通过 Aware 回调接口注入

public class ValueAnnotationBootstrap implements EnvironmentAware {
  
  private Environment environment3;
  
  @Override
	public void setEnvironment(Environment environment) {
			this.environment3 = environment;
	}
  
  @Bean
	public User user6() {
		Long id = this.environment3.getProperty("user.id", Long.class);
		String name = this.environment3.getProperty("user.name");
		Integer age = this.environment3.getProperty("user.age", Integer.class, 20);

		User user = new User();
		user.setId(id);
		user.setName(name);
		user.setAge(age);
		return user;
	}
User user6 = applicationContext.getBean("user6", User.class);
System.out.println("用户对象 Environment 通过Aware回调接口注入 user6: " + user6);

运行结果:

用户对象 Environment 通过Aware回调接口注入 user6: User{id=1, name='zhangquan', age=32}

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(),相关代码如下:

// Register default environment beans.
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
  	beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}

3. @ConfigurationProperties Bean 绑定

3.1 类级别标注

只需要在 User 类上加 @ConfigurationProperties 注解即可

其中 “user” 表示的是 properties 文件中的前缀 user.id = 1

@ConfigurationProperties("user")
public class User {
  ...

application.properties 文件

#user.age = 32

测试引导类,@EnableAutoConfiguration 注解不能少

@EnableAutoConfiguration
public class ConfigurationPropertiesBootstrap {

	@Bean
	public User user() {
		return new User();
	}

	public static void main(String[] args) {
		ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(ConfigurationPropertiesBootstrap.class)
				.web(WebApplicationType.NONE)
				.run(args);

		User user = applicationContext.getBean("user", User.class);
		System.out.println("用户对象 @ConfigurationProperties 绑定方式 user: " + user);

		// 关闭 Spring 应用上下文
		applicationContext.close();
	}
}

执行结果:

用户对象 @ConfigurationProperties 绑定方式 user: User{id=1, name='zhangquan', age=null}

可以看到 user 确实也被赋值了,但是 age = null ,表示值可以为空,也不会报错,这点和 @Value 不一样。

官方还有一种方式,可以在引导类上加上 @EnableConfigurationProperties(User.class),这样我们就不用自己来注册一个 User Bean 对象,但是只能通过类型的方式获取 Bean 对象。

@EnableAutoConfiguration
@EnableConfigurationProperties(User.class)
public class ConfigurationPropertiesBootstrap {
	//
	//@Bean
	//public User user() {
	//	return new User();
	//}

	public static void main(String[] args) {
		ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(ConfigurationPropertiesBootstrap.class)
				.web(WebApplicationType.NONE)
				.run(args);

		//User user = applicationContext.getBean("user", User.class);
		//System.out.println("用户对象 @ConfigurationProperties 绑定方式 user: " + user);

		User user = applicationContext.getBean(User.class);
		System.out.println("用户对象 @EnableConfigurationProperties 绑定方式 user: " + user);

		// 关闭 Spring 应用上下文
		applicationContext.close();
	}
}

3.2 @Bean 方法声明

同样的效果, @ConfigurationProperties 可以标准在对应的方法上

@EnableAutoConfiguration
//@EnableConfigurationProperties(User.class)
public class ConfigurationPropertiesBootstrap {
  
  @Bean
	@ConfigurationProperties("user")
	public User user() {
		return new User();
	}
  ...

3.3 嵌套类型绑定

Spring Boot 官网原文档地址:https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#boot-features-external-config-typesafe-configuration-properties

改造 User 类型,增加 City 的属性,同样增加 setter/getter 方法

@ConfigurationProperties("user")
public class User {

	private Long id;

	private String name;

	//@Value("${user.age}")
	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 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;
	}

	public City getCity() {
		return city;
	}

	public void setCity(City city) {
		this.city = city;
	}

	@Override
	public String toString() {
		return "User{" +
				"id=" + id +
				", name='" + name + '\'' +
				", age=" + age +
				", city=" + city +
				'}';
	}
}

application.properties

user.id = 1
user.name = 张三-application
#user.age = 32
#my.user.age = 33
# 增加 user.city 外部化配置
user.city.postCode = 4000
user.city.name = shanghai
用户对象 user: User{id=1, name='zhangquan', age=null, city=City{postCode='4000', name='shanghai'}}

3.4 松散绑定(Relaxed Binding)

Spring Boot 官网原文档地址:https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#boot-features-external-config-relaxed-binding

比如我们将 application.properties 中的 postCode 属性修改为其他几种方式效果都一样

user.city.postCode = 4000
user.city.post-code = 4000
user.city.post_code = 4000
#USER_CITY_POST_CODE = 4000

USER_CITY_POST_CODE = 4004 这种方式必须是 system environment variables 系统环境变量,

所以我们需要修改 Idea Run 引导类启动配置,才能看到效果,增加一个system environment variables

执行结果:

用户对象 user: User{id=1, name='zhangquan', age=null, city=City{postCode='4000', name='shanghai'}}

3.5 效验

Spring Boot 官网原文档地址:https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/#boot-features-external-config-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
@ConfigurationProperties("user")
public class User {

	//@Value("${user.age}")
	@NotNull
	private Integer age;

注释掉 properties 中相应的属性

#user.age = 32

执行结果:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'user' to com.zq.externalized.configuration.domain.User failed:

    Property: user.age
    Value: null
    Reason: 不能为null

4.@ConditionalOnProperty

这个条件注解表示如果当前环境中存在此 Property 属性,加载当前 Bean,如果不存在则不会加载。

@EnableAutoConfiguration
public class ConditionalOnPropertyBootstrap {

	@Bean
	@ConfigurationProperties("user")
	@ConditionalOnProperty("user.city.post_code")
	public User user() {
		return new User();
	}

	public static void main(String[] args) {
		ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(ConditionalOnPropertyBootstrap.class)
				.web(WebApplicationType.NONE)
				.run(args);

		//User user = applicationContext.getBean("user", User.class);
		//System.out.println("用户对象 @ConfigurationProperties 绑定方式 user: " + user);

		User user = applicationContext.getBean(User.class);
		System.out.println("用户对象 user: " + user);

		// 关闭 Spring 应用上下文
		applicationContext.close();
	}
}

如果注释掉 application.properties 中的 user.city.post_code属性。

执行结果:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.zq.externalized.configuration.domain.User' available

如果配置这个属性那么正常执行:

用户对象 user: User{id=1, name='zhangquan', age=32, city=City{postCode='4000', name='shanghai'}}

这个注解还有两个方法可以配置

/**
 * 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 = true, havingValue = "4000")
    public User user() {
    	return new User();
    }

5.参考

  • 慕课网-小马哥《Spring Boot2.0深度实践之核心技术篇》

文章作者: 张权
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张权 !
评论
  目录