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 配置的简单方式。如果需要更灵活的控制,可以通过自定义 PropertySourceFactoryContextInitializer 来实现。

虽然嵌套属性的处理需要额外注意,但借助 Spring 的强大机制,完全是可以解决的。

代码示例可以在 GitHub 获取。


原始标题:Load Spring Boot Properties From a JSON File | Baeldung