SpringBoot2.4からの設定ファイル処理方法

# 概要

SpringBoot2.4以降、設定ファイル(`application.yml`の類)の処理方法がガラッと変わっている。[^1]
[^1]: [SpringBoot2.4のリリースノート](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes#importing-additional-application-config)

SpringBoot2.x系を使っている分には、2.3以前の処理方法をonにするオプション(`spring.config.use-legacy-processing`)を利用できるが、SpringBoot3に追随する場合は左記のオプションは使えないので、新しい処理方法への適応が必須である。[^2]
[^2]: [公式のブログ](https://spring.io/blog/2022/05/24/preparing-for-spring-boot-3-0#:~:text=Migrate%20from%20Legacy%20application.properties%20and%20application.yaml%20Processing)でもこの点について言及されている

ここではこのSpringBoot2.4で入った設定ファイルの処理方法変更の背景、変更内容、spring-cloudへの影響を記述する。

## 設定ファイル処理方法の変更が入った背景

そもそもこのような変更が必要となった背景はSpring公式の[このブログ](https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4)に記載されている。
要約すると、Kubernetesの`ConfigMap`の設定読み取りに対応しつつ、従来の方法も提供し続けたいが、2.3以前の設定ファイルを処理するクラスである`ConfigFileApplicationListener`を使い続けるのは厳しいため、新たな仕組みへと刷新することにした、という内容であった。

> Kubernetesの`ConfigMap`の設定読み取りに対応しつつ

ここについてもう少し詳しく記述すると、Kubernetesにおいてはファイルのディレクトリをキーとし、ファイルの内容を値とする設定方法があり、SpringBoot2.4以降はその読み取りに対応できるようになっている。

従来の方法と新しい設定方法を比較してみる。
従来の`application.yml`から下記のような設定値を読み取ることができる。
```yaml
myapp:
  username: nannany
  password: dolphins
```

ファイルのディレクトリをキーとし、ファイルの内容を値とする設定で同様のことをしようとした場合、下記のようなディレクトリ構造をしていて、`username`ファイルの中に`nannany`という値、`password`ファイルの中に`dolphins`という値が含まれている、というイメージである。[^3]
```
/etc
└── config
    └── myapp
        ├── username
        └── password
```

[^3]: [このページ](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config.files.configtree)に具体的な方法が書いてある

SpringBoot2.4以降では、上記のどちらからも設定を取得することができるようになっている。

## どんな変更が入っている？

入った変更は下記ページがよくまとまっている。

https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4

- `application.properties`でも`#---`を使ったら1つのファイルを論理的に分割することができるようになった
    - `application.yml`では`---`を使って以前から分割できていた
- `spring.config.activate.on-profile`でactivateなprofileを切り替えることができるようになった
    - `spring.profiles.active`との共存に関しては、論理的に分割された別の範囲で共存することは可能。論理的にも一緒の部分で共存することは不可

```properties:これはエラーにならない
test=value
spring.profiles.active=local
#---
spring.config.activate.on-profile=dev
test=overridden value
```
```properties:これはエラーになる
test=value
#---
spring.config.activate.on-profile=dev
spring.profiles.active=local # will fail
test=overridden value
```
- 複数のprofileを1つのグループにまとめることができるようになった

```properties:こんな感じ
spring.profiles.group.prod=proddb,prodmq,prodmetrics
```

- `spring.config.import`で読み取るべき設定ファイルの指定、kubernetes likeな設定値の読み取りができるようになった
    - 肝の機能と思われる
    - 複数のキーバリューが設定してある設定ファイルから設定読み取りができる
    - ディレクトリをキー、ファイル内容をバリューとした設定からの読み取りができる

```properties: 複数のキーバリューが設定してある設定ファイルから設定読み取りができる
spring.config.import=file:/etc/config/myconfig.yaml
```

```properties: ディレクトリをキー、ファイル内容をバリューとした設定からの読み取りができる
spring.config.import=optional:configtree:/etc/config/
```

- クラウドプラットフォームによってアクティブにするか切り替えられる`spring.config.activate.on-cloud-platform`が用意された
    - kubernetesだけでなく、cloudfoundry、heroku、sap、azureappserviceが(おそらく)デフォルトで指定できる[^4]
    
```properties: こんな感じで、k8sならこうやって設定値を読み取る、と指定できる
spring.config.activate.on-cloud-platform=kubernetes
spring.config.import=configtree:/etc/config
```

[^4]: [CloudPlatform.java](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java)。このコードに出てくるものを列挙した

- 2.3以前の処理方法を継続したい人のために`spring.config.use-legacy-processing`オプションが用意された

## spring-cloudへの影響

spring-cloud側でもこの変更の影響を受けていて、例えばspring-cloud-commons内の`ConfigDataMissingEnvironmentPostProcessor.java`では下記のように`spring.config.import`が設定されていないと落ちることがある。[^5]
```java: ConfigDataMissingEnvironmentPostProcessor.java
	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		if (!shouldProcessEnvironment(environment)) {
			return;
		}
		List<Object> property = getConfigImports(environment);
		if (property == null || property.isEmpty()) {
			throw new ImportException("No spring.config.import set", false);
		}
		if (!property.stream().anyMatch(impt -> ((String) impt).contains(getPrefix()))) {
			throw new ImportException("spring.config.import missing " + getPrefix(), true);
		}
	}
```

[^5]: [ConfigDataMissingEnvironmentPostProcessor.javaのソースコード](https://github.com/spring-cloud/spring-cloud-commons/blob/efe5131a36088d915ba037ab85dd2e5daf3a0967/spring-cloud-commons/src/main/java/org/springframework/cloud/commons/ConfigDataMissingEnvironmentPostProcessor.java#L76-L87)

私自身、SpringBootとspring-cloudのバージョンを上げた際に、ユニットテストが上記のエラー分岐に入るようになって下記のエラーメッセージに直面した。

```
Caused by: org.springframework.cloud.commons.ConfigDataMissingEnvironmentPostProcessor$ImportException: No spring.config.import set
```

ユニットテストであったので、spring-cloud-configを無効にすべく`spring.cloud.config.enabled=false`にして対処することができた。

(直面した事象ベースで記述しており、ここ以外にもspring-cloudへの影響はあると思う)

# まとめ

SpringBoot2.4で大きく変更の入った設定ファイルの処理方法について、変更の背景・変更内容・spring-cloudへの影響(の一例)を記述した。

~~雑感として、SpringBootのドキュメントは色々あって本当に充実しているなぁと感じる一方、spring-cloud側はもうちょっと変更の影響をガイドする何かがあっても良いのでは、と思った。~~ -> ちゃんとみたらあった
https://spring.pleiades.io/spring-cloud-config/docs/current/reference/html/#config-data-import

追加で面白そうだと感じたのは、[ここ](https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4#:~:text=Supporting%20Additional%20Locations) に書いてあるのだが、`spring.config.import`設定ファイル読み込みはプラガブルで、`ConfigDataLocationResolver`、`ConfigDataLoader`を実装すれば独自のサポートが可能らしい。
実際、spring-cloud-config-clientでは、下記のように`configserver`なる文字列を利用した独自の指定が可能となっている。

```properties
spring.config.import=optional:configserver:http://myconfigserver.com
```




# 参考
- [Config file processing in Spring Boot 2.4](https://spring.io/blog/2020/08/14/config-file-processing-in-spring-boot-2-4)
    - 変更の経緯・変更内容が記されている
- [Spring Boot 2.4で導入されたConfig TreeでKubernetesのSecretの値をSpring Bootアプリのプロパティに設定](https://ik.am/entries/626)
    - configtreeで設定値を取得する実例が載っている。諸々の背景も詳しく書いてある