1. 序章

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

また、最も一般的な質問の1つは、デプロイメントアーティファクトを変更せずに、開発、テスト、本番などの複数の環境でアプリケーションの動作を変更できることです。

このチュートリアルでは、 Spring BootアプリケーションでJSONファイルからプロパティを読み込む方法に焦点を当てます。

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

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

このサポートは主に.propertiesファイルと.ymlファイルに焦点を当てているため、JSONを操作するには通常、追加の構成が必要です

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

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

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

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

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

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

また、 JSON Systemプロパティとしてロードできます(例:)。

$ 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データをアプリケーションにロードするための最も簡単なオプションです。 この最小限のアプローチの欠点は、スケーラビリティの欠如です。

コマンドラインに大量のデータをロードするのは面倒でエラーが発生しやすい場合があります。

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

したがって、JSONパーサーを読み込みメカニズムに挿入する必要があります。幸い、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<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クラスに追加できます。

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

プロパティの読み込みをより細かく制御したい場合は、カスタム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ファイルからデータをロードし、解析後、Spring EnvironmentMapPropertySources:を入力します。

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データをロードできます。

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

いつものように、コードはGitHubから入手できます。