Spring Boot 外部化配置 - 中篇

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 = 小仙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 生命周期进行创建。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PuxiBKIV-1590200563471)(G:\workspace\csdn\learn-document\spring-boot\慕课课程\csdn\image-20200523101021934.png)]
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

PropertyNote
acme.my-project.person.first-nameKebab case, which is recommended for use in .properties and .yml files.
acme.myProject.person.firstNameStandard camel case syntax.
acme.my_project.person.first_nameUnderscore notation, which is an alternative format for use in .properties and .yml files.
ACME_MYPROJECT_PERSON_FIRSTNAMEUpper 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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xSEL0PVX-1590200563473)(G:\workspace\csdn\learn-document\spring-boot\慕课课程\csdn\image-20200521152953027.png)]

执行结果:

用户对象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深度实践之核心技术篇》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值