Spring Bootの外部設定値の扱い方を理解する

今回は外部設定値（プロパティファイル、JVMのシステムプロパティ、環境変数などに定義した設定値）をSpring Bootがどのように扱うのか紹介したいと思います。
なお、前回紹介した「[Spring BootのAutoConfigureの仕組みを理解する](http://qiita.com/kazuki43zoo/items/8645d9765edd11c6f1dd)」でも外部設定値を参照した条件付きBean定義の仕組みがサポートされていたり、各AutoConfigure用のコンフィギュレーションクラスの中から外部設定値を参照してBean定義していたりします！！

# 前提バージョン

* Spring Boot **3.1.1** ~~(1.4.1.BUILD-SNAPSHOT→2.4.5)~~
* Spring Framework **6.0.10** ~~(4.3.3.BUILD-SNAPSHOT→5.3.6)~~

:::note info
**変更履歴**

**[2023/6/25]**
投稿から7年くらいたっても一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。また前回反映できなかった機能追加や改善内容についても以下のセクションを追加して反映しました。なお、全て動作検証したわけではありませんが、Spring Boot 2.7.xおよび3.0.xでも本投稿に記載している内容は有効だと思います。

* [@DynamicPropertySourceの利用](#dynamicpropertysourceの利用)
* [プロパティファイルのインクルード](#プロパティファイルのインクルード)
* [Configuration Treesの利用](#configuration-treesの利用)
* [マルチドキュメントの利用](#マルチドキュメントの利用)

**[2021/5/7]**
投稿から5年くらいたっても一定のViewが継続してあるので、最新のSpring(Spring Boot)バージョンの内容に更新しました。外部設定周りは投稿時のSpring Boot 1.4.x系からかなり機能追加・改善が行われているため、

* 「バージョンアップに伴う記載内容の見直し（必要最小限の修正）」
* 「機能追加部分の説明追加」

の２回に分けて更新したいと思います。まずは、「バージョンアップに伴う記載内容の見直し」になります。
:::

# 外部設定として扱う値は？

アプリケーションを作成する場合、ソースコード上にハードコーディングできない（or したくない）値を扱うことがよくあります。具体的には・・・以下のような値が外部設定値として扱う候補になります。

* データベースへの接続情報などの環境に依存する値
* 機能要件で決められたアプリケーションの動作を変更するためのパラメータ値
* 機能要件上はパラメータ化する必要はないけどテスト容易性などを考慮して外部化しておいた方がよい値
* 性能要件に応じてチューニングが必要になる事が想定されるパラメータ値
* etc..

# 外部設定値の取得方法

Spring Bootアプリケーションでは、以下のいずれかの方法で外部設定値を取得することができます。

* Spring Bootが提供している「[Type-safe Configuration Properties](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties)」の仕組みを利用する （**基本的にはこの仕組みを使うのがお勧めです** :laughing: :laughing: :laughing:）
* Spring Frameworkが提供している`@Value`を使用する
* Spring Frameworkが提供している`Environment`インタフェースを利用する

それぞれ簡単に取得方法を紹介しておきましょう。

なお、「`@Value`」と「Type-safe Configuration Properties」には機能的な違い（制限）があるので、[Spring Bootのリファレンス](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties.vs-value-annotation)を参照しておくと良いと思います。「Type-safe Configuration Properties」ではSpELが利用できないので、プロパティ指定時にSpELを使う必要がある場合のみ`@Value`を使えば良いと思います。


```properties:src/main/resources/application.properties
app.timezone=Asia/Tokyo
```

## Type-safe Configuration Propertiesの利用

DIコンテナで管理するクラスに`@ConfigurationProperties`を付与すると、クラス内のプロパティに設定値（「`prefix`属性に指定したプレフィック + "." + プロパティ名」の値）が自動的にインジェクションされます。デフォルト値を設けたい場合は、プロパティの初期値として指定してください。

```java
@ConfigurationProperties("app")
public class AppProperties {

  private TimeZone timezone = TimeZone.getTimeZone("UTC"); // ← "app.timezone"の設定値がインジェクションされる

  // ... getter/setter

}
```

```java
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan // @ConfigurationPropertiesが付与されたクラスをスキャンしてDIコンテナに登録する
public class SpringBootPropertiesDemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringBootPropertiesDemoApplication.class, args);
  }

}
```

この方法は、Spring BootのAutoConfigureの中でも利用されています。

:::note info
`@ConfigurationPropertiesScan`はSpring Boot 2.2で追加されたアノテーションです。
:::

## `@Value`の利用

DIコンテナで管理するクラスのフィールド、setterメソッド、コンストラクタ引数などに`@Value(${設定名:デフォルト値})`の形式で指定すると、指定した設定値がインジェクションされます。デフォルト値は省略可能ですが、デフォルト値が未指定の状態で設定値の指定がないとエラーになります。

```java
@Component
public class AppProperties {

  @Value("${app.timezone:UTC}") 
  private TimeZone timezone; // ← "app.timezone"の設定値がインジェクションされる

  // ... getter/setter

}
```

## `Environment`インタフェースの利用

外部設定値は、Spring Frameworkが提供している`Environment`インタフェースのメソッドを介して取得することもできます。この方法を使うことは基本的にはないと思いますが、プログラム内で動的に取得先(設定名)を切り替えるような使い方をしたい場合は、この方法を利用を検討してみてください。

```java
@Service
public class MyService {
  private static final TimeZone DEFAULT_TIMEZONE= TimeZone.getTimeZone("UTC");
  private final Environment environment;
  public MyService(Environment environment) { // Environmentをインジェクション
    this.environment = environment;
  }
  public void printApplicationManagedTimezone() {
    TimeZone timezone = environment.getProperty("app.timezone", TimeZone.class, DEFAULT_TIMEZONE);
    System.out.println(timezone);
  }
}
```

# 外部設定値の指定方法

以下に、外部設定値の主な指定方法を(優先度が高い順に)紹介します。なお、個人的にたぶん利用しないだろうな〜と思った方法については説明は割愛します。（サポートしている指定方法の全量は、[Spring Bootのリファレンス](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config)をご覧ください）

| 指定方法 | 説明 |
| --- | --- |
| Developer tools用のプロパティファイル | [Spring Boot Developer tools](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.devtools.globalsettings)用のプロパティファイル(`~/.config/spring-boot/spring-boot-devtools.properties`)に「`設定名=値`」の形式で指定する。（Spring Boot Developer toolsを適用している場合のみ） |
| `@TestPropertySource` | `@TestPropertySource`（テスト用のプロパティファイル or アノテーションの属性値）に「`設定名=値`」の形式で指定する。テストケース内で設定値を指定したい場合に利用。 |
| `@DynamicPropertySource` | テストケース内に[`@DynamicPropertySource`を付与したstaticメソッド](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.testcontainers.dynamic-properties)を用意し、そのメソッド内で動的に値を設定する。これはTestcontainersなどとの接続情報を実行時に取得してSpring Bootの設定値に反映したい場合に使える仕組みです。（Spring Boot 3.1より[Service Connections](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.testcontainers.service-connections)と呼ばれる仕組みが導入されたため、Testcontainersを使ったテストで`@DynamicPropertySource`を使う必要はなくなっています） |
| `@SpringBootTest` | `@SpringBootTest`(`@XxxxTest`)の`properties`属性に「`設定名=値`」の形式で指定する。テストケース内で設定値を指定したい場合に利用。|
| コマンドライン引数 | コマンドライン引数に「`--設定名=値`」の形式で指定する。 | |
| JSONアプリケーションプロパティ | 環境変数「`SPRING_APPLICATION_JSON`」（プロパティ名「`spring.application.json`」）にJSON構造で設定名と設定値を指定する |
| JVMのシステムプロパティ | JVMの起動引数に「`-D設定名=値`」の形式で指定する。| 
| OSの環境変数 | OSの環境変数に「`設定名=値`」の形式で指定する。|
| Spring Boot専用のプロパティファイル | Spring Boot専用のプロパティファイル/YAMLファイルに「`設定名=値`」の形式で指定する。 |
| `@PropertySource` | `@PropertySource`に指定した任意のプロパティファイルに「`設定名=値`」の形式で指定する。 |

:::note info
warとしてデプロイする場合は、JNDI、サーブレットコンテナのパラメータ、サーブレットの初期化パラメータにプロパティ値を指定することも可能ですが、warとしてデプロイするパターンはあまりないと思うので、本エントリーでは説明は割愛します。
:::

## Developer tools用のプロパティファイル

[Spring Boot Developer tools](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.devtools)を適用している場合は、OSユーザーのホームディレクトリ直下の「`~/.config/spring-boot/spring-boot-devtools.properties`」に開発者毎の設定値を指定することができます。
このプロパティファイルには、アプリケーションの機能的な動作仕様に影響を与えない設定値（例えば、ログレベルなど）のみ指定するように心がけましょう。さもないと・・・あなたの環境では仕様通り動くけど、他の環境では動かない・・・といった事象を引き起こすかもしれません :scream: 

:::note info
過去バージョンとの互換性を保つために、「`~/.config/spring-boot/spring-boot-devtools.properties`」が存在しない場合は「`~/.spring-boot-devtools.properties`」を読み込む仕組みになっています。
:::

## `@SpringBootTest`の`properties`属性の利用

特定のテストケースクラスのみで設定値を変更したい場合は、`@SpringBootTest`の`properties`属性に直接設定値を指定することができます。

```java
// ...
@SpringBootTest(properties = "app.timezone=Asia/Tokyo")
public class IntegrationTests {
    // ...
}
```

:::note info
`@JdbcTest`などのアノテーションでサポートされている`properties`属性も同様の扱いになります。
具体的なアノテーションについては、[Spring Bootのリファレンス](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.spring-boot-applications.autoconfigured-tests)を参照してください。
:::

## `@TestPropertySource`の利用

複数のテストケースクラスでテスト用の設定値を共有したい場合は、プロパティファイルを使うとよいです。

```java:テスト用のプロパティファイルを指定
// ...
@TestPropertySource(locations = "/test.properties")
public class IntegrationTests {
    // ...
}
```

```properties:テスト用のプロパティファイル（test.properties）
app.timezone=Asia/Tokyo
```

特定のテストケースクラスのみで設定値を変更したい場合は、`properties`属性に直接設定値を指定することができます。これは、前述した`@SpringBootTest`の`properties`属性で代替することができます。

```java
// ...
@TestPropertySource(properties = "app.timezone=Asia/Tokyo")
public class IntegrationTests {
    // ...
}
```

`@TestPropertySource`の詳しい利用方法については、[Spring Frameworkのリファレンスページ](https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/property-sources.html)をご覧ください。

## `@DynamicPropertySource`の利用

Spring Boot 3.0以前でTestcontainersを使ってテストを行う場合や、サードパーティ製のサービスや自前のMock機能との接続情報をテスト実行時にSpring Bootの設定値として認識させたい場合は`DynamicPropertySource`が使えます。

```java
@SpringBootTest
class MyIntegrationTests {

  static MockTcpServer mockTcpServer;

  @BeforeAll
  static void setupMockServer() {
    // port=0の場合は空きポートで起動
    mockTcpServer = new MockTcpServer(0);
  }

  // ...

  @DynamicPropertySource
  static void mockTcpServerProperties(DynamicPropertyRegistry registry) {
    // MockTcpServerに割り当てられたポート番号を設定値として追加
    registry.add("external.tcp-server.port", mockTcpServer::getPort);
  }

  @Test
  void myTest() {
    // ...
  }

}
```

:::note info
Spring Boot 3.1以降でTestcontainersを使う場合は、[Service Connections](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.testcontainers)と呼ばれる抽象化の仕組みを使って主要なプロダクトと連動するための仕組みがSpring Bootより提供されているため、`@DynamicPropertySource`を使う必要がなくなっています。
:::


## コマンドライン引数の利用

アプリケーションの実行時に設定値を変更したい場合は、コマンドライン引数を使います。

```
$ java -jar xxx.jar --app.timezone=Asia/Tokyo
```

:::note info
`@SpringBootTest`の`args`属性を利用すると、テストでコマンドライン引数相当の指定をすることができます。

```java
// ...
@SpringBootTest(args = "--app.timezone=Asia/Tokyo")
public class IntegrationTests {
   // ...
}
```
:::

## JSONアプリケーションプロパティ

環境変数やシステムプロパティには指定可能なプロパティに制限があるため、それら制限を回避するための方法として、環境変数名「`SPRING_APPLICATION_JSON`」またはプロパティ名「`spring.application.json`」にJSON形式でプロパティを指定することができます。

```
$ export SPRING_APPLICATION_JSON='{"app":{"timezone":"Asia/Tokyo"}}' && java -jar xxx.jar
```

```
$ java -Dspring.application.json='{"app":{"timezone":"Asia/Tokyo"}}' -jar xxx.jar
```

上記の例では、「`app.timezone=Asia/Tokyo`」と同じ扱いになります。


## JVMのシステムプロパティ

アプリケーションの実行時に設定値を変更したい場合は、JVMのシステムプロパティを使います。コマンドライン引数はSpring Boot独自の仕組みなのに対して、システムプロパティはJava標準の仕組みなので、Spring Bootの管理外のコンポーネントと設定値を共有したい場合は、システムプロパティの利用を検討するとよいでしょう。

```
$ java -Dapp.timezone=Asia/Tokyo -jar xxx.jar
```

## OSの環境変数

アプリケーションを実行するOSユーザー毎に設定値を変更したい場合は、OSの環境変数(ユーザー環境変数)を使います。

```
$ APP_TIMEZONE=Asia/Tokyo
$ java -jar xxx.jar
```

環境変数(`_`区切り)で指定した設定値は、「`.`」や「`-`」区切りの名前で参照することができます。上記の例であれば、Spring Bootからは「`app.timezone`」または「`app-timezone`」という名前を指定して値を取得することができます。

## Spring Boot専用のプロパティファイル

Spring Bootは、Spring Bootアプリケーション専用のプロパティファイル/YAMLファイル(`application.properties`または`application.yml`)を読み込む仕組みになっています。Spring Bootアプリケーション専用のプロパティファイル/YAMLファイルの特徴としては、プロファイル毎にプロパティファイル/YAMLファイルを用意できる点があげられます。

明示的にアクティブなプロファイルを指定しない場合は、暗黙的に`default`というプロファイルを指定したことになり、以下の順番(優先順)で設定値が適用されます。

* `application-default.properties` (or `.yml`)
* `application.properties` (or `.yml`)

### プロファイル指定時のプロパティファイルの読み込み

また、[明示的にアクティブなプロファイルを指定した場合](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.profile-specific)は、プロファイルの指定順とは逆順でプロパティファイル/YAMLファイルの設定値が適用されます。例えば、`-Dspring.profiles.active=p1,p2`とした場合は、以下の順番(優先順)で設定値が適用されます。

* `application-p2.properties` (or `.yml`)
* `application-p1.properties` (or `.yml`)
* `application.properties` (or `.yml`)

### プロファイルのインクルード時の読み込み

さらに・・・Spring Boot専用のプロパティファイル/YAMLファイルでは、以下のように指定すると[プロファイルをインクルード](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles.adding-active-profiles)することができます。

```properties:application.properties
spring.profiles.include=default,p3
```

プロファイルをインクルードした場合は、インクルード先のプロパティファイル/YAMLファイルが優先されます。例えば、アクティブなプロファイルを指定しなかったケースを例に見てみると、以下の順番で設定値が適用されます。

* `application-p3.properties` (or `.yml`)
* `application-default.properties` (or `.yml`)
* `application.properties` (or `.yml`)

ポイントは、インクルードしたプロファイルのプロパティファイル/YAMLファイルの方が優先される点でしょう。たとえば以下のように、インクルード元でインクルード先に指定している設定値を上書きすることはできません。

```properties:application-p3.properties(インクルード先)
app.timezone=PST
```
```properties:application.properties(インクルード元)
spring.profiles.include=default,p3
app.timezone=Asia/Tokyo
```

上記の例では、`app.timezone`には`PST`が適用されます。
直感的には・・・ `Asia/Tokyo`になってほしい気がしますが・・・現時点のSpring Bootでは上書でされません。

### プロパティファイルのインクルード

目的に応じて任意のプロパティファイル設けて、それらをインクルードすることもできます。例えば、以下のようにデフォルトの設定を`application.properties`に定義しつつ、カスタマイズ用のプロパティファイルが存在していればそのファイルに指定されている値を優先して使用するといったことができます。

```properties:application.properties
app.timezone=Asia/Tokyo
spring.config.import=optional:timezone-custom.properties
```

```properties:timezone-custom.properties
app.timezone=Asia/Tokyo
```

プロパティファイルのインクルードの仕組に対してもプロファイルによる切り替えが可能なので、例えば `-Dspring.profiles.active=p1` とした場合は、`timezone-custom-p1.properties` というファイルがあればそちらに指定されている設定値が有効になります。

:::note info
Cloud環境の実行プラットフォームから提供されている構成情報管理（シークレット情報の管理など）の仕組みと連動したい場合に、それらが提供するyamlファイルを読み込みたい場合にこの機能が利用できます。
:::

### プロパティファイルの格納場所

Spring Boot専用のプロパティファイル/YAMLファイルは、デフォルトでは以下の場所に格納することができ、以下の順番で設定値が適用されます。

* アプリケーションの実行ディレクトリ直下の「config」ディレクトリのサブディレクトリ（`file:./config/*/`）
* アプリケーションの実行ディレクトリ直下の「config」ディレクトリ（`file:./config/`）
* アプリケーションの実行ディレクトリ（`file:./`）
* クラスパス直下の「config」ディレクトリ（`classpath:/config/`）
* クラスパス直下（`classpath:/`）

:::note info
**カスタマイズポイント**
プロパティファイルのベース名前(デフォルトは`application`)の変更・格納ディレクトリの追加・対象ファイルの追加(任意のファイルを対象にする場合)などは、専用の外部設定値(`spring.config.name`、`spring.config.location`、`spring.config.additional-location`)を指定することで実現することができます。詳しくは、「[Spring Bootのリファレンスページ](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files)」をご覧ください。
:::

### YAMLファイルの利用

Spring Bootでは、プロパティファイルの代わりにYAMLファイルを利用することができます。YAML形式はプロパティ形式に比べてデータ構造を表現するのに優れているため、Spring Bootが提供する「Type-safe Configuration Properties」との相性がよいです。

```yaml:application.yml
app:
    name: MyApp
```

本投稿ではYAMLファイルの使い方については説明は割愛するため、YAMLファイルを利用する場合は「[Spring Bootのリファレンスページ](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.yaml)」をご覧ください。

### マルチドキュメントの利用

[マルチドキュメントの仕組み](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.multi-document)を利用して、一つのプロパティファイルやYAMLファイルに異なるプロファイルやCloudプラットフォーム向けの設定値を指定することができます。
予め決められたキーワードを指定してマルチドキュメントを区切り、その設定を[有効にする条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.activation-properties)を指定します。

なお、設定を有効にするためのプロパティは以下の２つが用意されています。

| プロパティキー | 説明 |
| --- | --- |
| `spring.config.activate.on-profile` | 指定されたプロファイルと一致する場合に有効になる |
| `spring.config.activate.on-cloud-platform` | 指定されたCloud環境と一致した場合に有効になる |

■プロパティファイルの場合

`#---` or `!---` でくぎります（Spring Bootでのみ有効な表現です）。

```properties
app.timezone=UTC
#---
app.timezone=Asia/Tokyo
spring.config.activate.on-profile=p1
```

■YAMLファイルの場合

`---` でくぎります（YAMLとしてサポートされている表現です）。


```yaml
app:
  timezone: UTC
---
app:
  timezone: Asia/Tokyo
spring:
  config:
    activate:
      on-profile: "p1"  
```

## Configuration Treesの利用

Cloud環境の実行プラットフォームから構成情報を提供する代表的な方法として「構成情報の構造をファイルシステムのディレクトリツリーとしての提供」があり、[Configuration Trees](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.configtree)はこの形式をサポートする仕組みです。

例えば、以下のようなディレクトリ構成で設定値が格納されたファイルがマウントされるとします。

```
etc/
  config/
    app/
      timezone ←　設定値が格納されるファイル
```

```:/etc/config/app/timezone
Asia/Tokyo
```

これをSpring Bootのアプリケーション内で「`app.timezone`」という設定値として扱いたい場合は、以下のような指定で読み込むことで実現することができます。

```properties:application.properties
app.timezone=UTC
spring.config.import=optional:configtree:/etc/config/
```

## `@PropertySource`の利用

Spring Bootでは、Spring Frameworkが提供している`@PropertySource`を使用して任意のプロパティファイルを読み込むこともできます。

```java
@SpringBootApplication
@PropertySource("classpath:security.properties")
public class SpringBootDemoApplication  {
    // ...
}
```

## プロパティ値の置き換え（プレースホルダの利用）

プロパティファイルやYAMLファイル内では、[プレースホルダを利用して](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.files.property-placeholders)プロパティ値に別の外部設定値を埋め込むことができます。

```properties:application.properties
app.name=MyApp
app.description=${app.name} is a Spring Boot application
```

```yaml:application.yml
app:
    name: MyApp
    description: ${app.name} is a Spring Boot application
```

さらに、Spring Bootは乱数を埋め込むための特別の設定値（`random.`で始まる設定値）を提供しています。詳しくは、「[Spring Bootのリファレンスページ](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.random-values)」をご覧ください。


# まとめ

今回は、Spring Bootアプリケーションで外部設定値を扱う方法について紹介してみました。外部設定の管理は、アプリケーションを作る上で絶対に欠かすことができない技術要素の一つといえます。Spring Bootの場合は、実行環境に依存する設定値はプロファイル毎のプロパティファイル/YAMLファイルに定義し、アプリケーション起動時のパラメータ(`spring.profiles.acive`)で利用するプロファイルを切り替えるのが標準的な管理方法になります。また、Cloud環境で実行する際にプラットフォーム側で用意されている機能と連携するための仕組みもサポートされているので、利用しているCloud環境に最適な方法を選択することができるようになっています。

なお、本投稿では触れていない部分もいくつかあるので、ちゃんと把握したい方はSpring Bootのリファレンスページを一読することをお勧めします。

* https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

# 参考サイト

* https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
* https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/ctx-management/property-sources.html
* https://docs.spring.io/spring-framework/reference/core/beans/environment.html#beans-using-propertysource