译:从 JSON 文件加载 Spring Boot 属性
Posted SpringForAll社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了译:从 JSON 文件加载 Spring Boot 属性相关的知识,希望对你有一定的参考价值。
原文链接:https://www.baeldung.com/spring-boot-json-properties
译者:Darren Luo
1. 简介
使用外部配置属性(properties)是一种常见的模式。
最常见的一个情况是我们的应用程序能在多种环境中(比如开发、测试和生产)改变行为,而不需要改变部署工件。
在本教程中,我们将关注 Spring Boot 应用程序如何从 JSON 文件中加载属性。
2. 在 Spring Boot 中加载属性
Spring 和 Spring Boot 对加载外部属性有极强的支持,你可以在本文中找到基础知识的精彩概述。
由于这种支持主要集中于 .properties 和 .yml 文件,使用 JSON 通常需要额外配置。
我们将假设基本功能是大家都知道的,并且在这里特别关注 JSON 方面。
3. 通过命令行加载属性
我们能在命令行中以三种预定义格式提供 JSON 数据。
首先,我们可以在 UNIX shell 中设置 SPRING_APPLIACATION_JSON 环境变量:
$ SPRING_APPLICATION_JSON='{"environment":{"name":"production"}}' java -jar app.jar
提供的数据将填充到 Spring Enviroment。再这个例子中,我们将获取一个带有“production”值的 environment.name 属性。
此外,我们可以加载我们的 JSON 作为系统属性,举一个例子:
$ java -Dspring.application.json='{"environment":{"name":"production"}}' -jar app.jar
最后选项是使用简单的命令行参数:
$ java -jar app.jar --spring.application.json='{"environment":{"name":"production"}}'
使用最后两种方法,spring.application.json 属性将使用给定数据填充为未解析的 String。
这些是加载 JSON 数据进我们应用程序的最简单的选项。这种简单的方法的缺点是缺乏可扩展性。
再命令行中加载大量数据可能很麻烦并且容易出错。
4. 通过 PropertySource 注解加载属性
Spring Boot 提供一个强大的生态系统,可通过注解创建配置类。
首先,我们使用一些简单成员定义一个配置类:
public class JsonProperties {
private int port;
private boolean resend;
private String host;
// getters and setters
}
我们可以在外部文件(我们将其命名为 configprops.json)中提供标准 JSON 个数的数据:
{
"host" : "mailer@mail.com",
"port" : 9090,
"resend" : true
}
现在我们必须见我们的 JSON 文件连接到配置类:
@Component
@PropertySource(value = "classpath:configprops.json")
@ConfigurationProperties
public class JsonProperties {
// same code as before
}
我们在类和 JSON 文件之间进行解耦。该连接基于字符串和变量名。因此,我们没有编译时检查,但是我们可以通过测试验证绑定。
因为字段应该由框架填充,我们需要使用集成测试。
使用简约设置,我们可以定义应用程序的主要入口:
@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("mailer@mail.com", 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
我们必须提供一个自定义的具有解析 JSON 数据能力的 PropertySourceFactory:
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
}
我们的测试将因此通过。此外,此属性源工厂也将适当的解析列表值。
所以,现在我们可以使用列表成员(以及相应的 getters 和 setters)扩展我们的配置类:
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。
所以我们可以使用 getters 和 setters 将 Map 成员添加到我们的 JsonProperties类:
private LinkedHashMap<String, ?> sender;
// getter and setter
在 JSON 文件中我们可以为此字段提供嵌套数据结构:
{
// same fields as before
"sender" : {
"name": "sender",
"address": "street"
}
}
现在我们可以通过 map 访问嵌套数据:
@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenNestedLoadedAsMap() {
assertEquals("sender", jsonProperties.getSender().get("name"));
assertEquals("street", jsonProperties.getSender().get("address"));
}
6. 使用自定义 ContextInitializer
如果我们需要更多的控制属性的加载,我们可以使用自定义 ContextInitializers。
这种手动方法更繁琐。但是,我们将因此完全控制加载和解析数据。
我们将使用和以前一样的 JSON 数据,但是我们将其加载到不同的配置类。
@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {
private String host;
private int port;
private boolean resend;
// getters and setters
}
注意,我们不再使用 PropertySource 注解。但是在 ConfigurationProperties 注解中,我们定义了一个前缀。
在下一节中,我们将研究如何将属性加载到‘customer’命名空间中。
6.1. 将属性加载到自定义命名空间
要为上面的属性类提供输入,我们将从 JSON 文件中加载数据并在之后解析,我们将使用 MapPropertySources 填充 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);
}
}
}
我们可以看到,它需要相当复杂的代码,但是这是灵活的代价。在上面的代码中,我们可以指定我们自己的解析器并决定如何处理每个条目。
在本演示中,我们只将属性放入自定义命名空间。
要使用此初始化器,我们必须将其连接到应用程序。对于生产用途,我们可以在 SpringApplicationBuilder 中添加它:
@EnableAutoConfiguration
@ComponentScan(basePackageClasses = { JsonProperties.class,
CustomJsonProperties.class })
public class ConfigPropertiesDemoApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class)
.initializers(new JsonPropertyContextInitializer())
.run();
}
}
另外需要注意,CustomJsonProperties 类已经被添加到 basePackageClasses中。
对于我们的测试环境,我们可以在 ContextConfiguration 注解中提供我们的自定义初始化器:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ConfigPropertiesDemoApplication.class,
initializers = JsonPropertyContextInitializer.class)
public class JsonPropertiesIntegrationTest {
// same code as before
}
在自动装配好我们的 CustomerJsonProperties 类之后,我们可以测试从自定义命名空间绑定数据:
@Test
public void whenLoadedIntoEnvironment_thenFlatValuesPopulated() {
assertEquals("mailer@mail.com", customJsonProperties.getHost());
assertEquals(9090, customJsonProperties.getPort());
assertTrue(customJsonProperties.isResend());
}
6.2. 扁平化嵌套结构
Spring 框架提供了一个强大的机制来绑定属性到对象成员。此功能的基础是属性中的名称前缀。
如果我们扩展我们自定义 ApplicationInitializer 来将 Map 的值转换为命名空间结构,然后框架可以直接加载我们的嵌套数据结构到想对应的对象中。
增强的 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 加载 JSON 数据。
虽然加载嵌套属性是可解决的,但是需要格外小心。
和往常一样,代码在Github上可见。
推荐:
上一篇:
关注公众号
以上是关于译:从 JSON 文件加载 Spring Boot 属性的主要内容,如果未能解决你的问题,请参考以下文章
在 Spring Boot jpa 中将延迟加载的对象转换为 JSON
如何从 Spring Boot 中的资源中读取 JSON 文件
Spring boot:从文件系统加载配置文件特定的 application.properties