查看原文
其他

译:从 JSON 文件加载 Spring Boot 属性

Darren Luo SpringForAll社区 2020-10-17

原文链接:https://www.baeldung.com/spring-boot-json-properties

作者:baeldung

译者:Darren Luo

1. 简介

使用外部配置属性(properties)是一种常见的模式。

最常见的一个情况是我们的应用程序能在多种环境中(比如开发、测试和生产)改变行为,而不需要改变部署工件。

在本教程中,我们将关注 Spring Boot 应用程序如何从 JSON 文件中加载属性

2. 在 Spring Boot 中加载属性

Spring 和 Spring Boot 对加载外部属性有极强的支持,你可以在本文中找到基础知识的精彩概述。

由于这种支持主要集中于 .properties 和 .yml 文件,使用 JSON 通常需要额外配置

我们将假设基本功能是大家都知道的,并且在这里特别关注 JSON 方面。

3. 通过命令行加载属性

我们能在命令行中以三种预定义格式提供 JSON 数据。

首先,我们可以在 UNIX shell 中设置 SPRING_APPLIACATION_JSON 环境变量:

  1. $ SPRING_APPLICATION_JSON='{"environment":{"name":"production"}}' java -jar app.jar

提供的数据将填充到 Spring Enviroment。再这个例子中,我们将获取一个带有“production”值的 environment.name 属性。

此外,我们可以加载我们的 JSON 作为系统属性,举一个例子:

  1. $ java -Dspring.application.json='{"environment":{"name":"production"}}' -jar app.jar

最后选项是使用简单的命令行参数:

  1. $ java -jar app.jar --spring.application.json='{"environment":{"name":"production"}}'

使用最后两种方法,spring.application.json 属性将使用给定数据填充为未解析的 String

这些是加载 JSON 数据进我们应用程序的最简单的选项。这种简单的方法的缺点是缺乏可扩展性

再命令行中加载大量数据可能很麻烦并且容易出错。

4. 通过 PropertySource 注解加载属性

Spring Boot 提供一个强大的生态系统,可通过注解创建配置类。

首先,我们使用一些简单成员定义一个配置类:

  1. public class JsonProperties {

  2.    private int port;

  3.    private boolean resend;

  4.    private String host;

  5.   // getters and setters

  6. }

我们可以在外部文件(我们将其命名为 configprops.json)中提供标准 JSON 个数的数据:

  1. {

  2.  "host" : "mailer@mail.com",

  3.  "port" : 9090,

  4.  "resend" : true

  5. }

现在我们必须见我们的 JSON 文件连接到配置类:

  1. @Component

  2. @PropertySource(value = "classpath:configprops.json")

  3. @ConfigurationProperties

  4. public class JsonProperties {

  5.    // same code as before

  6. }

我们在类和 JSON 文件之间进行解耦。该连接基于字符串和变量名。因此,我们没有编译时检查,但是我们可以通过测试验证绑定。

因为字段应该由框架填充,我们需要使用集成测试。

使用简约设置,我们可以定义应用程序的主要入口:

  1. @SpringBootApplication

  2. @ComponentScan(basePackageClasses = { JsonProperties.class})

  3. public class ConfigPropertiesDemoApplication {

  4.    public static void main(String[] args) {

  5.        new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class).run();

  6.    }

  7. }

现在,我们可以创建我们的集成测试:

  1. @RunWith(SpringRunner.class)

  2. @ContextConfiguration(

  3.  classes = ConfigPropertiesDemoApplication.class)

  4. public class JsonPropertiesIntegrationTest {

  5.    @Autowired

  6.    private JsonProperties jsonProperties;

  7.    @Test

  8.    public void whenPropertiesLoadedViaJsonPropertySource_thenLoadFlatValues() {

  9.        assertEquals("mailer@mail.com", jsonProperties.getHost());

  10.        assertEquals(9090, jsonProperties.getPort());

  11.        assertTrue(jsonProperties.isResend());

  12.    }

  13. }

实测将因此生成一个错误。即使加载 ApplicationContext 也会失败,原因如下:

  1. ConversionFailedException:

  2. Failed to convert from type [java.lang.String]

  3. to type [boolean] for value 'true,'

加载机制通过 PropertySource 注解成功将类和 JSON 文件连接起来。但是 resend属性的值被评估为“true,”(带逗号),不能转换为 boolean。

因此,我们必须将 JSON 解析器注入加载机制。幸运的是,Spring Boot 附带了 Jackson 库,我们可以通过 PropertySourceFactory 使用它。

5. 使用 PropertySourceFactory 解析 JSON

我们必须提供一个自定义的具有解析 JSON 数据能力的 PropertySourceFactory

  1. public class JsonPropertySourceFactory

  2.  implements PropertySourceFactory {

  3.    @Override

  4.    public PropertySource<?> createPropertySource(

  5.      String name, EncodedResource resource)

  6.          throws IOException {

  7.        Map readValue = new ObjectMapper()

  8.          .readValue(resource.getInputStream(), Map.class);

  9.        return new MapPropertySource("json-property", readValue);

  10.    }

  11. }

我们可以提供这个工厂来加载我们的配置类。为此,我们必须从 PropertySource注解引用该工厂:

  1. @Configuration

  2. @PropertySource(

  3.  value = "classpath:configprops.json",

  4.  factory = JsonPropertySourceFactory.class)

  5. @ConfigurationProperties

  6. public class JsonProperties {

  7.    // same code as before

  8. }

我们的测试将因此通过。此外,此属性源工厂也将适当的解析列表值。

所以,现在我们可以使用列表成员(以及相应的 getters 和 setters)扩展我们的配置类:

  1. private List<String> topics;

  2. // getter and setter

我们可以在 JSON 文件中提供输入值:

  1. {

  2.    // same fields as before

  3.    "topics" : ["spring", "boot"]

  4. }

我们可以使用新的测试用例轻松测试列表值的绑定:

  1. @Test

  2. public void whenPropertiesLoadedViaJsonPropertySource_thenLoadListValues() {

  3.    assertThat(

  4.      jsonProperties.getTopics(),

  5.      Matchers.is(Arrays.asList("spring", "boot")));

  6. }

5.1 嵌套结构

处理嵌套的 JSON 结构不是一个简单的任务。作为更强大的解决方案,Jackson 库的映射器将映射嵌套数据到 Map

所以我们可以使用 getters 和 setters 将 Map 成员添加到我们的 JsonProperties类:

  1. private LinkedHashMap<String, ?> sender;

  2. // getter and setter

在 JSON 文件中我们可以为此字段提供嵌套数据结构:

  1. {

  2.  // same fields as before

  3.   "sender" : {

  4.     "name": "sender",

  5.     "address": "street"

  6.  }

  7. }

现在我们可以通过 map 访问嵌套数据:

  1. @Test

  2. public void whenPropertiesLoadedViaJsonPropertySource_thenNestedLoadedAsMap() {

  3.    assertEquals("sender", jsonProperties.getSender().get("name"));

  4.    assertEquals("street", jsonProperties.getSender().get("address"));

  5. }

6. 使用自定义 ContextInitializer

如果我们需要更多的控制属性的加载,我们可以使用自定义 ContextInitializers。

这种手动方法更繁琐。但是,我们将因此完全控制加载和解析数据。

我们将使用和以前一样的 JSON 数据,但是我们将其加载到不同的配置类。

  1. @Configuration

  2. @ConfigurationProperties(prefix = "custom")

  3. public class CustomJsonProperties {

  4.    private String host;

  5.    private int port;

  6.    private boolean resend;

  7.    // getters and setters

  8. }

注意,我们不再使用 PropertySource 注解。但是在 ConfigurationProperties 注解中,我们定义了一个前缀。

在下一节中,我们将研究如何将属性加载到‘customer’命名空间中。

6.1. 将属性加载到自定义命名空间

要为上面的属性类提供输入,我们将从 JSON 文件中加载数据并在之后解析,我们将使用 MapPropertySources 填充 Spring Environment

  1. public class JsonPropertyContextInitializer

  2. implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  3.    private static String CUSTOM_PREFIX = "custom.";

  4.    @Override

  5.    @SuppressWarnings("unchecked")

  6.    public void

  7.      initialize(ConfigurableApplicationContext configurableApplicationContext) {

  8.        try {

  9.            Resource resource = configurableApplicationContext

  10.              .getResource("classpath:configpropscustom.json");

  11.            Map readValue = new ObjectMapper()

  12.              .readValue(resource.getInputStream(), Map.class);

  13.            Set<Map.Entry> set = readValue.entrySet();

  14.            List<MapPropertySource> propertySources = set.stream()

  15.               .map(entry-> new MapPropertySource(

  16.                 CUSTOM_PREFIX + entry.getKey(),

  17.                 Collections.singletonMap(

  18.                 CUSTOM_PREFIX + entry.getKey(), entry.getValue()

  19.               )))

  20.               .collect(Collectors.toList());

  21.            for (PropertySource propertySource : propertySources) {

  22.                configurableApplicationContext.getEnvironment()

  23.                    .getPropertySources()

  24.                    .addFirst(propertySource);

  25.            }

  26.        } catch (IOException e) {

  27.            throw new RuntimeException(e);

  28.        }

  29.    }

  30. }

我们可以看到,它需要相当复杂的代码,但是这是灵活的代价。在上面的代码中,我们可以指定我们自己的解析器并决定如何处理每个条目。

在本演示中,我们只将属性放入自定义命名空间。

要使用此初始化器,我们必须将其连接到应用程序。对于生产用途,我们可以在 SpringApplicationBuilder 中添加它:

  1. @EnableAutoConfiguration

  2. @ComponentScan(basePackageClasses = { JsonProperties.class,

  3.  CustomJsonProperties.class })

  4. public class ConfigPropertiesDemoApplication {

  5.    public static void main(String[] args) {

  6.        new SpringApplicationBuilder(ConfigPropertiesDemoApplication.class)

  7.            .initializers(new JsonPropertyContextInitializer())

  8.            .run();

  9.    }

  10. }

另外需要注意,CustomJsonProperties 类已经被添加到 basePackageClasses中。

对于我们的测试环境,我们可以在 ContextConfiguration 注解中提供我们的自定义初始化器:

  1. @RunWith(SpringRunner.class)

  2. @ContextConfiguration(classes = ConfigPropertiesDemoApplication.class,

  3.  initializers = JsonPropertyContextInitializer.class)

  4. public class JsonPropertiesIntegrationTest {

  5.    // same code as before

  6. }

在自动装配好我们的 CustomerJsonProperties 类之后,我们可以测试从自定义命名空间绑定数据:

  1. @Test

  2. public void whenLoadedIntoEnvironment_thenFlatValuesPopulated() {

  3.    assertEquals("mailer@mail.com", customJsonProperties.getHost());

  4.    assertEquals(9090, customJsonProperties.getPort());

  5.    assertTrue(customJsonProperties.isResend());

  6. }

6.2. 扁平化嵌套结构

Spring 框架提供了一个强大的机制来绑定属性到对象成员。此功能的基础是属性中的名称前缀。

如果我们扩展我们自定义 ApplicationInitializer 来将 Map 的值转换为命名空间结构,然后框架可以直接加载我们的嵌套数据结构到想对应的对象中。

增强的 CustomJsonProperties 类:

  1. @Configuration

  2. @ConfigurationProperties(prefix = "custom")

  3. public class CustomJsonProperties {

  4.   // same code as before

  5.    private Person sender;

  6.    public static class Person {

  7.        private String name;

  8.        private String address;

  9.        // getters and setters for Person class

  10.   }

  11.   // getters and setters for sender member

  12. }

增强的 ApplicationContextInitializer

  1. public class JsonPropertyContextInitializer

  2.  implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  3.    private final static String CUSTOM_PREFIX = "custom.";

  4.    @Override

  5.    @SuppressWarnings("unchecked")

  6.    public void

  7.      initialize(ConfigurableApplicationContext configurableApplicationContext) {

  8.        try {

  9.            Resource resource = configurableApplicationContext

  10.              .getResource("classpath:configpropscustom.json");

  11.            Map readValue = new ObjectMapper()

  12.              .readValue(resource.getInputStream(), Map.class);

  13.            Set<Map.Entry> set = readValue.entrySet();

  14.            List<MapPropertySource> propertySources = convertEntrySet(set, Optional.empty());

  15.            for (PropertySource propertySource : propertySources) {

  16.                configurableApplicationContext.getEnvironment()

  17.                  .getPropertySources()

  18.                  .addFirst(propertySource);

  19.            }

  20.        } catch (IOException e) {

  21.            throw new RuntimeException(e);

  22.        }

  23.    }

  24.    private static List<MapPropertySource>

  25.      convertEntrySet(Set<Map.Entry> entrySet, Optional<String> parentKey) {

  26.        return entrySet.stream()

  27.            .map((Map.Entry e) -> convertToPropertySourceList(e, parentKey))

  28.            .flatMap(Collection::stream)

  29.            .collect(Collectors.toList());

  30.    }

  31.    private static List<MapPropertySource>

  32.      convertToPropertySourceList(Map.Entry e, Optional<String> parentKey) {

  33.        String key = parentKey.map(s -> s + ".")

  34.          .orElse("") + (String) e.getKey();

  35.        Object value = e.getValue();

  36.        return covertToPropertySourceList(key, value);

  37.    }

  38.    @SuppressWarnings("unchecked")

  39.    private static List<MapPropertySource>

  40.       covertToPropertySourceList(String key, Object value) {

  41.        if (value instanceof LinkedHashMap) {

  42.            LinkedHashMap map = (LinkedHashMap) value;

  43.            Set<Map.Entry> entrySet = map.entrySet();

  44.            return convertEntrySet(entrySet, Optional.ofNullable(key));

  45.        }

  46.        String finalKey = CUSTOM_PREFIX + key;

  47.        return Collections.singletonList(

  48.          new MapPropertySource(finalKey,

  49.            Collections.singletonMap(finalKey, value)));

  50.    }

  51. }

我们的嵌套 JSON 数据结构将因此被加载到一个配置对象中:

  1. @Test

  2. public void whenLoadedIntoEnvironment_thenValuesLoadedIntoClassObject() {

  3.    assertNotNull(customJsonProperties.getSender());

  4.    assertEquals("sender", customJsonProperties.getSender()

  5.      .getName());

  6.    assertEquals("street", customJsonProperties.getSender()

  7.      .getAddress());

  8. }

7. 总结

Spring Boot 框架提供了一个从命令行加载外部 JSON 数据的简单方法。如果需要,我们可以通过正确配置的 PropertySourceFactory 加载 JSON 数据。

虽然加载嵌套属性是可解决的,但是需要格外小心。

和往常一样,代码在Github上可见。

推荐: 【SFA官方翻译】:Spring中的组件扫描

上一篇:SpringBoot使用Mybatis-Generator

关注公众号


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存