写在前面

本篇来学习使用SpringBoot进行日常开发过程中,经常使用到的6种读取配置文件内容的方式,掌握和熟练使用对于提升自我能力有极大帮助。使用的SpringBoot版本为 2.7.11 。

Environment

Environment简介

Environment 是 SpringBoot 的核心环境配置接口,它提供了很多方法用于访问应用程序属性,包括系统属性、操作系统环境变量、命令行参数和应用程序配置文件中定义的属性等。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();

String[] getDefaultProfiles();

/** @deprecated */
@Deprecated
boolean acceptsProfiles(String... profiles);

boolean acceptsProfiles(Profiles profiles);
}

public interface PropertyResolver {
boolean containsProperty(String key);

@Nullable
String getProperty(String key);

String getProperty(String key, String defaultValue);

@Nullable
<T> T getProperty(String key, Class<T> targetType);

<T> T getProperty(String key, Class<T> targetType, T defaultValue);

String getRequiredProperty(String key) throws IllegalStateException;

<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

String resolvePlaceholders(String text);

String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

可以看到这个接口继承自 PropertyResolver 接口,PropertyResolver接口中定义了很多获取属性的方法,因此Environment这个接口才具备上述能力。

初始化配置

接下来我们通过分析SpringBoot项目在启动时,对配置进行初始化这一过程来深度了解Environment接口的作用。

我们知道SpringBoot项目在启动时会调用 SpringApplication.run() 方法,而这个方法内部会调用prepareEnvironment()方法:
image.png
查看一下这个prepareEnvironment()方法的源码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
/**
* 1、创建ConfigurableEnvironment对象:调用getOrCreateEnvironment()方法获取或创建ConfigurableEnvironment对象,
* 该对象用于存储环境参数。如果已经存在ConfigurableEnvironment对象,则直接使用它;否则根据用户的配置和默认配置创建一个新的。
*/
ConfigurableEnvironment environment = this.getOrCreateEnvironment();

/**
* 2、解析并加载用户指定的配置文件,将其作为 PropertySource 添加到环境对象中。该方法默认会解析application.properties和application.yml文件,并将其添加到ConfigurableEnvironment对象中。
* PropertySource或PropertySourcesPlaceholderConfigurer加载应用程序的定制化配置。
*/
this.configureEnvironment(environment, applicationArguments.getSourceArgs());

// 3、加载所有的系统属性,并将它们添加到ConfigurableEnvironment对象中
ConfigurationPropertySources.attach(environment);

// 4、通知监听器环境参数已经准备就绪
listeners.environmentPrepared(bootstrapContext, environment);

/**
* 5、将默认属性源中的所有属性值移到环境对象的队列末尾,这样用户自定义的属性值就可以覆盖默认的属性值。
* 还可以避免用户无意中覆盖了SpringBoot提供的默认属性
*/
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");

// 6、将SpringBoot应用程序的属性绑定到环境对象上,以便能正确地读取和使用这些配置属性
this.bindToSpringApplication(environment);

// 7、如果没有自定义的环境类型,则使用EnvironmentConverter类型将环境对象转换为标准的环境类型,并添加到ConfigurableEnvironment对象中
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(this.getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, this.deduceEnvironmentClass());
}

// 8、再次加载系统配置,以防止被其他配置覆盖
ConfigurationPropertySources.attach(environment);
return environment;
}

简单解释一下上述方法的流程:

  • 创建 ConfigurableEnvironment 环境对象 ,用于存储环境参数;
  • 调用 configureEnvironment 方法加载默认的 application.properties 和 application.yml 配置文件,以及用户指定的配置文件,并将其封装为 PropertySource 对象添加到环境对象中;
  • ConfigurationPropertySources.attach 方法,用于加载所有的系统属性,并将它们添加到环境对象中;
  • listeners.environmentPrepared 方法,用于发送环境参数配置已经准备就绪的监听通知;
  • DefaultPropertiesPropertySource.moveToEnd 方法,用于将系统默认属性源中的所有属性值移到环境对象的队列末尾,这样用户自定义的属性值就能覆盖默认的属性值,还可以避免用户无意中覆盖了SpringBoot提供的默认属性;
  • bindToSpringApplication方法,用于将应用程序的属性绑定到Bean对象上;
  • ConfigurationPropertySources.attach方法,用于再次加载系统配置,防止被其他配置覆盖。

下面是对于上述几个比较重要的方法进行详细介绍,如下所示:
(1)getOrCreateEnvironment方法的源码如下所示,可以看到该方法会返回一个ConfigurableEnvironment对象,该对象用于存储环境参数。如果已存在ConfigurableEnvironment对象,则直接使用它;否则根据用户的配置和默认配置创建一个新的ConfigurableEnvironment对象:

1
2
3
4
5
6
7
8
9
10
11
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
} else {
ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(this.webApplicationType);
if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.webApplicationType);
}
return (ConfigurableEnvironment)(environment != null ? environment : new ApplicationEnvironment());
}
}

(2)configureEnvironment方法的源码如下所示,可以看到该方法会解析并加载用户指定的配置文件,并将其作为 PropertySource对象添加到环境对象中。configureEnvironment方法默认会解析application.propertiesapplication.yml文件,并将其添加到ConfigurableEnvironment对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
environment.setConversionService(new ApplicationConversionService());
}
this.configurePropertySources(environment, args);
this.configureProfiles(environment, args);
}

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
MutablePropertySources sources = environment.getPropertySources();
if (!CollectionUtils.isEmpty(this.defaultProperties)) {
DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
}

if (this.addCommandLineProperties && args.length > 0) {
String name = "commandLineArgs";
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
} else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
}

(3)DefaultPropertiesPropertySource.moveToEnd方法的源码如下所示,该方法会将默认属性源中的所有属性值移到环境对象的队列末尾,这样用户自定义的属性值就可以覆盖默认的属性值,还可以避免用户无意中覆盖了SpringBoot提供的默认属性:

1
2
3
4
5
6
7
8
9
10
public static void moveToEnd(ConfigurableEnvironment environment) {
moveToEnd(environment.getPropertySources());
}

public static void moveToEnd(MutablePropertySources propertySources) {
PropertySource<?> propertySource = propertySources.remove("defaultProperties");
if (propertySource != null) {
propertySources.addLast(propertySource);
}
}

通过前面的分析,我们知道各种配置属性最终都会被封装为一个个PropertySource对象,查看一下该对象的源码:

1
2
3
4
5
6
7
8
public abstract class PropertySource<T> {
protected final String name; // 属性名称
protected final T source; // 属性值(一个泛型,比如Map,Property)
public String getName(); // 获取属性的名字
public T getSource(); // 获取属性值
public boolean containsProperty(String name); //是否包含某个属性
public abstract Object getProperty(String name); //根据属性名获取到对应属性
}

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 会加载配置,这样程序在运行时就可以随时获取到配置信息:
image.png
image.png

读取配置

前面我们已经学习了加载配置的整个流程,那么读取配置就是从维护的 PropertySource 队列中根据name获取对应的source对象了。

一般而言,我们会使用Environment接口对象提供的方法来获取配置信息,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@SpringBootTest
public class EnvironmentTest {
@Resource
private Environment environment;

@Test
public void test(){
String username = environment.getProperty("user.name");
log.info("当前用户名为{}", username);
}
}

实际上在前面阅读源码的时候,我们知道这个Environment接口继承PropertyResolver接口,PropertyResolver是获取配置的关键接口,其内部提供了操作PropertySource 队列的方法,查看一下这个接口的继承依赖关系:

因此,我们也可以直接使用PropertyResolver接口中的方法来获取对应的属性信息:

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@SpringBootTest
public class EnvironmentTest {
@Resource
private PropertyResolver propertyResolver;

@Test
public void test(){
String username = propertyResolver.getProperty("user.name");
log.info("当前用户名为{}", username);
}
}

Value注解

@Value注解,是 Spring 框架提供的用于获取注入配置属性值的注解,可用在类的成员变量、方法参数和构造函数参数上。

我们知道,在应用程序启动时,使用@Value注解修饰的Bean会被实例化并加入到 PropertySourcesPlaceholderConfigurer 的后置处理器集合中。当后置处理器开始执行时,它会读取Bean中所有被 @Value 注解所修饰的值,并通过反射将解析后的属性值,赋值给被@Value 注解修饰成员变量、方法参数和构造函数参数。
请注意,在使用 @Value 注解时,需要确保注入的属性值已被加载到 Spring 容器中,否则会导致注入失败。

快速使用

在项目的 src/main/resources 目录下新建 application.yml 配置文件,并往其中添加如下属性:

1
2
quick:
use: true

对应的测试代码如下,只需变量上加 @Value(“${quick.use}”)注解,那么@Value 注解便会自动将配置文件中的quick.use 属性值注入到 isQuickUsed 字段中:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@SpringBootTest
public class ValueTest {
@Value("${quick.use}")
private boolean isQuickUsed;

@Test
public void test(){
log.info("isQuickUsed:{}", isQuickUsed);
}
}

尽管@Value注解使用起来很方便,但是也存在一些需要注意的地方,下面介绍几个比较容易出错的地方。

缺失配置

如果开发者在代码中引用变量,但是在配置文件中为进行配置,此时就会出现如下的错误信息:
image.png
为了避免此类错误导致服务无法正常启动,我们可以在引用变量的同时,给它赋一个默认值,这样即使未在配置文件中赋值,程序也是可以正常启动的:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@SpringBootTest
public class ValueTest {
@Value("${quick.use:true}")
private boolean isQuickUsed;

@Test
public void test(){
log.info("isQuickUsed:{}", isQuickUsed);
}
}

静态变量赋值

请注意,将 @Value 注解添加到静态变量上,这样是无法获取静态变量的属性值。我们知道,静态变量属于类,不属于某个对象,而 Spring是基于对象的属性进行依赖注入,且类在应用启动时,静态变量就被初始化,此时 Bean还未被实例化,因此无法通过 @Value 注解来注入属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@SpringBootTest
public class StaticVarTest {
@Value("${quick.use}")
private static String isQuickUsed;

@Test
public void test(){
log.info("isQuickUsed:{}", isQuickUsed);
}
}

//输出结果为:null

尽管 @Value 注解无法直接作用在静态变量上,但是开发者可通过获取已有 Bean实例化后的属性值,并将其赋值给静态变量进而实现给静态变量赋值这一目的。

上述过程对应的具体操作如下:
(1)通过 @Value 注解将属性值注入到普通 Bean中;
(2)获取该 Bean对应的属性值;
(3)将其赋值给静态变量;
(4)在静态变量中使用该属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@SpringBootTest
public class StaticVarTest {
private static String isUsed;

@Value("${quick.use}")
private void setIsQuickUsed(String used){
isUsed = used;
}

private String getIsQuickUsed(){
return isUsed;
}

@Test
public void test(){
System.out.println(getIsQuickUsed());
}
}

//输出结果为:true

当然了也可以在构造方法中设置isUsed变量的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@SpringBootTest
public class StaticVarTest {
private static String isUsed;

StaticVarTest(@Value("${quick.use}") String used){
isUsed = used;
}

private String getIsQuickUsed(){
return isUsed;
}

@Test
public void test(){
System.out.println(getIsQuickUsed());
}
}

常量赋值

**请注意,将 @Value 注解添加到常量上,这样是无法获取常量的属性值。 **我们知道,被final修饰的变量在使用前必须赋值,且一旦赋值便不能修改。final修饰的变量可以在定义时、构造方法中或者静态代码中进行赋值,这里只讨论final变量在静态代码块中赋值的情况。前面说过, @Value 注解是在 Bean 实例化后才进行属性注入,因此无法在构造方法中初始化 final 变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@SpringBootTest
public class FinalVarTest {
private final String isUsed;

FinalVarTest(@Value("${quick.use}") String used){
isUsed = used;
}

@Test
public void test(){
log.info("isUsed的值为:{}", isUsed);
}
}

//输出结果为:true

非Spring容器管理的Bean中使用

在Spring中,只有被 @Component、@Service、@Controller、@Repository 或 @Configuration 等注解标识的类,才会被Spring容器所管理,在这些类中使用 @Value注解才会生效。而对于普通的POJO类,无法使用 @Value注解进行属性注入:

1
2
3
4
5
6
7
8
9
10
@Slf4j
public class NoBeanTest {
@Value("${quick.use}")
private boolean isQuickUsed;

@Test
public void test(){
log.info("isQuickUsed:{}", isQuickUsed);
}
}

上面的代码就无法获取配置文件中 quick.use 配置项的值。

引入方式不对

当我们需要使用某个Spring容器管理的对象时,需要使用依赖注入的方式,不能通过new关键字来创建实例。

ConfigurationProperties注解

@ConfigurationProperties 注解是 SpringBoot 提供的一种更优雅的方式,来读取配置文件中的属性值。通过自动绑定和类型转换等机制,可将指定前缀的属性集合自动绑定到一个Bean对象上。

实现原理

前面在分析 SpringBoot 项目的启动流程中,我们发现这个 prepareEnvironment() 方法中调用了一个非常重要的方法 bindToSpringApplication(environment),该方法的作用是将配置文件中的属性值绑定到被 @ConfigurationProperties 注解标记的 Bean对象中。不过此时这些对象还没有被 Spring 容器管理,因此无法完成属性的自动注入。

那么问题来了,这些 Bean 对象是在何时被注册到 Spring 容器中呢?这就涉及到了 ConfigurationPropertiesBindingPostProcessor 类,它是 Bean 后置处理器,负责扫描容器中所有被 @ConfigurationProperties 注解所标记的 Bean对象。如果找到了,则使用 Binder 组件将外部属性的值绑定到它们身上,从而实现自动注入:
image.png

1
2
3
4
5
6
7
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
} catch (Exception var3) {
throw new IllegalStateException("Cannot bind to SpringApplication", var3);
}
}

  • bindToSpringApplication方法,主要将属性值绑定到 Bean 对象中;
  • ConfigurationPropertiesBindingPostProcessor类,主要负责在Spring容器启动时,将被注解标记的 Bean 对象注册到容器中,并完成后续的属性注入操作。

    示例使用

    第一步,在application.yml配置文件中新增如下配置信息:
    1
    2
    3
    4
    config:
    custom:
    prop1: prop1
    prop2: prop2
    第二步,定义一个名为 UsePropertiesConfig 的类,该类用于承载所有前缀为 config.custom 的配置属性:
    1
    2
    3
    4
    5
    6
    7
    @Data
    @Configuration
    @ConfigurationProperties(prefix = "config.custom")
    public class UsePropertiesConfig {
    private String prop1;
    private String prop2;
    }
    第三步,定义一个名为 UsePropertiesConfigTest 的类,该类会使用到前面的配置项,因此需要将UsePropertiesConfig类进行注入并使用:
    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和prop2

    PropertySources 注解

    一般来说,系统默认提供的名为 application.yml 或者 application.properties 配置文件能满足绝大多数业务场景,但是在某些场景下我们还是希望自定义配置文件名称及内容。请注意,与默认的配置文件所不同的是,用户自定义的配置文件无法被应用自动加载,需要开发者手动加载。

    实现原理

    @PropertySources 注解的实现原理比较简单,如下所示:
    (1)应用程序启动时,扫描所有被@PropertySources 注解修饰的类,并获取到注解中指定自定义配置文件的路径;
    (2)将指定路径下的配置文件内容加载到 Environment 中,这样就可通过 @Value 注解或 Environment.getProperty() 方法来获取其中定义的属性值。

    示例使用

    第一步,在 src/main/resources 目录下定义一个名为 customProperties.properties 的自定义配置类,里面的配置项如下:
    1
    2
    custom.sex=male
    custom.address=shanghai
    第二步,在需要使用自定义配置文件的类上添加 @PropertySources 注解,并在该注解中指定自定义配置文件的路径,多个路径使用逗号隔开。这里定义一个名为 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;
    }
    第三步,定义一个名为 UsePropertiesConfigTest 的类,该类会使用到前面的配置项,因此需要将CustomPropertiesConfig 类进行注入并使用:
    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 注解的源码,如下所示:
    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;
    }
    可以看到这里的 factory 属性,默认使用的是 PropertySourceFactory 类,而这个类是一个接口,查看一下该接口的源码:
    1
    2
    3
    public interface PropertySourceFactory {
    PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException;
    }
    里面只有一个 createPropertySource 方法,该方法会返回一个 PropertySource 对象,这个PropertySourceFactory 接口只有一个默认的实现类 DefaultPropertySourceFactory ,该实现类的源码如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    public 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);
    }
    }
    也就是说它只能加载 .properties 结尾的配置文件,无法加载 yml 格式结尾的文件。如果我们需要加载 yml 格式的配置文件,那么需要自定义 PropertySourceFactory 接口实现类。

第一步,在 src/main/resources 目录下定义一个名为 customYaml.yml 的自定义配置类,里面的配置项如下:

1
2
3
custom:
sex: male
city: shanghai

第二步,定义一个名为 YamlPropertySourceFactory 的类,该类需要实现 PropertySourceFactory 接口并重写其中的 createPropertySource 方法:

1
2
3
4
5
6
7
8
9
10
public class YamlPropertySourceFactory  implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
factoryBean.setResources(resource.getResource());

Properties properties = factoryBean.getObject();
return new PropertiesPropertySource(resource.getResource().getFilename(), properties);
}
}

第三步,定义一个名为 UseYamlConfig 的配置类,并在该类上添加 @PropertySources 注解,并在该注解中指定自定义配置文件的路径,多个路径使用逗号隔开:

1
2
3
4
5
6
7
8
9
10
11
@Data
@PropertySources({
@PropertySource(value = "classpath:customYaml.yml", encoding = "utf-8", factory = YamlPropertySourceFactory.class)
})
@Configuration
public class UseYamlConfig {
@Value("${custom.sex}")
private String sex;
@Value("${custom.city}")
private String city;
}

第四步,定义一个名为 UseYamlConfigTest 的类,该类会使用到前面的配置项,因此需要将UseYamlConfig 类进行注入并使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
@SpringBootTest
public class UseYamlConfigTest {
@Resource
private UseYamlConfig useYamlConfig;

@Test
public void test(){
log.info("custom.sex is: {}", useYamlConfig.getSex());
log.info("custom.city is: {}", useYamlConfig.getCity());
}
}

//输出结果:custom.sex is: male 和 custom.address is: shanghai

YamlPropertiesFactoryBean 加载 YAML 文件

我们还可以使用 YamlPropertiesFactoryBean 这个类将 YAML 配置文件中的属性值注入到 Bean 中,具体操作如下:
第一步,定义一个名为 customBeanYaml.yml 的配置文件,里面的代码如下所示:

1
2
custom:
email: qq@qq.com

第二步,定义一个名为 CustomYamlPropertiesFactoryBeanConfig 的类,里面定义一个名为 yamlConfigurer 的方法,该方法需要返回一个 PropertySourcesPlaceholderConfigurer 对象:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class CustomYamlPropertiesFactoryBeanConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer yamlConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ClassPathResource("customBeanYaml.yml"));
configurer.setProperties(Objects.requireNonNull(yaml.getObject()));
return configurer;
}
}

第三步,定义一个测试方法,开发者可通过 @Value 注解或 Environment.getProperty() 方法来获取配置文件中定义的属性值:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
@Slf4j
public class YamlPropertiesFactoryBeanTest {
@Value("${custom.email}")
private String email;

@Test
public void test(){
log.info("custom.email is: {}", email);
}
}

自定义读取

如果开发者觉得上述读取方式不够优雅,自己想造轮子,此时可以直接注入 PropertySources 对象,来获取所有属性的配置队列,之后就可以按照要求进行实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@SpringBootTest
public class CustomTest {
@Autowired
private PropertySources propertySources;

@Test
public void customTest() {
for (PropertySource<?> propertySource : propertySources) {
log.info("自定义方式,来实现配置获取逻辑,属性名称为:{} ,属性源为:{}", propertySource.getName(), propertySource.getSource());
}
}
}

案例使用

接下来通过一个例子来灵活学习如何获取指定配置文件中的属性值,步骤如下所示:
第一步,定义一个名为PropertiesLoader的类,我们定义 loadProperties 方法用于读取指定配置文件中的属性信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PropertiesLoader {
private static final Logger logger = Logger.getLogger(PropertiesLoader.class.getName());

private PropertiesLoader() {}

public static Properties loadProperties(String location) {
Properties props = new Properties();
logger.info("Loading properties file from path:" + location);
InputStreamReader in = null;
try {
in = new InputStreamReader(PropertiesLoader.class.getClassLoader().getResourceAsStream(location), "UTF-8");
props.load(in);
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
if(in != null) {
try {
in.close();
} catch (IOException e) {
logger.log(Level.WARNING, "error close inputstream", e);
}
}
}
return props;
}
}


第二步,定义一个方法用于获取配置文件中指定名称的属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PropertiesTest {
public static void init(String location){
Properties properties = PropertiesLoader.loadProperties(location);
String userName = properties.getProperty("user.name");
String userSex = properties.getProperty("user.sex");
System.out.println(userName);
System.out.println(userSex);
}

public static void main(String[] args) {
String location = "user.properties";
init(location);
}
}

注意这个 user.properties 文件是定义在项目resources目录下的文件。

小结

通过上面的学习,我们知道可通过 @Value 注解、Environment 类、@ConfigurationProperties 注解和@PropertySource 注解等方式来获取配置信息。
其中,@Value 注解适用于单个值的注入,其他几种方式适用于多个配置的批量注入,而且不同方式在效率、灵活性、易用性等方面存在差异,在选择的时候需要多方面进行考虑。

这里笔者结合实际的工作体会,给出一些比较有参考意义的建议:如果重视代码的可读性和可维护性,可选择 @ConfigurationProperties 注解;如果更注重运行效率,可选择使用 Environment 类。