@ConfigurationProperties でプロパティをバインドしているのに、値が空だったり不正な形式だったりして実行時に落ちた、という経験はないでしょうか。起動時に検知できれば障害を未然に防げるのに、と思いますよね。

この記事では @ConfigurationProperties に Bean Validation を組み合わせて、 アプリ起動時に設定値を検証する 実装手順を解説します。さらにテストコードで検証ロジックを自動化する方法まで見ていきましょう。

@ConfigurationProperties の基本的な使い方は Spring Bootのプロパティ管理ガイド を参照してください。

なぜ起動時に検証するのか

設定値の問題がやっかいなのは、エラーが遅れて現れることです。たとえばデータベースのURLが空文字だった場合、実際に接続しようとするまでエラーが出ません。本番リリース直後に障害になって初めて気づく、というパターンは珍しくありません。

Fail Fast の考え方では、問題はできるだけ早い段階で顕在化させるべきです。設定値の検証を起動時に行えば、デプロイした瞬間に設定ミスが判明します。開発環境での発見なら修正コストは最小で済みますよね。

spring-boot-starter-validationを追加する

Spring Boot 2.3以降、spring-boot-starter-validationspring-boot-starter-web に含まれなくなりました。明示的に依存を追加する必要があります。

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

これで Hibernate Validator が実装として使われ、Bean Validation のアノテーションが有効になります。

@ConfigurationPropertiesに@Validatedを付与する

設定クラスには @ConfigurationProperties に加えて @Validated を付けるのがポイントです。@Validated がないと、フィールドに制約アノテーションを書いても検証が走りません。

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    @NotBlank
    private String name;

    @NotNull
    private Integer timeoutSeconds;

    // getter/setter
}

Spring Boot 3.x では javax.validation から jakarta.validation パッケージに変わっています。2.x 系を使っている場合は javax.validation.constraints.* を import してください。

クラスの登録方法は @Component を付けるか、設定クラスで @EnableConfigurationProperties(AppProperties.class) を宣言するかのどちらかです。ライブラリとして配布する場合は @EnableConfigurationProperties が推奨されます(コンポーネントスキャン対象にならないため)。

代表的な制約アノテーションの適用例

実際によく使うアノテーションを組み合わせた例を見てみましょう。

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    @NotBlank
    private String name;

    // 正規表現でURLの形式チェック
    @Pattern(regexp = "https?://.+", message = "有効なURLを指定してください")
    private String endpointUrl;

    // 1〜300秒の範囲チェック
    @Min(1)
    @Max(300)
    private int timeoutSeconds;

    // ゼロ以下を禁止
    @Positive
    private int maxConnections;

    // getter/setter
}

@NotNullnull のみを弾きますが、@NotBlanknull と空文字・空白のみの文字列も弾いてくれます。文字列フィールドには基本的に @NotBlank を使う方が安全ですね。

ネストしたオブジェクトへの検証の伝播

データベース設定などをまとめた内部クラスを使うケースもよくあります。この場合、 @Valid を忘れると内部クラスの制約が無視されるので注意が必要です。@Valid の基本的な使い方については Spring Boot @Validアノテーションでバリデーションをシンプルに実装する方法 も参考にしてください。

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    // @NotNull は null チェック、@Valid はネスト検証の伝播と役割が異なる
    @NotNull
    @Valid
    private Database database;

    public static class Database {

        @NotBlank
        private String url;

        @Min(1)
        @Max(100)
        private int poolSize;

        // getter/setter
    }

    // getter/setter
}

対応する application.yml はこんな感じです。

app:
  database:
    url: jdbc:postgresql://localhost:5432/mydb
    pool-size: 10

app.database を丸ごと省略したり、pool-size0 を設定したりすると起動時にエラーになります。

起動エラーメッセージの読み方

バリデーションが失敗すると ConfigurationPropertiesBindException がスローされ、起動ログに次のような形式で出力されます。

APPLICATION FAILED TO START

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException:
Failed to bind properties under 'app' to com.example.AppProperties failed:

    Property: app.timeoutSeconds
    Value:    "0"
    Origin:   "app.timeout-seconds" from property source "application.yml" - 5:20
    Reason:   must be greater than or equal to 1

Action:

Update your application's configuration

Property: でどのフィールドが問題か、Value: で実際にバインドされた値、Origin:application.yml のどこに書かれた値か、Reason: で違反した制約の内容がわかります。複数のフィールドが失敗している場合は同じブロックが繰り返し出力されます。Property: はフィールド名(キャメルケース)で表示されますが、Origin: にはケバブケースの実際のキー名も出るので application.yml との対応付けがしやすいですね。

@SpringBootTestで設定バリデーションをテストする

バリデーションが機能することをテストコードで担保しておきましょう。

正常系は @SpringBootTest を使います。アプリ全体のコンテキストを起動して、設定値が正しくバインドされることを統合的に確認するのに向いています。

@SpringBootTest
@TestPropertySource(properties = {
    "app.name=MyApp",
    "app.endpoint-url=https://api.example.com",
    "app.timeout-seconds=30",
    "app.max-connections=5"
})
class AppPropertiesValidTest {

    @Autowired
    private AppProperties props;

    @Test
    void 有効な設定でコンテキストが起動する() {
        assertThat(props.getName()).isEqualTo("MyApp");
        assertThat(props.getTimeoutSeconds()).isEqualTo(30);
    }
}

異常系には ApplicationContextRunner を使います。コンテキスト全体を起動しないため高速で、設定プロパティクラス単体のバリデーション検証に向いています。@SpringBootTest はフルコンテキスト統合確認向き、ApplicationContextRunner は軽量・高速な単体検証向きと使い分けるとよいでしょう。

import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

class AppPropertiesInvalidTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
        .withUserConfiguration(TestConfig.class);

    @EnableConfigurationProperties(AppProperties.class)
    static class TestConfig {}

    @Test
    void 必須項目が未設定だと起動に失敗する() {
        runner.withPropertyValues(
                "app.name=",             // 空文字はNG
                "app.timeout-seconds=0"  // 1未満はNG
            )
            .run(context ->
                assertThat(context).hasFailed()
            );
    }
}

Spring Boot 2.xと3.xの注意点

コンストラクタバインディングの記法がバージョン間で異なります。

2.x スタイル@ConstructorBinding をクラスに付与します。

// Spring Boot 2.x
@ConfigurationProperties(prefix = "app")
@ConstructorBinding
public class AppProperties {
    private final String name;
    private final int timeoutSeconds;

    public AppProperties(String name, int timeoutSeconds) {
        this.name = name;
        this.timeoutSeconds = timeoutSeconds;
    }
}

3.x スタイル はコンストラクタが1つだけなら @ConstructorBinding を省略できます。複数コンストラクタがある場合は、バインドしたいコンストラクタに直接付与してください。

// Spring Boot 3.x(コンストラクタが1つなら @ConstructorBinding 不要)
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private final String name;
    private final int timeoutSeconds;

    public AppProperties(String name, int timeoutSeconds) {
        this.name = name;
        this.timeoutSeconds = timeoutSeconds;
    }
}

また、3.x では jakarta.validation パッケージを使います。2.x から移行する際は import 文の一括置換を忘れずに。

まとめ

@ConfigurationProperties にバリデーションを入れる手順はシンプルです。

  1. spring-boot-starter-validation を依存に追加
  2. @ConfigurationProperties クラスに @Validated を付与
  3. 各フィールドに @NotBlank@Pattern などの制約アノテーションを付ける
  4. ネストしたオブジェクトには @Valid を付けて検証を伝播

これだけで設定ミスがアプリ起動時に即検出できるようになります。ApplicationContextRunner を使ったテストを書いておけば、CI で設定バリデーションの品質を継続的に担保できますね。

設定値をプロファイルで切り替えている場合は Spring Bootのプロファイルで環境別設定を安全に切り替える も参考にしてください。設定値の暗号化に興味がある方は Jasyptによる設定値の暗号化 もどうぞ。