聊一聊六种常用的属性配置读取方式
写在前面
本篇来学习使用SpringBoot进行日常开发过程中,经常使用到的6种读取配置文件内容的方式,掌握和熟练使用对于提升自我能力有极大帮助。使用的SpringBoot版本为 2.7.11 。
Environment
Environment简介
Environment 是 SpringBoot 的核心环境配置接口,它提供了很多方法用于访问应用程序属性,包括系统属性、操作系统环境变量、命令行参数和应用程序配置文件中定义的属性等。源码如下:
1 | public interface Environment extends PropertyResolver { |
可以看到这个接口继承自 PropertyResolver 接口,PropertyResolver接口中定义了很多获取属性的方法,因此Environment这个接口才具备上述能力。
初始化配置
接下来我们通过分析SpringBoot项目在启动时,对配置进行初始化这一过程来深度了解Environment接口的作用。
我们知道SpringBoot项目在启动时会调用 SpringApplication.run() 方法,而这个方法内部会调用prepareEnvironment()方法:
查看一下这个prepareEnvironment()方法的源码,如下所示:
1 | private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { |
简单解释一下上述方法的流程:
- 创建 ConfigurableEnvironment 环境对象 ,用于存储环境参数;
- 调用 configureEnvironment 方法加载默认的 application.properties 和 application.yml 配置文件,以及用户指定的配置文件,并将其封装为 PropertySource 对象添加到环境对象中;
- ConfigurationPropertySources.attach 方法,用于加载所有的系统属性,并将它们添加到环境对象中;
- listeners.environmentPrepared 方法,用于发送环境参数配置已经准备就绪的监听通知;
- DefaultPropertiesPropertySource.moveToEnd 方法,用于将系统默认属性源中的所有属性值移到环境对象的队列末尾,这样用户自定义的属性值就能覆盖默认的属性值,还可以避免用户无意中覆盖了SpringBoot提供的默认属性;
- bindToSpringApplication方法,用于将应用程序的属性绑定到Bean对象上;
- ConfigurationPropertySources.attach方法,用于再次加载系统配置,防止被其他配置覆盖。
下面是对于上述几个比较重要的方法进行详细介绍,如下所示:
(1)getOrCreateEnvironment方法的源码如下所示,可以看到该方法会返回一个ConfigurableEnvironment对象,该对象用于存储环境参数。如果已存在ConfigurableEnvironment对象,则直接使用它;否则根据用户的配置和默认配置创建一个新的ConfigurableEnvironment对象:
1 | private ConfigurableEnvironment getOrCreateEnvironment() { |
(2)configureEnvironment方法的源码如下所示,可以看到该方法会解析并加载用户指定的配置文件,并将其作为 PropertySource对象添加到环境对象中。configureEnvironment方法默认会解析application.properties
和 application.yml
文件,并将其添加到ConfigurableEnvironment对象中。
1 | protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { |
(3)DefaultPropertiesPropertySource.moveToEnd方法的源码如下所示,该方法会将默认属性源中的所有属性值移到环境对象的队列末尾,这样用户自定义的属性值就可以覆盖默认的属性值,还可以避免用户无意中覆盖了SpringBoot提供的默认属性:
1 | public static void moveToEnd(ConfigurableEnvironment environment) { |
通过前面的分析,我们知道各种配置属性最终都会被封装为一个个PropertySource对象,查看一下该对象的源码:
1 | public abstract class PropertySource<T> { |
PropertySource这个抽象类有很多实现类,分别用于管理应用程序的配置属性。不同的PropertySource实现类,可从不同的来源来获取配置属性,如文件、环境变量、命令行参数等。下面是涉及到的一些常用实现类:
简单解释一下上述涉及到的实现类的作用:
- MapPropertySource,用于将Map键值对转换为PropertySource对象;
- PropertiesPropertySource,用于将Properties对象中的配置属性转换为PropertySource对象;
- ResourcePropertySource,用于从文件系统或classpath中加载配置属性,并封装为PropertySource对象;
- ServletConfigPropertySource,用于从Servlet配置中读取配置属性,并封装为PropertySource对象;
- ServletContextPropertySource,用于从Servlet上下文中读取配置属性,并封装为PropertySource对象;
- StubPropertySource,这是一个空的实现类,它的作用仅仅是给CompositePropertySource类作为默认的父级属性源,以避免空指针异常;
- CompositePropertySource:,这是一个复合型的实现类,内部维护了PropertySource集合队列,可以将多个PropertySource对象进行合并;
- SystemEnvironmentPropertySource,用于从操作系统环境变量中读取配置属性,并将其封装为PropertySource对象。
上面各类配置初始化生成的PropertySource对象都会被维护到集合队列中:
1 | List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>() |
配置初始化完成后,应用程序上下文 AbstractApplicationContext 会加载配置,这样程序在运行时就可以随时获取到配置信息:
读取配置
前面我们已经学习了加载配置的整个流程,那么读取配置就是从维护的 PropertySource 队列中根据name获取对应的source对象了。
一般而言,我们会使用Environment接口对象提供的方法来获取配置信息,示例如下:
1 | @Slf4j |
实际上在前面阅读源码的时候,我们知道这个Environment接口继承PropertyResolver接口,PropertyResolver是获取配置的关键接口,其内部提供了操作PropertySource 队列的方法,查看一下这个接口的继承依赖关系:
因此,我们也可以直接使用PropertyResolver接口中的方法来获取对应的属性信息:
1 | @Slf4j |
Value注解
@Value注解,是 Spring 框架提供的用于获取注入配置属性值的注解,可用在类的成员变量、方法参数和构造函数参数上。
我们知道,在应用程序启动时,使用@Value注解修饰的Bean会被实例化并加入到 PropertySourcesPlaceholderConfigurer 的后置处理器集合中。当后置处理器开始执行时,它会读取Bean中所有被 @Value 注解所修饰的值,并通过反射将解析后的属性值,赋值给被@Value 注解修饰成员变量、方法参数和构造函数参数。
请注意,在使用 @Value 注解时,需要确保注入的属性值已被加载到 Spring 容器中,否则会导致注入失败。
快速使用
在项目的 src/main/resources 目录下新建 application.yml 配置文件,并往其中添加如下属性:
1 | quick: |
对应的测试代码如下,只需变量上加 @Value(“${quick.use}”)注解,那么@Value 注解便会自动将配置文件中的quick.use 属性值注入到 isQuickUsed 字段中:
1 | @Slf4j |
尽管@Value注解使用起来很方便,但是也存在一些需要注意的地方,下面介绍几个比较容易出错的地方。
缺失配置
如果开发者在代码中引用变量,但是在配置文件中为进行配置,此时就会出现如下的错误信息:
为了避免此类错误导致服务无法正常启动,我们可以在引用变量的同时,给它赋一个默认值,这样即使未在配置文件中赋值,程序也是可以正常启动的:
1 | @Slf4j |
静态变量赋值
请注意,将 @Value 注解添加到静态变量上,这样是无法获取静态变量的属性值。我们知道,静态变量属于类,不属于某个对象,而 Spring是基于对象的属性进行依赖注入,且类在应用启动时,静态变量就被初始化,此时 Bean还未被实例化,因此无法通过 @Value 注解来注入属性值:
1 | @Slf4j |
尽管 @Value 注解无法直接作用在静态变量上,但是开发者可通过获取已有 Bean实例化后的属性值,并将其赋值给静态变量进而实现给静态变量赋值这一目的。
上述过程对应的具体操作如下:
(1)通过 @Value 注解将属性值注入到普通 Bean中;
(2)获取该 Bean对应的属性值;
(3)将其赋值给静态变量;
(4)在静态变量中使用该属性值。
1 | @Slf4j |
当然了也可以在构造方法中设置isUsed变量的值:
1 | @Slf4j |
常量赋值
**请注意,将 @Value 注解添加到常量上,这样是无法获取常量的属性值。 **我们知道,被final修饰的变量在使用前必须赋值,且一旦赋值便不能修改。final修饰的变量可以在定义时、构造方法中或者静态代码中进行赋值,这里只讨论final变量在静态代码块中赋值的情况。前面说过, @Value 注解是在 Bean 实例化后才进行属性注入,因此无法在构造方法中初始化 final 变量。
1 | @Slf4j |
非Spring容器管理的Bean中使用
在Spring中,只有被 @Component、@Service、@Controller、@Repository 或 @Configuration 等注解标识的类,才会被Spring容器所管理,在这些类中使用 @Value注解才会生效。而对于普通的POJO类,无法使用 @Value注解进行属性注入:
1 | @Slf4j |
上面的代码就无法获取配置文件中 quick.use 配置项的值。
引入方式不对
当我们需要使用某个Spring容器管理的对象时,需要使用依赖注入的方式,不能通过new关键字来创建实例。
ConfigurationProperties注解
@ConfigurationProperties 注解是 SpringBoot 提供的一种更优雅的方式,来读取配置文件中的属性值。通过自动绑定和类型转换等机制,可将指定前缀的属性集合自动绑定到一个Bean对象上。
实现原理
前面在分析 SpringBoot 项目的启动流程中,我们发现这个 prepareEnvironment() 方法中调用了一个非常重要的方法 bindToSpringApplication(environment),该方法的作用是将配置文件中的属性值绑定到被 @ConfigurationProperties 注解标记的 Bean对象中。不过此时这些对象还没有被 Spring 容器管理,因此无法完成属性的自动注入。
那么问题来了,这些 Bean 对象是在何时被注册到 Spring 容器中呢?这就涉及到了 ConfigurationPropertiesBindingPostProcessor 类,它是 Bean 后置处理器,负责扫描容器中所有被 @ConfigurationProperties 注解所标记的 Bean对象。如果找到了,则使用 Binder 组件将外部属性的值绑定到它们身上,从而实现自动注入:
1 | protected void bindToSpringApplication(ConfigurableEnvironment environment) { |
- bindToSpringApplication方法,主要将属性值绑定到 Bean 对象中;
- ConfigurationPropertiesBindingPostProcessor类,主要负责在Spring容器启动时,将被注解标记的 Bean 对象注册到容器中,并完成后续的属性注入操作。
示例使用
第一步,在application.yml配置文件中新增如下配置信息:第二步,定义一个名为 UsePropertiesConfig 的类,该类用于承载所有前缀为 config.custom 的配置属性:1
2
3
4config:
custom:
prop1: prop1
prop2: prop2第三步,定义一个名为 UsePropertiesConfigTest 的类,该类会使用到前面的配置项,因此需要将UsePropertiesConfig类进行注入并使用:1
2
3
4
5
6
7@Data
@Configuration
@ConfigurationProperties(prefix = "config.custom")
public class UsePropertiesConfig {
private String prop1;
private String prop2;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14@Slf4j
@SpringBootTest
public class UsePropertiesConfigTest {
@Resource
private UsePropertiesConfig usePropertiesConfig;
@Test
public void test(){
log.info("prop1 is :{}", usePropertiesConfig.getProp1());
log.info("prop2 is :{}", usePropertiesConfig.getProp2());
}
}
//输出结果:prop1和prop2PropertySources 注解
一般来说,系统默认提供的名为 application.yml 或者 application.properties 配置文件能满足绝大多数业务场景,但是在某些场景下我们还是希望自定义配置文件名称及内容。请注意,与默认的配置文件所不同的是,用户自定义的配置文件无法被应用自动加载,需要开发者手动加载。实现原理
@PropertySources 注解的实现原理比较简单,如下所示:
(1)应用程序启动时,扫描所有被@PropertySources 注解修饰的类,并获取到注解中指定自定义配置文件的路径;
(2)将指定路径下的配置文件内容加载到 Environment 中,这样就可通过 @Value 注解或 Environment.getProperty() 方法来获取其中定义的属性值。示例使用
第一步,在 src/main/resources 目录下定义一个名为 customProperties.properties 的自定义配置类,里面的配置项如下:第二步,在需要使用自定义配置文件的类上添加 @PropertySources 注解,并在该注解中指定自定义配置文件的路径,多个路径使用逗号隔开。这里定义一个名为 CustomPropertiesConfig 的类,里面的代码如下:1
2custom.sex=male
custom.address=shanghai第三步,定义一个名为 UsePropertiesConfigTest 的类,该类会使用到前面的配置项,因此需要将CustomPropertiesConfig 类进行注入并使用:1
2
3
4
5
6
7
8
9
10
11@Data
@PropertySources({
@PropertySource(value = "classpath:customProperties.properties",encoding = "utf-8")
})
@Configuration
public class CustomPropertiesConfig {
@Value("${custom.sex}")
private String sex;
@Value("${custom.address}")
private String address;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14@Slf4j
@SpringBootTest
public class UsePropertiesConfigTest {
@Resource
private CustomPropertiesConfig customPropertiesConfig;
@Test
public void test(){
log.info("custom.sex is: {}", customPropertiesConfig.getSex());
log.info("custom.address is: {}", customPropertiesConfig.getAddress());
}
}
//输出结果:custom.sex is: male 和 custom.address is: shanghai支持YML格式
查看一下这个 @PropertySource 注解的源码,如下所示:可以看到这里的 factory 属性,默认使用的是 PropertySourceFactory 类,而这个类是一个接口,查看一下该接口的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {
String name() default "";
String[] value();
boolean ignoreResourceNotFound() default false;
String encoding() default "";
Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}里面只有一个 createPropertySource 方法,该方法会返回一个 PropertySource 对象,这个PropertySourceFactory 接口只有一个默认的实现类 DefaultPropertySourceFactory ,该实现类的源码如下所示:1
2
3public interface PropertySourceFactory {
PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException;
}也就是说它只能加载 .properties 结尾的配置文件,无法加载 yml 格式结尾的文件。如果我们需要加载 yml 格式的配置文件,那么需要自定义 PropertySourceFactory 接口实现类。1
2
3
4
5
6
7
8public class DefaultPropertySourceFactory implements PropertySourceFactory {
public DefaultPropertySourceFactory() {
}
public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
return name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource);
}
}
第一步,在 src/main/resources 目录下定义一个名为 customYaml.yml 的自定义配置类,里面的配置项如下:
1 | custom: |
第二步,定义一个名为 YamlPropertySourceFactory 的类,该类需要实现 PropertySourceFactory 接口并重写其中的 createPropertySource 方法:
1 | public class YamlPropertySourceFactory implements PropertySourceFactory { |
第三步,定义一个名为 UseYamlConfig 的配置类,并在该类上添加 @PropertySources 注解,并在该注解中指定自定义配置文件的路径,多个路径使用逗号隔开:
1 | @Data |
第四步,定义一个名为 UseYamlConfigTest 的类,该类会使用到前面的配置项,因此需要将UseYamlConfig 类进行注入并使用:
1 | @Slf4j |
YamlPropertiesFactoryBean 加载 YAML 文件
我们还可以使用 YamlPropertiesFactoryBean 这个类将 YAML 配置文件中的属性值注入到 Bean 中,具体操作如下:
第一步,定义一个名为 customBeanYaml.yml 的配置文件,里面的代码如下所示:
1 | custom: |
第二步,定义一个名为 CustomYamlPropertiesFactoryBeanConfig 的类,里面定义一个名为 yamlConfigurer 的方法,该方法需要返回一个 PropertySourcesPlaceholderConfigurer 对象:
1 | @Configuration |
第三步,定义一个测试方法,开发者可通过 @Value 注解或 Environment.getProperty() 方法来获取配置文件中定义的属性值:
1 | @SpringBootTest |
自定义读取
如果开发者觉得上述读取方式不够优雅,自己想造轮子,此时可以直接注入 PropertySources 对象,来获取所有属性的配置队列,之后就可以按照要求进行实现:
1 | @Slf4j |
案例使用
接下来通过一个例子来灵活学习如何获取指定配置文件中的属性值,步骤如下所示:
第一步,定义一个名为PropertiesLoader的类,我们定义 loadProperties 方法用于读取指定配置文件中的属性信息:
1 | public class PropertiesLoader { |
第二步,定义一个方法用于获取配置文件中指定名称的属性值:
1 | public class PropertiesTest { |
注意这个 user.properties 文件是定义在项目resources目录下的文件。
小结
通过上面的学习,我们知道可通过 @Value 注解、Environment 类、@ConfigurationProperties 注解和@PropertySource 注解等方式来获取配置信息。
其中,@Value 注解适用于单个值的注入,其他几种方式适用于多个配置的批量注入,而且不同方式在效率、灵活性、易用性等方面存在差异,在选择的时候需要多方面进行考虑。
这里笔者结合实际的工作体会,给出一些比较有参考意义的建议:如果重视代码的可读性和可维护性,可选择 @ConfigurationProperties 注解;如果更注重运行效率,可选择使用 Environment 类。