知識庫 / Spring / Spring Boot RSS 訂閱

從 JSON 文件加載 Spring Boot 屬性

JSON,Spring Boot
HongKong
9
09:55 PM · Dec 05 ,2025

1. 引言

使用外部配置屬性是一種非常常見的模式。

而且,最常見的問題之一是能夠在多個環境中(如開發、測試和生產)更改應用程序的行為,而無需更改部署工件。

在本教程中,我們將重點介紹如何在 Spring Boot 應用程序中從 JSON 文件加載屬性

2. 在 Spring Boot 中加載屬性

Spring 和 Spring Boot 都對加載外部配置提供了強大的支持 – 您可以在這篇文章中找到關於基礎知識的全面概述。

由於 這種支持主要集中在 .properties.yml 文件上 – 使用 JSON 通常需要額外的配置。

我們假設基本的特性已經為人熟知 – 並且將重點關注 JSON 特定的方面,這裏。

3. 通過命令行加載屬性

我們可以通過三個預定義的格式,在命令行中提供 JSON 數據。

首先,可以在 UNIX shell 中設置環境變量 SPRING_APPLICATION_JSON

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

提供的將會被填充到 Spring Environment 中。通過這個例子,我們將會獲得一個屬性 environment.name,其值為“production”。

此外,我們還可以將我們的 JSON 作為 System property 加載,例如:

$ 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. 通過 <em 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
}

現在我們需要將我們的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("[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,” (帶逗號),無法轉換為布爾值。

因此,我們需要將 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

因此,我們可以將 Map 成員添加到我們的 JsonProperties 類中,並添加相應的 getter 和 setter 方法:

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"));
}

使用自定義 ContextInitializer

如果我們希望對屬性的加載擁有更大的控制權,我們可以使用自定義的 ContextInitializer

這種手動方法更繁瑣。但是,作為結果,我們將完全控制數據的加載和解析。

我們將使用之前相同的 JSON 數據,但會將數據加載到不同的配置類中:

@Configuration
@ConfigurationProperties(prefix = "custom")
public class CustomJsonProperties {

    private String host;

    private int port;

    private boolean resend;

    // getters and setters

}

請注意,我們不再使用 PropertySource 註解。但是,在 ConfigurationProperties 註解中,我們定義了一個前綴。

在下一部分,我們將研究如何將屬性加載到 ‘custom’ 命名空間中。

6.1. 將屬性加載到自定義命名空間

為了為上述的屬性類提供輸入,我們將從 JSON 文件中加載數據,並在解析後將數據填充到 Spring 的 Environment 中,使用 MapPropertySources

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

}

在自動注入 CustomJsonProperties 類後,我們可以測試從自定義命名空間的數據綁定:

@Test
public void whenLoadedIntoEnvironment_thenFlatValuesPopulated() {
    assertEquals("[email protected]", 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 數據。

雖然加載嵌套屬性是可行的,但需要格外小心。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.