1. Introduction
1.介绍
Use external configuration properties is quite a common pattern.
使用外部配置属性是一个相当常见的模式。
And, one of the most common questions is the ability to change the behavior of our application in multiple environments – such as development, test, and production – without having to change the deployment artifact.
而且,最常见的问题之一是能够改变我们的应用程序在多个环境中的行为–如开发、测试和生产–而不必改变部署工件。
In this tutorial, we’ll focus on how you can load properties from JSON files in a Spring Boot application.
在本教程中,我们将重点讨论如何在Spring Boot应用程序中从JSON文件加载属性。
2. Loading Properties in Spring Boot
2.在Spring Boot中加载属性
Spring and Spring Boot have strong support for loading external configurations – you can find a great overview of the basics in this article.
Spring和Spring Boot对加载外部配置有强大的支持 – 您可以在这篇文章中找到关于基础知识的精彩概述。
Since this support mainly focuses on .properties and .yml files – working with JSON typically needs extra configuration.
由于这种支持主要集中在.properties和.yml文件上 – 使用JSON工作通常需要额外的配置。
We’ll assume that the basic features are well known – and will focus on JSON specific aspects, here.
我们将假设基本功能是众所周知的–并将在此集中讨论JSON的具体方面。
3. Load Properties via Command Line
3.通过命令行加载属性
We can provide JSON data in the command line in three predefined formats.
我们可以在命令行中以三种预定义的格式提供JSON数据。
First, we can set the environment variable SPRING_APPLICATION_JSON in a UNIX shell:
首先,我们可以在UNIX shell中设置环境变量SPRING_APPLICATION_JSON。
$ SPRING_APPLICATION_JSON='{"environment":{"name":"production"}}' java -jar app.jar
The provided data will be populated into the Spring Environment. With this example, we’ll get a property environment.name with the value “production”.
提供的数据将被填充到Spring Environment中。在这个例子中,我们将得到一个属性environment.name,值为 “production”。
Also, we can load our JSON as a System property, for example:
此外,我们可以将我们的JSON作为系统属性加载,例如。
$ java -Dspring.application.json='{"environment":{"name":"production"}}' -jar app.jar
The last option is to use a simple command line argument:
最后一个选项是使用一个简单的命令行参数。
$ java -jar app.jar --spring.application.json='{"environment":{"name":"production"}}'
With the last two approaches, the spring.application.json property will be populated with the given data as unparsed String.
使用后两种方法, spring.application.json属性将以未解析的String形式填充给定数据。
These are the most simple options to load JSON data into our application. The drawback of this minimalistic approach is the lack of scalability.
这些是将JSON数据加载到我们的应用程序的最简单选项。这种最小化的方法的缺点是缺乏可扩展性。。
Loading huge amount of data in the command line can be cumbersome and error-prone.
在命令行中加载大量的数据可能很麻烦,而且容易出错。
4. Load Properties via PropertySource Annotation
4.通过PropertySource注释加载属性
Spring Boot provides a powerful ecosystem to create configuration classes through annotations.
Spring Boot提供了一个强大的生态系统,通过注解创建配置类。
First of all, we define a configuration class with some simple members:
首先,我们定义一个具有一些简单成员的配置类。
public class JsonProperties {
private int port;
private boolean resend;
private String host;
// getters and setters
}
We can provide the data in the standard JSON format in an external file (let’s name it configprops.json):
我们可以在一个外部文件中以标准的JSON格式提供数据(我们将其命名为configprops.json)。
{
"host" : "mailer@mail.com",
"port" : 9090,
"resend" : true
}
Now we have to connect our JSON file to the configuration class:
现在我们必须将我们的JSON文件连接到配置类。
@Component
@PropertySource(value = "classpath:configprops.json")
@ConfigurationProperties
public class JsonProperties {
// same code as before
}
We have a loose coupling between the class and the JSON file. This connection is based on strings and variable names. Therefore we don’t have a compile-time check but we can verify the bindings with tests.
我们在类和JSON文件之间有一个松散的耦合。这种连接是基于字符串和变量名的。因此,我们没有编译时的检查,但我们可以通过测试来验证绑定关系。
Because the fields should be populated by the framework, we need to use an integration test.
因为这些字段应该由框架来填充,所以我们需要使用集成测试。
For a minimalistic setup, we can define the main entry point of the application:
对于一个最小化的设置,我们可以定义应用程序的主要入口点。
@SpringBootApplication
@ComponentScan(basePackageClasses = { JsonProperties.class})
public class ConfigPropertiesDemoApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class).run();
}
}
Now we can create our integration test:
现在我们可以创建我们的集成测试。
@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());
}
}
As a result, this test will generate an error. Even loading the ApplicationContext will fail with the following cause:
结果是,这个测试将产生一个错误。甚至加载ApplicationContext也会失败,原因如下。
ConversionFailedException:
Failed to convert from type [java.lang.String]
to type [boolean] for value 'true,'
The loading mechanism successfully connects the class with the JSON file through the PropertySource annotation. But the value for the resend property is evaluated as “true,” (with a comma), which cannot be converted to a boolean.
加载机制通过PropertySource注解成功地将该类与JSON文件连接起来。但是resend属性的值被评估为”true,” (有一个逗号),这不能被转换为布尔值。
Therefore, we have to inject a JSON parser into the loading mechanism. Fortunately, Spring Boot comes with the Jackson library and we can use it through PropertySourceFactory.
因此,我们必须在加载机制中注入一个JSON解析器。幸运的是,Spring Boot自带Jackson库,我们可以通过PropertySourceFactory使用它。
5. Using PropertySourceFactory to Parse JSON
5.使用PropertySourceFactory来解析JSON
We have to provide a custom PropertySourceFactory with the capability of parsing JSON data:
我们必须提供一个自定义的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);
}
}
We can provide this factory to load our configuration class. For that, we have to reference the factory from the PropertySource annotation:
我们可以提供这个工厂来加载我们的配置类。为此,我们必须从PropertySource注解中引用该工厂。
@Configuration
@PropertySource(
value = "classpath:configprops.json",
factory = JsonPropertySourceFactory.class)
@ConfigurationProperties
public class JsonProperties {
// same code as before
}
As a result, our test will pass. Furthermore, this property source factory will happily parse list values also.
因此,我们的测试将通过。此外,这个属性源工厂也会很乐意解析列表值。
So now we can extend our configuration class with a list member (and with the corresponding getters and setters):
所以现在我们可以用一个列表成员来扩展我们的配置类(以及相应的获取器和设置器)。
private List<String> topics;
// getter and setter
We can provide the input values in the JSON file:
我们可以在JSON文件中提供输入值。
{
// same fields as before
"topics" : ["spring", "boot"]
}
We can easily test the binding of list values with a new test case:
我们可以用一个新的测试用例轻松地测试列表值的绑定。
@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenLoadListValues() {
assertThat(
jsonProperties.getTopics(),
Matchers.is(Arrays.asList("spring", "boot")));
}
5.1. Nested Structures
5.1.嵌套结构
Dealing with nested JSON structures isn’t an easy task. As the more robust solution, the Jackson library’s mapper will map the nested data into a Map.
处理嵌套的JSON结构并不是一件容易的事。作为更稳健的解决方案,Jackson库的映射器将把嵌套的数据映射到一个Map。
So we can add a Map member to our JsonProperties class with getters and setters:
因此,我们可以在我们的JsonProperties类中添加一个Map成员,并带有getters和setters。
private LinkedHashMap<String, ?> sender;
// getter and setter
In the JSON file we can provide a nested data structure for this field:
在JSON文件中,我们可以为这个字段提供一个嵌套的数据结构。
{
// same fields as before
"sender" : {
"name": "sender",
"address": "street"
}
}
Now we can access the nested data through the map:
现在我们可以通过地图访问嵌套的数据。
@Test
public void whenPropertiesLoadedViaJsonPropertySource_thenNestedLoadedAsMap() {
assertEquals("sender", jsonProperties.getSender().get("name"));
assertEquals("street", jsonProperties.getSender().get("address"));
}
6. Using a Custom ContextInitializer
6.使用一个自定义的ContextInitializer
If we’d like to have more control over the loading of properties, we can use custom ContextInitializers.
如果我们想对属性的加载有更多的控制,我们可以使用自定义的ContextInitializers。。
This manual approach is more tedious. But, as a result, we’ll have full control of loading and parsing the data.
这种手工方法比较繁琐。但是,作为一个结果,我们将完全控制加载和解析数据。
We’ll use the same JSON data as before, but we’ll load into a different configuration class:
我们将使用与之前相同的JSON数据,但我们将加载到一个不同的配置类。
@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {
private String host;
private int port;
private boolean resend;
// getters and setters
}
Note that we don’t use the PropertySource annotation anymore. But inside the ConfigurationProperties annotation, we defined a prefix.
注意,我们不再使用PropertySource注解了。但是在ConfigurationProperties注解里面,我们定义了一个前缀。
In the next section, we’ll investigate how we can load the properties into the ‘custom’ namespace.
在下一节,我们将研究如何将属性加载到‘custom’命名空间。
6.1. Load Properties into a Custom Namespace
6.1.将属性加载到自定义命名空间
To provide the input for the properties class above, we’ll load the data from the JSON file and after parsing we’ll populate the Spring Environment with MapPropertySources:
为了给上述属性类提供输入,我们将从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);
}
}
}
As we can see, it requires a bit of quite complex code, but this is the price of flexibility. In the above code, we can specify our own parser and decide what to do with each entry.
正如我们所看到的,它需要一点相当复杂的代码,但这是灵活性的代价。在上面的代码中,我们可以指定我们自己的分析器,并决定对每个条目做什么。
In this demonstration, we just put the properties into a custom namespace.
在这个演示中,我们只是把属性放到一个自定义命名空间中。
To use this initializer we have to wire it to the application. For production use, we can add this in the SpringApplicationBuilder:
为了使用这个初始化器,我们必须把它连接到应用程序中。对于生产使用,我们可以在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();
}
}
Also, note that the CustomJsonProperties class has been added to the basePackageClasses.
另外,请注意,CustomJsonProperties类已被添加到basePackageClasses。
For our test environment, we can provide our custom initializer inside of the ContextConfiguration annotation:
对于我们的测试环境,我们可以在ContextConfiguration注解中提供我们的自定义初始化器。
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ConfigPropertiesDemoApplication.class,
initializers = JsonPropertyContextInitializer.class)
public class JsonPropertiesIntegrationTest {
// same code as before
}
After auto-wiring our CustomJsonProperties class, we can test the data binding from the custom namespace:
在自动连接我们的CustomJsonProperties类之后,我们可以从自定义命名空间测试数据绑定。
@Test
public void whenLoadedIntoEnvironment_thenFlatValuesPopulated() {
assertEquals("mailer@mail.com", customJsonProperties.getHost());
assertEquals(9090, customJsonProperties.getPort());
assertTrue(customJsonProperties.isResend());
}
6.2. Flattening Nested Structures
6.2.扁平化的嵌套结构
The Spring framework provides a powerful mechanism to bind the properties into objects members. The foundation of this feature is the name prefixes in the properties.
Spring框架提供了一个强大的机制来将属性绑定到对象成员中。这一功能的基础是属性中的名称前缀。
If we extend our custom ApplicationInitializer to convert the Map values into a namespace structure, then the framework can load our nested data structure directly into a corresponding object.
如果我们扩展我们的自定义ApplicationInitializer,将Map值转换为命名空间结构,那么框架可以将我们的嵌套数据结构直接加载到相应的对象中。
The enhanced CustomJsonProperties class:
增强的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
}
The enhanced ApplicationContextInitializer:
增强的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)));
}
}
As a result, our nested JSON data structure will be loaded into a configuration object:
因此,我们的嵌套JSON数据结构将被加载到一个配置对象中:。
@Test
public void whenLoadedIntoEnvironment_thenValuesLoadedIntoClassObject() {
assertNotNull(customJsonProperties.getSender());
assertEquals("sender", customJsonProperties.getSender()
.getName());
assertEquals("street", customJsonProperties.getSender()
.getAddress());
}
7. Conclusion
7.结论
The Spring Boot framework provides a simple approach to load external JSON data through the command line. In case of need, we can load JSON data through properly configured PropertySourceFactory.
Spring Boot框架提供了一个简单的方法来通过命令行加载外部JSON数据。在需要的情况下,我们可以通过正确配置的PropertySourceFactory加载JSON数据。
Although, loading nested properties is solvable but requires extra care.
虽然,加载嵌套属性是可以解决的,但需要格外小心。
As always, the code is available over on GitHub.
像往常一样,代码可在GitHub上获得。