Eugen Paraschiv 2013-10-15
1. 概述
本教程将展示如何在 Spring 中通过 Java 配置和 @PropertySource 注解来设置和使用属性(properties)。
我们还将了解 Spring Boot 中属性的工作方式。
2. 通过注解注册属性文件
Spring 3.1 引入了新的 @PropertySource 注解,作为向环境(Environment)中添加属性源的便捷机制。
我们可以将此注解与 @Configuration 注解结合使用:
@Configuration
@PropertySource("classpath:foo.properties")
public class PropertiesWithJavaConfig {
//...
}
另一种非常有用的方式是使用占位符,这样可以在运行时动态选择正确的属性文件:
@PropertySource({
"classpath:persistence-${envTarget:mysql}.properties"
})
...
2.1 定义多个属性文件位置
根据 Java 8 的规范,@PropertySource 注解是可重复的(repeatable)。因此,如果我们使用的是 Java 8 或更高版本,可以直接多次使用该注解来定义多个属性文件路径:
@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {
//...
}
当然,我们也可以使用 @PropertySources 注解并指定一个 @PropertySource 数组。这种方式适用于所有受支持的 Java 版本,而不仅限于 Java 8+:
@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 能够解析 bean 定义属性值和 @Value 注解中的 ${...} 占位符。
此外,我们也可以通过 Environment API 获取属性值:
@Autowired
private Environment env;
...
dataSource.setUrl(env.getProperty("jdbc.url"));
4. Spring Boot 中的属性配置
在深入更高级的属性配置选项之前,我们先来看看 Spring Boot 对属性的新支持。
总体而言,这种新支持相比标准 Spring 需要的配置更少,这当然也是 Spring Boot 的主要目标之一。
4.1 application.properties:默认属性文件
Boot 对属性文件采用了“约定优于配置”的方式。这意味着我们只需在 src/main/resources 目录下放置一个 application.properties 文件,它就会被自动检测到。之后,我们可以像平常一样注入其中加载的任意属性。
因此,使用这个默认文件时,无需显式注册 PropertySource,也无需提供属性文件路径。
如果需要在运行时指定其他文件,可以使用环境属性:
java -jar app.jar --spring.config.location=classpath:/another-location.properties
从 Spring Boot 2.3 开始,我们还可以为配置文件指定通配符路径。
例如,可以将 spring.config.location 设置为 config/*/:
java -jar app.jar --spring.config.location=config/*/
这样,Spring Boot 将在 JAR 文件外部查找符合 config/*/ 目录模式的配置文件。这在存在多个配置来源时非常有用。
自 2.4.0 版本起,Spring Boot 支持多文档属性文件(multi-document properties files),类似于 YAML 文件的原生支持:
baeldung.customProperty=defaultValue
#---
baeldung.customProperty=overriddenValue
注意:对于 .properties 文件,三连横线(---)前必须加上注释符号 #。
4.2 环境特定的属性文件
如果我们需要针对不同环境进行配置,Boot 内置了相应的机制。
我们只需在 src/main/resources 目录中定义一个名为 application-{environment}.properties 的文件,并设置一个同名的 Spring Profile。
例如,若要定义“staging”环境,则需激活名为 staging 的 profile,并创建 application-staging.properties 文件。
该环境文件会被加载,并优先于默认属性文件。注意,默认文件仍会被加载,只是在属性冲突时,环境特定文件的值会覆盖默认值。
4.3 测试专用的属性文件
在应用程序进行测试时,我们可能需要使用不同的属性值。
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 层次化属性(Hierarchical Properties)
如果属性是分组的,我们可以使用 @ConfigurationProperties 注解,将这些层次化的属性映射为 Java 对象图。
例如,用于配置数据库连接的属性:
database.url=jdbc:postgresql://localhost:5432/instance
database.username=foo
database.password=bar
然后使用注解将其映射到一个 Java 对象:
@ConfigurationProperties(prefix = "database")
public class Database {
String url;
String username;
String password;
// 标准的 getter 和 setter
}
Spring Boot 再次应用“约定优于配置”原则,自动将属性名映射到对应的字段。我们只需提供属性前缀即可。
4.6 替代方案:YAML 文件
Spring 也支持 YAML 文件。
所有关于测试专用、环境专用和默认属性文件的命名规则同样适用,唯一的区别是文件扩展名以及需要在类路径中包含 SnakeYAML 库。
YAML 特别适合存储层次化属性。以下属性文件:
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
值得注意的是:YAML 文件不支持 @PropertySource 注解。因此,如果需要使用该注解,则必须使用 .properties 文件。
另外,从 Spring Boot 2.4.0 开始,框架改变了从多文档 YAML 文件加载属性的方式。此前,属性加载顺序依赖于 profile 的激活顺序;而在新版本中,框架遵循与 .properties 文件相同的规则——文件中靠后的属性会覆盖靠前的同名属性。
此外,从该版本开始,不能再从 profile-specific 文档中激活 profile,从而使行为更加清晰和可预测。
4.7 导入额外的配置文件
在 2.4.0 版本之前,Spring Boot 允许通过 spring.config.location 和 spring.config.additional-location 属性引入额外的配置文件,但存在一些限制。例如,这些属性必须在应用启动前通过环境变量、系统属性或命令行参数定义,因为它们在启动早期就被使用。
从 2.4.0 开始,我们可以在 application.properties 或 application.yml 文件中使用 spring.config.import 属性轻松引入其他文件。该属性支持以下特性:
- 添加多个文件或目录
- 文件可来自 classpath 或外部目录
- 可指定文件缺失时是否导致启动失败(即是否为可选文件)
- 支持无扩展名的文件
示例:
spring.config.import=classpath:additional-application.properties,\
classpath:additional-application[.yml],\
optional:file:./external.properties,\
classpath:additional-application-properties/
注:此处为了可读性使用了换行和续行符
\。
Spring 会将导入的内容视为紧随 import 声明之后插入的新文档。
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 其他类型的属性源
Spring Boot 支持多种属性源,并实现了精心设计的优先级顺序,以支持合理的覆盖机制。建议查阅官方文档,其内容超出了本文范围。
5. 使用原始 Bean 配置 —— PropertySourcesPlaceholderConfigurer
除了上述便捷方式,我们也可以手动定义并注册属性配置 Bean。
使用 PropertySourcesPlaceholderConfigurer 能让我们完全控制配置过程,但缺点是代码更冗长,且大多数情况下并不必要。
以下是使用 Java 配置定义该 Bean 的示例:
@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. 父子上下文中的属性
这个问题反复出现:当我们的 Web 应用具有父上下文和子上下文时(例如父上下文包含通用核心功能,子上下文包含 Servlet 特定的 Bean),应该如何定义和加载属性文件?又该如何从 Spring 中获取这些属性?
以下是简明总结:
如果属性文件定义在父上下文中:
@Value在子上下文中:✅ 可用@Value在父上下文中:✅ 可用environment.getProperty()在子上下文中:✅ 可用environment.getProperty()在父上下文中:✅ 可用
如果属性文件定义在子上下文中:
@Value在子上下文中:✅ 可用@Value在父上下文中:❌ 不可用environment.getProperty()在子上下文中:✅ 可用environment.getProperty()在父上下文中:❌ 不可用
7. 结论
本文展示了在 Spring 中使用属性和属性文件的多种示例,涵盖了从基础配置到 Spring Boot 高级特性的内容。