JSONファイルからSpring Bootのプロパティを読み込む

JSONファイルからSpring Bootプロパティをロードする

1. 前書き

外部構成プロパティを使用することは、非常に一般的なパターンです。

そして、最も一般的な質問の1つは、開発アーティファクトを変更せずに、開発、テスト、本番などの複数の環境でアプリケーションの動作を変更する機能です。

このチュートリアルでは、we’ll focus onhow you can load properties from JSON files in a Spring Boot application

2. SpringBootでのプロパティの読み込み

SpringとSpringBootは、外部構成のロードを強力にサポートしています。基本の概要については、this articleを参照してください。

this support mainly focuses on .properties and .yml files – working with JSON typically needs extra configuration以降。

基本的な機能はよく知られていると想定し、ここではJSONの特定の側面に焦点を当てます。

3. コマンドラインからプロパティを読み込む

コマンドラインでJSONデータを3つの事前定義された形式で提供できます。

まず、UNIXシェルで環境変数SPRING_APPLICATION_JSONを設定できます。

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

提供されたデータは、SpringEnvironmentに入力されます。 この例では、値が「production」のプロパティenvironment.nameを取得します。

また、JSONSystem property, としてロードできます。例:

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

最後のオプションは、単純なコマンドライン引数を使用することです。

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

最後の2つのアプローチでは、spring.application.jsonプロパティに、解析されていないStringとして指定されたデータが入力されます。

これらは、JSONデータをアプリケーションにロードするための最も簡単なオプションです。 The drawback of this minimalistic approach is the lack of scalability.

コマンドラインに大量のデータをロードすると、面倒でエラーが発生しやすくなります。

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
}

ここで、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,”(コンマ付き)」として評価され、ブール値に変換できません。

Therefore, we have to inject a JSON parser into the loading mechanism.幸い、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

}

その結果、テストに合格します。 さらに、このプロパティソースファクトリはリスト値も喜んで解析します。

リストメンバー(および対応するゲッターとセッター)で構成クラスを拡張できます。

private List 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クラスに追加できます。

private LinkedHashMap 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の使用

プロパティの読み込みをより細かく制御したい場合は、カスタムContextInitializersを使用できます。

この手動のアプローチはより面倒です。 ただし、その結果、データの読み込みと解析を完全に制御できるようになります。

以前と同じ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ファイルからデータを読み込み、解析後、SpringEnvironmentMapPropertySources:を入力します

public class JsonPropertyContextInitializer
 implements ApplicationContextInitializer {

    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 set = readValue.entrySet();
            List 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 {

    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 set = readValue.entrySet();
            List propertySources = convertEntrySet(set, Optional.empty());
            for (PropertySource propertySource : propertySources) {
                configurableApplicationContext.getEnvironment()
                  .getPropertySources()
                  .addFirst(propertySource);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static List
      convertEntrySet(Set entrySet, Optional parentKey) {
        return entrySet.stream()
            .map((Map.Entry e) -> convertToPropertySourceList(e, parentKey))
            .flatMap(Collection::stream)
            .collect(Collectors.toList());
    }

    private static List
      convertToPropertySourceList(Map.Entry e, Optional parentKey) {
        String key = parentKey.map(s -> s + ".")
          .orElse("") + (String) e.getKey();
        Object value = e.getValue();
        return covertToPropertySourceList(key, value);
    }

    @SuppressWarnings("unchecked")
    private static List
       covertToPropertySourceList(String key, Object value) {
        if (value instanceof LinkedHashMap) {
            LinkedHashMap map = (LinkedHashMap) value;
            Set 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データを読み込むことができます。

ただし、ネストされたプロパティの読み込みは解決可能ですが、特別な注意が必要です。

いつものように、コードはover on GitHubで利用できます。