1. 简介
使用外部配置属性是一种常见的做法。
最常见的需求之一是:在不同环境(如开发、测试、生产)中改变应用程序行为,而无需修改部署包。
在本教程中,我们将重点介绍 如何在 Spring Boot 应用中从 JSON 文件加载配置属性。
2. Spring Boot 中加载属性的方式
Spring 和 Spring Boot 对外部配置的支持非常强大——你可以参考 这篇文章 了解基础用法。
由于这些机制主要针对 .properties
和 .yml
文件,处理 JSON 通常需要额外配置。
我们假设你已经熟悉基本用法,这里专注于 JSON 的特殊处理方式。
3. 通过命令行加载属性
我们可以在命令行中通过三种预定义格式提供 JSON 数据。
首先,在 UNIX 环境中设置环境变量 SPRING_APPLICATION_JSON
:
$ SPRING_APPLICATION_JSON='{"environment":{"name":"production"}}' java -jar app.jar
提供的数据会被注入 Spring 的 Environment
中。以上例子中,会得到属性 environment.name
,其值为 "production"
。
其次,也可以将 JSON 作为系统属性加载,例如:
$ java -Dspring.application.json='{"environment":{"name":"production"}}' -jar app.jar
最后,还可以使用简单的命令行参数:
$ java -jar app.jar --spring.application.json='{"environment":{"name":"production"}}'
后两种方式中,spring.application.json
属性会被填充为未解析的字符串。
这是最简单的加载 JSON 数据的方法。但缺点也很明显:可扩展性差。
在命令行中加载大量数据不仅麻烦,而且容易出错。
4. 通过 @PropertySource 注解加载属性
Spring Boot 提供了强大的注解机制来创建配置类。
首先,定义一个配置类,包含一些简单字段:
public class JsonProperties {
private int port;
private boolean resend;
private String host;
// getters and setters
}
接着,准备一个 JSON 格式的外部文件(命名为 configprops.json
):
{
"host" : "[email protected]",
"port" : 9090,
"resend" : true
}
然后,使用 @PropertySource
注解将 JSON 文件绑定到配置类:
@Component
@PropertySource(value = "classpath:configprops.json")
@ConfigurationProperties
public class JsonProperties {
// same code as before
}
这种绑定是松耦合的,基于字符串和字段名匹配。因此没有编译期检查,但可以通过测试验证绑定是否正确。
由于字段需要框架自动填充,我们需要使用集成测试。
简单起见,定义应用主类:
@SpringBootApplication
@ComponentScan(basePackageClasses = { JsonProperties.class})
public class ConfigPropertiesDemoApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class).run();
}
}
编写集成测试:
@RunWith(SpringRunner.class)
@ContextConfiguration(
classes = ConfigPropertiesDemoApplication.class)
public class JsonPropertiesIntegrationTest {
@Autowired
private JsonProperties jsonProperties;
@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenLoadFlatValues() {
assertEquals("[email protected]", jsonProperties.getHost());
assertEquals(9090, jsonProperties.getPort());
assertTrue(jsonProperties.isResend());
}
}
运行测试会报错,甚至连 ApplicationContext
都加载失败,错误如下:
ConversionFailedException:
Failed to convert from type [java.lang.String]
to type [boolean] for value 'true,'
虽然 @PropertySource
成功连接了类和 JSON 文件,但 resend
的值被识别为 "true,"
(带逗号),无法转换为 boolean。
✅ 解决方案是注入 JSON 解析器。幸运的是,Spring Boot 内置了 Jackson,我们可以借助 PropertySourceFactory
来实现。
5. 使用 PropertySourceFactory 解析 JSON
我们需要自定义一个 PropertySourceFactory
,具备解析 JSON 的能力:
public class JsonPropertySourceFactory
implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(
String name, EncodedResource resource)
throws IOException {
Map readValue = new ObjectMapper()
.readValue(resource.getInputStream(), Map.class);
return new MapPropertySource("json-property", readValue);
}
}
然后在 @PropertySource
中指定该工厂类:
@Configuration
@PropertySource(
value = "classpath:configprops.json",
factory = JsonPropertySourceFactory.class)
@ConfigurationProperties
public class JsonProperties {
// same code as before
}
✅ 这样,测试就可以通过了。而且这个工厂类也能处理列表数据。
例如,我们可以在配置类中新增一个列表字段:
private List<String> topics;
// getter and setter
JSON 文件中提供数据:
{
// same fields as before
"topics" : ["spring", "boot"]
}
测试列表绑定:
@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenLoadListValues() {
assertThat(
jsonProperties.getTopics(),
Matchers.is(Arrays.asList("spring", "boot")));
}
5.1. 嵌套结构处理
处理嵌套 JSON 结构并不容易。Jackson 默认将嵌套结构映射为 Map
。
我们可以为 JsonProperties
类添加一个 Map
字段:
private LinkedHashMap<String, ?> sender;
// getter and setter
JSON 文件中提供嵌套数据:
{
// same fields as before
"sender" : {
"name": "sender",
"address": "street"
}
}
测试访问嵌套数据:
@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenNestedLoadedAsMap() {
assertEquals("sender", jsonProperties.getSender().get("name"));
assertEquals("street", jsonProperties.getSender().get("address"));
}
6. 使用自定义 ContextInitializer
如果你希望更精细地控制属性加载过程,可以使用自定义的 ContextInitializer
。
这种方式更繁琐,但控制力更强。
我们使用相同的 JSON 数据,但加载到另一个配置类中:
@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {
private String host;
private int port;
private boolean resend;
// getters and setters
}
⚠️ 注意:这次我们不再使用 @PropertySource
注解,而是通过 @ConfigurationProperties
指定前缀。
6.1. 加载属性到自定义命名空间
我们需要从 JSON 文件中读取数据,解析后填充到 Spring 的 Environment
中:
public class JsonPropertyContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static String CUSTOM_PREFIX = "custom.";
@Override
@SuppressWarnings("unchecked")
public void
initialize(ConfigurableApplicationContext configurableApplicationContext) {
try {
Resource resource = configurableApplicationContext
.getResource("classpath:configpropscustom.json");
Map readValue = new ObjectMapper()
.readValue(resource.getInputStream(), Map.class);
Set<Map.Entry> set = readValue.entrySet();
List<MapPropertySource> propertySources = set.stream()
.map(entry-> new MapPropertySource(
CUSTOM_PREFIX + entry.getKey(),
Collections.singletonMap(
CUSTOM_PREFIX + entry.getKey(), entry.getValue()
)))
.collect(Collectors.toList());
for (PropertySource propertySource : propertySources) {
configurableApplicationContext.getEnvironment()
.getPropertySources()
.addFirst(propertySource);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
代码虽然复杂,但换来的是完全的控制权。你可以自定义解析逻辑。
使用该初始化器:
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = { JsonProperties.class,
CustomJsonProperties.class })
public class ConfigPropertiesDemoApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class)
.initializers(new JsonPropertyContextInitializer())
.run();
}
}
测试中也可以指定初始化器:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ConfigPropertiesDemoApplication.class,
initializers = JsonPropertyContextInitializer.class)
public class JsonPropertiesIntegrationTest {
// same code as before
}
测试绑定结果:
@Test
public void whenLoadedIntoEnvironment_thenFlatValuesPopulated() {
assertEquals("[email protected]", customJsonProperties.getHost());
assertEquals(9090, customJsonProperties.getPort());
assertTrue(customJsonProperties.isResend());
}
6.2. 展平嵌套结构
Spring 提供了强大的属性绑定机制,核心是属性名前缀。
如果我们把嵌套结构转换为扁平的命名空间结构,Spring 就能直接绑定到对象中。
增强后的 CustomJsonProperties
类:
@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {
// same code as before
private Person sender;
public static class Person {
private String name;
private String address;
// getters and setters for Person class
}
// getters and setters for sender member
}
增强后的 ApplicationContextInitializer
:
public class JsonPropertyContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private final static String CUSTOM_PREFIX = "custom.";
@Override
@SuppressWarnings("unchecked")
public void
initialize(ConfigurableApplicationContext configurableApplicationContext) {
try {
Resource resource = configurableApplicationContext
.getResource("classpath:configpropscustom.json");
Map readValue = new ObjectMapper()
.readValue(resource.getInputStream(), Map.class);
Set<Map.Entry> set = readValue.entrySet();
List<MapPropertySource> propertySources = convertEntrySet(set, Optional.empty());
for (PropertySource propertySource : propertySources) {
configurableApplicationContext.getEnvironment()
.getPropertySources()
.addFirst(propertySource);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static List<MapPropertySource>
convertEntrySet(Set<Map.Entry> entrySet, Optional<String> parentKey) {
return entrySet.stream()
.map((Map.Entry e) -> convertToPropertySourceList(e, parentKey))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static List<MapPropertySource>
convertToPropertySourceList(Map.Entry e, Optional<String> parentKey) {
String key = parentKey.map(s -> s + ".")
.orElse("") + (String) e.getKey();
Object value = e.getValue();
return covertToPropertySourceList(key, value);
}
@SuppressWarnings("unchecked")
private static List<MapPropertySource>
covertToPropertySourceList(String key, Object value) {
if (value instanceof LinkedHashMap) {
LinkedHashMap map = (LinkedHashMap) value;
Set<Map.Entry> entrySet = map.entrySet();
return convertEntrySet(entrySet, Optional.ofNullable(key));
}
String finalKey = CUSTOM_PREFIX + key;
return Collections.singletonList(
new MapPropertySource(finalKey,
Collections.singletonMap(finalKey, value)));
}
}
✅ 最终,嵌套 JSON 数据会被正确绑定到配置对象中:
@Test
public void whenLoadedIntoEnvironment_thenValuesLoadedIntoClassObject() {
assertNotNull(customJsonProperties.getSender());
assertEquals("sender", customJsonProperties.getSender()
.getName());
assertEquals("street", customJsonProperties.getSender()
.getAddress());
}
7. 总结
Spring Boot 提供了通过命令行加载 JSON 配置的简单方式。如果需要更灵活的控制,可以通过自定义 PropertySourceFactory
或 ContextInitializer
来实现。
虽然嵌套属性的处理需要额外注意,但借助 Spring 的强大机制,完全是可以解决的。
代码示例可以在 GitHub 获取。