不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

Springboot教程系列(3) Springboot的外部化配置

Springboot的外部化配置

外部化配置概念理解

什么是外部化配置

这个名词来源于Springboot官方文档的某一个章节名称,官方并没有对其下过准确的定义。一般研发人员,运维人员之间沟通时,经常会提及它。

有外部化配置也就有内部化配置,一般我们把在代码中枚举类,或硬性编码的部分称之为内部化配置。内部化配置缺少灵活性。

一个很熟悉的场景:一般公司的系统都会划分为开发(dev),测试(test),生产(prod)三个环境,每个环境的数据库、参数配置肯定是不一样的。一般公司都会借助spring的profile结合maven或gradle构建软件实现灵活的构建,部署好的软件系统自动对应到相应的环境,不用进行源码的修改。

还有springcloud的微服务实践中,通常也会引入总线配置方式,实现在系统不重启的情况下,实现修改某些参数或配置的目的。这些业务场景下配置其实就是外部化配置思想。抽象下概念外部化配置可以理解为:对于可扩展性应用系统,其内部组件是可配置化的,比如:认证信息、端口范围、线程池属性等。

  • 官方链接:https://docs.spring.io/spring-boot/docs/2.0.2.RELEASE/reference/htmlsingle/#boot-features-external-config

springboot中外部化配置

springboot官方提供了三种外部化配置应用方式:

  • Bean的@Value注入
  • Spring Eviroment读取
  • @ConfigurationProperties綁定到结构化对象

外部化配置实际应用

XML Bean的属性占位符

  • 比如在spring的xml配置文件中添加如下配置:
<bean id="user" class="com.hzqiuxm.configuration.domain.User">
<property name="id" value="${user.id}"/>
<property name="name" value="${user.name}"/>
</bean>
  • 另外一个xml配置文件内容:
<!-- 属性占位符配置-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <!-- Properties 文件 classpath 路径 -->
    <property name="location" value="classpath:/config/application.properties"/>
    <!-- 文件字符编码 -->
    <property name="fileEncoding" value="UTF-8"/>
</bean>
  • 在属性配置文件(application.properties)中添加
# 用户配置属性
user.id = 10
user.name =临江仙2018

创建对应的实体类user,添加对应字段的get/set方法后,启动Spirng启动到类获取到实体类User的Bean,可以看到其id和name字段的值为配置文件中配置的值

public class SpringXmlConfigPlaceholderBootstrap {
    public static void main(String[] args) {
        String[] locations = {"META-INF/spring/spring-context.xml", "META-INF/spring/user-context.xml"};
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(locations);
        User user = applicationContext.getBean("user", User.class);
        System.err.println("用户对象 : " + user);
        // 关闭上下文
        applicationContext.close();
    }
}

输出结果:id=10, name='临江仙 2018',符合预期和我们配置文件中的值一致。这种方式是SpringFrame中普遍使用的方式,springboot出现后使用频率已经越来越少了。

接下来我们来搞点事情,换成springboot的方式启动

@ImportResource("META-INF/spring/user-context.xml") // 加载 Spring 上下文 XML 文件
@EnableAutoConfiguration
public class XmlPlaceholderExternalizedConfigurationBootstrap {

    public static void main(String[] args) {

        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(XmlPlaceholderExternalizedConfigurationBootstrap.class)
                        .web(WebApplicationType.NONE) // 非 Web 应用
                        .run(args);

        User user = context.getBean("user", User.class);

        System.err.println("用户对象 : " + user);
        // 关闭上下文
        context.close();
    }
}

输出结果:id=10, name='hzqiuxm',为什么不符合预期?id是能对应上的,name的值确不对了?其实是 PropertySources顺序问题捣的鬼,这也是本文需要介绍的内容之一,相信看完文章后你就恍然大悟了。

@Value的注解方式

这种应用在平时开发中非常常见,很多开发人员都采用这种方式注入自定义的一些配置属性值。主要有三种注入方式:

  • 字段注入
  • 构造器注入
  • 方法注入

主要的用法举例:

@Value("${user.id}") //普通属性注入
private Long userId;
@Value("${user.age:${my.user.age:32}}") //嵌套属性注入,非常适合新老API兼容的设计,user.age代表老的,my.user.age代表新的,32代表默认的
private int age;
@Value("#{'${list}'.split(',')}") //list注入
private List<String> list;
@Value("#{${maps}}")  //map注入
private Map<String,String> maps;
--------------------------------------
对应配置文件:
user.id:1
my.user.age:32
list: topic1,topic2,topic3
maps: "{key1: 'value1', key2: 'value2'}"

Eviroment方式读取

  • 方法/构造器依赖注入
@Override
public void setEnvironment(Environment environment) {
    if (this.environment != environment) {
        throw new IllegalStateException();
    }
}
  • @Autowired依赖注入
@Autowired
@Qualifier(ENVIRONMENT_BEAN_NAME)
private Environment environment;
  • EviromentAware 接口回调
    实现 EnvironmentAwarek接口

  • BeanFactory 依赖查找Environment
    实现 BeanFactoryAware接口

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    if (this.environment != beanFactory.getBean(ENVIRONMENT_BEAN_NAME, Environment.class)) {
        throw new IllegalStateException();
    }
}

三者的执行顺序:1 @Autowired; 2 BeanFactoryAware ;3 EviromentAware

@ConfigurationProperties Bean绑定

  • 类级别注入

  • @Bean方法声明

  • 嵌套类型绑定

外部化配置扩展

定义外部化属性源

  • PropertySources的顺序问题:官方提供的参考如下(Springboot版本需要1.5以上,低版本会缺少部分)


- 什么是PropertySource
带有名称的属性源,Properties文件、Map、YAML 文件等都可以称之为PropertySource。

  • 什么是Eviroment抽象
    Environment与PropertySources可以看成是一一对应的关系;PropertySource与PropertySources从单词的单数和复数关系也可以看的出是 1 对 多的关系;ConfigurableEnvironment与MutablePropertySources相对应。

PropertySources属性源使用时机

  • Spring Framework 中,尽量在org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory方法前初始化。
  • Spring Boot 中,尽量在org.springframework.boot.SpringApplication#refreshContext(context)方法前初始化。

扩展外部化配置属性源

基于 SpringApplicationRunListener#environmentPrepared 扩展外部化配置属性源
  • 实现两个接口:SpringApplicationRunListener, Ordered
  • META-INF下新建spring.factories文件,添加如下配置
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesRunListener
  • 重写SpringApplicationRunListener#environmentPrepared 和 getOrder
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {

    MutablePropertySources propertySources = environment.getPropertySources();
    Map<String, Object> source = new HashMap<>();
    source.put("user.id", "0"); //设置编号为0
    MapPropertySource propertySource = new MapPropertySource("from-environmentPrepared", source);
    propertySources.addFirst(propertySource);
}

@Override
public int getOrder() {
    return new EventPublishingRunListener(application,args).getOrder() + 1;//返回排在默认的后面
}

各个文件中的配置如下:

自定义environmentPrepared中: 0
application.properties : 10
META-INF/default.properties : 11
  • 定义引导类ExtendPropertySourcesBootstrap,并模拟一个Command line arguments(88) 和Default properties(99) 配置方式
@EnableAutoConfiguration
@Configuration
@PropertySource(name = "from default.properties", value = "classpath:META-INF/spring/default.properties")
public class ExtendPropertySourcesBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                new SpringApplicationBuilder(ExtendPropertySourcesBootstrap.class)
                        .web(WebApplicationType.NONE) // 非 Web 应用
                        .properties("user.id=99")        // Default properties
                        .run(of("--user.id=88")); // Command line arguments.
        // 获取 Environment 对象
        ConfigurableEnvironment environment = context.getEnvironment();
        System.err.printf("用户id : %d\n", environment.getProperty("user.id", Long.class));
        environment.getPropertySources().forEach(propertySource -> {
            System.err.printf("PropertySource[名称:%s] : %s\n", propertySource.getName(), propertySource);
        });

        // 关闭上下文
        context.close();
    }
    private static <T> T[] of(T... args) {
        return args;
    }

}

根据上一节提到的PropertySources的顺序问题,我们可以猜测 我们自定义的优先级应该最高,所以结果应该是0
输出结果如下,符合预期:

用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

类似的我们还可以重载:contextPrepared和contextLoaded方式来实现自定义的配置。三个方法的执行顺序为:
1 environmentPrepared;2 contextPrepared;3 contextLoaded;因为我们采用的是addFirst方法,先执行会被后执行的覆盖,三者优先级是倒过来的,这点需要特别注意

执行顺序在SpringApplication#run中可以看到,可以翻看之前的一篇文章获得具体详情,这里给标注下源码和相应位置

基于 SpringApplicationRunListener#contextPrepared 扩展外部化配置属性源
  • 参考SpringApplicationRunListener#environmentPrepared
基于 SpringApplicationRunListener#contextLoaded 扩展外部化配置属性源
  • 参考SpringApplicationRunListener#environmentPrepared
基于 ApplicationEnvironmentPreparedEvent 扩展外部化配置属性源
  • 实现ApplicationListener接口,并重载onApplicationEvent方法
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    MutablePropertySources propertySources = environment.getPropertySources();
    Map<String, Object> source = new HashMap<>();
    source.put("user.id", "9");//设置程9
    MapPropertySource propertySource = new MapPropertySource("from-ApplicationEnvironmentPreparedEvent", source);
    propertySources.addFirst(propertySource);
}
  • META-INF下新建spring.factories文件,添加如下配置
# Event Listeners
org.springframework.context.ApplicationListener=\
com.imooc.diveinspringboot.externalized.configuration.configuration.ExtendPropertySourcesEventListener
  • 启动之前引导类ExtendPropertySourcesBootstrap
    由于ApplicationListener是在SpringApplication构造的时候调用的,执行顺序肯定在SpringApplicationRunListener相关方法之前执行,根据执行被覆盖的原则,输出的值应该是0

  • 输出结果如下:

用户id : 0
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from default.properties] : ResourcePropertySource {name='from default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

符合我们分析的结果

基于 EnvironmentPostProcessor 扩展外部化配置属性源
  • 实现 EnvironmentPostProcessor接口和Ordered接口,分别重载它们的postProcessEnvironment和getOrder方法
  • 在ApplicationEnvironmentPreparedEvent之前执行,所以如果实现了其它的自定义方式,它就会被覆盖
  • META-INF下新建spring.factories文件,添加如下配置
# EnvironmentPostProcessor
org.springframework.boot.env.EnvironmentPostProcessor=\
com.imooc.diveinspringboot.externalized.configuration.processor.ExtendPropertySourcesEnvironmentPostProcessor
  • 具体代码SpringApplicationRunListener#environmentPrepared
基于 ApplicationContextInitializer 扩展外部化配置属性源
  • 在ApplicationContextInitializer上下文初始化的时候进行配置,在SpringApplicationRunListener#environmentPrepared之后执行,所以会覆盖environmentPrepared
  • 不会覆盖contextPrepared和contextLoaded;
  • META-INF下新建spring.factories文件,添加如下配置
# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
com.imooc.diveinspringboot.externalized.configuration.initializer.ExtendPropertySourcesApplicationContextInitializer
  • 具体代码参考SpringApplicationRunListener#environmentPrepared

各自定义PropertySource优先级(从高到低)

PropertySource[名称:from-contextLoaded] : MapPropertySource {name='from-contextLoaded'}
PropertySource[名称:from-contextPrepared] : MapPropertySource {name='from-contextPrepared'}
PropertySource[名称:from-ApplicationContextInitializer] : MapPropertySource {name='from-ApplicationContextInitializer'}
PropertySource[名称:configurationProperties] : ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertySource[名称:from-environmentPrepared] : MapPropertySource {name='from-environmentPrepared'}
PropertySource[名称:from-ApplicationEnvironmentPreparedEvent] : MapPropertySource {name='from-ApplicationEnvironmentPreparedEvent'}
PropertySource[名称:from-EnvironmentPostProcessor] : MapPropertySource {name='from-EnvironmentPostProcessor'}
PropertySource[名称:commandLineArgs] : SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertySource[名称:systemProperties] : MapPropertySource {name='systemProperties'}
PropertySource[名称:systemEnvironment] : OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
PropertySource[名称:random] : RandomValuePropertySource {name='random'}
PropertySource[名称:applicationConfig: [classpath:/config/application.properties]] : OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/config/application.properties]'}
PropertySource[名称:from classpath:META-INF/default.properties] : ResourcePropertySource {name='from classpath:META-INF/default.properties'}
PropertySource[名称:defaultProperties] : MapPropertySource {name='defaultProperties'}

实时扩展外部化配置属性源:Eviroment支持,@Value和@ConfigurationProperties 不支持
理解清楚了各种自定义外部化配置的优先级,可以在自己设计框架的时候控制不想被开发人员影响到的配置。

未经允许不得转载:菡萏如佳人 » Springboot教程系列(3)

欢迎加入极客江湖

进入江湖关于作者