1. 概述

本文我们将演示如何在Spring及Spring Boot中,通过Java配置和@PropertySource注解的方式使用properties配置文件。

2. 通过注解注册 Property 文件

Spring 3.1 引入了 @PropertySource 注解,很方便将配置文件数据添加到 Spring Environment 中。

@PropertySource 可以结合 @Configuration 注解一起使用:

@Configuration
@PropertySource("classpath:foo.properties")
public class PropertiesWithJavaConfig {
    //...
}

我们也可以使用$占位符,在运行时确定文件路径

@PropertySource({ 
  "classpath:persistence-${envTarget:mysql}.properties"
})
...

2.1. 定义多个 Property 文件路径

Java 8 新特性 中支持重复注解。因此,在Java 8及以上版本中,我们可以重复使用 @PropertySource 注解来定义多个property路径。

@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {
    //...
}

当然如果是Java 8以前的版本,我们也可以使用@PropertySources 注解指定一个@PropertySource数组

@PropertySources({
    @PropertySource("classpath:foo.properties"),
    @PropertySource("classpath:bar.properties")
})
public class PropertiesWithJavaConfig {
    //...
}

另外值得一提的是,如果字段名冲突,则以最后读到的为准。

3. 使用/注入属性

使用 @Value 注解 注入属性很简单:

@Value( "${jdbc.url}" )
private String jdbcUrl;

同时可以指定一个缺省值:

@Value( "${jdbc.url:aDefaultUrl}" )
private String jdbcUrl;

Spring 3.1 新增 PropertySourcesPlaceholderConfigurer,负责解析 @Value 中的 ${..}占位符。

我们也可以使用Environment API获取属性值

@Autowired
private Environment env;
...
dataSource.setUrl(env.getProperty("jdbc.url"));

4. Spring Boot 中读取 Property 文件

在进入高级主题之前,我们先了解 Spring Boot 对 properties 文件的支持情况。

相较 Spring 而言,Spring Boot 涉及更少的配置,当然这也是其使命。

4.1. application.properties: 默认的 Property 文件

我们知道 Spring Boot 遵守“约定大于配置”的设计理念。它会自动检测 src/main/resources 路径下的application.properties文件作为系统默认的属性文件,然后我们就可自由的注入、读取这些属性字段。

使用默认配置文件,我们无需显式注册PropertySource指定文件路径。

我们也可以在程序运行时,指定文件路径。

java -jar app.jar --spring.config.location=classpath:/another-location.properties

Spring Boot 2.3 起, 还支持使用通配符定义配置文件路径

例如下面,Spring Boot会匹配jar包之外 config/*/ 目录下面的配置文件。 当我们需要加载多个properties文件时,这种方式很方便。

java -jar app.jar --spring.config.location=config/*/

2.4.0 版本起, Spring Boot 支持多文档配置文件, 类似于YAML的设计,使用---符号分隔分档:

baeldung.customProperty=defaultValue
#---
baeldung.customProperty=overriddenValue

注意 --- 前面有个 #前缀

4.2. 多环境配置

在实际开发过程中,我们通常会有多套环境,开发环境、测试环境、生产环境。那么在Spring Boot中如何根据特定的环境切换不同的配置文件呢?

我们可以在 src/main/resources 目录下创建名为application-environment.properties的配置文件,然后设置Spring profile为environment名

例如,我如果我们有一个 “staging” 环境,那我们需要设置profile为staging并创建application-staging.properties。

指定环境的配置文件会优先于默认的application.properties文件加载。注意默认配置依然会被加载,只是当属性冲突时,指定的配置文件会覆盖默认配置。

4.3. Test环境配置

在测试场景下,我们有时也有使用特定属性值的需求。

针对这种场景,Spring Boot在测试运行时会读取src/test/resources目录下的配置文件。同样,默认配置文件会被加载,冲突时会被覆盖。

4.4. @TestPropertySource 注解

我们可以使用@TestPropertySource注解,实现更细颗粒度的控制。

针对不同的测试用例,加载指定的配置文件,优先于默认属性:

@RunWith(SpringRunner.class)
@TestPropertySource("/foo.properties")
public class FilePropertyInjectionUnitTest {

    @Value("${foo}")
    private String foo;

    @Test
    public void whenFilePropertyProvided_thenProperlyInjected() {
        assertThat(foo).isEqualTo("bar");
    }
}

如果不想使用文件,我们还可以直接定义键值对:

@RunWith(SpringRunner.class)
@TestPropertySource(properties = {"foo=bar"})
public class PropertyInjectionUnitTest {

    @Value("${foo}")
    private String foo;

    @Test
    public void whenPropertyProvided_thenProperlyInjected() {
        assertThat(foo).isEqualTo("bar");
    }
}

也可以使用 @SpringBootTest 注解中的 properties 参数来实现类似的效果:

@RunWith(SpringRunner.class)
@SpringBootTest(
  properties = {"foo=bar"}, classes = SpringBootPropertiesTestApplication.class)
public class SpringBootPropertyInjectionIntegrationTest {

    @Value("${foo}")
    private String foo;

    @Test
    public void whenSpringBootPropertyProvided_thenProperlyInjected() {
        assertThat(foo).isEqualTo("bar");
    }
}

4.5. 使用ConfigurationProperties封装属性

如果我们有组合一起使用的属性,那么我们可以使用 @ConfigurationProperties 注解将这些属性按层次结构映射到Java Bean中。

例如,下面有一组关于数据库连接的配置:

database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar

然后我们定义一个Java类,使用 @ConfigurationProperties 注解将其映射到database字段上:

@ConfigurationProperties(prefix = "database")
public class Database {
    String url;
    String username;
    String password;

    // standard getters and setters
}

Spring Boot 会自动完成属性到Java对象字段之间的映射,这得益于其"约定优于配置" 的设计理念。我们只需要配置属性前缀(prefix)即可。

想进一步学习该注解的用法,请移步到这篇文章

4.6. Properties文件替代品: YAML 文件

Spring 同样支持 YAML 格式的配置文件。

YAML特别适合有层级关系的属性配置,可读性好层次感强

下面是properties后缀的配置文件:

database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar
secret: foo

用YAML重写后变为:

database:
  url: jdbc:postgresql:/localhost:5432/instance
  username: foo
  password: bar
secret: foo

值得一提的是,*@PropertySource* 注解不支持YAML文件。因此使用该注解时,只能选择properties文件。

Another remarkable point is that in version 2.4.0 Spring Boot changed the way in which properties are loaded from multi-document YAML files. Previously, the order in which they were added was based on the profile activation order. With the new version, however, the framework follows the same ordering rules that we indicated earlier for .properties files; properties declared lower in the file will simply override those higher up.

Additionally, in this version profiles can no longer be activated from profile-specific documents, making the outcome clearer and more predictable.

4.7. 导入扩展配置文件

Prior to version 2.4.0, Spring Boot allowed including additional configuration files using the spring.config.location and spring.config.additional-location properties, but they had certain limitations. For instance, they had to be defined before starting the application (as environment or system properties, or using command-line arguments) as they were used early in the process.

In the mentioned version, we can use the spring.config.import property within the application.properties or application.yml file to easily include additional files. This property supports some interesting features:

  • adding several files or directories
  • the files can be loaded either from the classpath or from an external directory
  • indicating if the startup process should fail if a file is not found, or if it's an optional file
  • importing extensionless files

Let's see a valid example:

spring.config.import=classpath:additional-application.properties,
  classpath:additional-application[.yml],
  optional:file:./external.properties,
  classpath:additional-application-properties/

Note: here we formatted this property using line breaks just for clarity.

Spring will treat imports as a new document inserted immediately below the import declaration.

4.8. 从命令行设置属性

除了使用配置文件外,我们也可通过命令行传递属性配置:

java -jar app.jar --property="value"

我们也可通过系统属性设置(在 -jar 命令前提供,而不是之后):

java -Dproperty.name="value" -jar app.jar

4.9. 从环境变量中设置

Spring Boot 还将检测环境变量,将它们视为属性:

export name=value
java -jar app.jar

4.10. 随机属性值

如果我们不想确定属性值,可以用RandomValuePropertySource生成随机数。

random.number=${random.int}
random.long=${random.long}
random.uuid=${random.uuid}

4.11. Additional Types of Property Sources

Spring Boot supports a multitude of property sources, implementing a well-thought-out ordering to allow sensible overriding. It's worth consulting the official documentation, which goes further than the scope of this article.

5. Configuration Using Raw Beans — the PropertySourcesPlaceholderConfigurer

Besides the convenient methods of getting properties into Spring, we can also define and regiter the property configuration bean manually.

Working with the PropertySourcesPlaceholderConfigurer gives us full control over the configuration, with the downside of being more verbose and most of the time, unnecessary.

Let's see how we can define this bean using Java configuration:

@Bean
public static PropertySourcesPlaceholderConfigurer properties(){
    PropertySourcesPlaceholderConfigurer pspc
      = new PropertySourcesPlaceholderConfigurer();
    Resource[] resources = new ClassPathResource[ ]
      { new ClassPathResource( "foo.properties" ) };
    pspc.setLocations( resources );
    pspc.setIgnoreUnresolvablePlaceholders( true );
    return pspc;
}

6. Properties in Parent-Child Contexts

This question comes up again and again: What happens when our web application has a parent and a child context? The parent context may have some common core functionality and beans, and then one (or multiple) child contexts, maybe containing servlet-specific beans.

In that case, what's the best way to define properties files and include them in these contexts? And how to best retrieve these properties from Spring?

We'll give a simple breakdown.

If the file is defined in the Parent context:

  • @Value works in Child context: YES

  • @Value works in Parent context: YES

  • environment.getProperty in Child context: YES

  • environment.getProperty in Parent context: YES

If the file is defined in the Child context:

  • @Value works in Child context: YES

  • @Value works in Parent context: NO

  • environment.getProperty in Child context: YES

  • environment.getProperty in Parent context: NO

7. 总结

本文我们展示了Spring中使属性配置的几种方式。

本文源码存储在GitHub上。