Spring Bootの@ConfigurationProperties入門|@Valueとの違い

Spring Bootの@ConfigurationProperties入門|@Valueとの違い

設定項目が増えると、Javaの各クラスに書いた設定キーと、その値の使い道を追いにくくなります。関連する設定をまとめて扱いたいときに使うのが、@ConfigurationPropertiesです。

この記事では、設定ファイルをJava側で利用し始めた方に向けて、設定クラスの定義・登録・利用から、階層構造と入力値の検証までを説明します。

目次

@ConfigurationPropertiesとは:設定値を型付きオブジェクトにまとめる

@ConfigurationPropertiesは、共通の接頭辞を持つ設定値をJavaオブジェクトに束縛するためのアノテーションです。「束縛」は、設定キーとJava側のプロパティを対応付け、値を取り込むことを指します。

たとえば通知機能の送信元、再試行回数、接続タイムアウトを一つの設定オブジェクトにまとめれば、利用側は設定キーの文字列を繰り返し書かずに値を参照できます。

prefixとJavaのプロパティの対応を理解する

@ConfigurationProperties(prefix = "app.notification")と指定すると、app.notification配下が対応付けの対象になります。たとえばapp.notification.sender-nameは、Java側のsenderNameに対応します。

prefixには設定グループの名前を指定します。Javaのパッケージ名や設定ファイル名を書く場所ではありません。

型への変換と設定値の妥当性検証を分けて考える

設定値をIntegerやDurationに変換することと、その値をアプリケーションで許容することは別です。再試行回数の-1は整数に変換できても、「0〜5回」という要件には合いません。

型変換は「その型として読めるか」、検証は「その値を受け入れてよいか」を確認します。設定クラスを定義するだけで、必須項目や範囲まで自動的に保証されるわけではありません。

最小のJava例で設定クラスを定義・登録・利用する

application.ymlと設定クラスを用意する

主例にはJavaのrecordを使い、コンストラクタ経由で設定値を受け取る方式に統一します。Java 17以上と、Spring Boot 3.5系の依存関係が設定されたプロジェクトを前提にします。ファイル構成は次のとおりです。

src/main/java/com/example/demo/DemoApplication.java
src/main/java/com/example/demo/config/NotificationProperties.java
src/main/java/com/example/demo/service/NotificationService.java
src/main/resources/application.yml

application.ymlには通知機能の設定をまとめます。

app:
  notification:
    sender-name: "Learning App"
    retry-count: 3
    channels:
      - email
      - push
    connection:
      timeout: 2s

NotificationProperties.javaでは、同じ構造をJavaの型で表現します。

ここではまず束縛だけを示し、検証は後の節で追加します。

package com.example.demo.config;

import java.time.Duration;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app.notification")
public record NotificationProperties(
        String senderName,
        Integer retryCount,
        List<String> channels,
        Connection connection) {

    public record Connection(Duration timeout) {
    }
}

この例のrecordは追加のコンストラクタを定義していないため、@ConstructorBindingを付けない形にしています。値はsenderName()やretryCount()で参照でき、setterは使いません。
Integerを選んだのは、後で未指定のnullと数値の0を区別して検証するためです。

@ConfigurationPropertiesScanと@EnableConfigurationPropertiesの登録方法

@ConfigurationPropertiesで対応関係を宣言したら、設定クラスをSpringのBeanとして登録します。
主例では、DemoApplication.javaに@ConfigurationPropertiesScanを付けます。

package com.example.demo;

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

@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {

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

スキャン範囲を省略した場合、アノテーションを付けたクラスのパッケージを起点に探索します。
この構成では、com.example.demo.configに置いた設定クラスが対象になります。

登録するクラスを明示したい場合は、代わりに@EnableConfigurationPropertiesを使えます。
次はDemoApplication.javaの置き換え例です。主例と両方を追加する必要はありません。

package com.example.demo;

import com.example.demo.config.NotificationProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(NotificationProperties.class)
public class DemoApplication {

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

複数の設定クラスをパッケージ単位で見つけたいならスキャン、対象をコードで列挙したいなら明示登録、と考えると選びやすくなります。この記事のコンストラクタ束縛の設定クラスには、@Componentを追加しません。

利用側のコンストラクタで設定オブジェクトを受け取る

登録した設定オブジェクトは、利用するServiceのコンストラクタで受け取ります。
NotificationService.javaでは、値を参照する部分だけを示します。通知の送信処理は含みません。

package com.example.demo.service;

import java.time.Duration;

import com.example.demo.config.NotificationProperties;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    private final NotificationProperties properties;

    public NotificationService(NotificationProperties properties) {
        this.properties = properties;
    }

    public String senderName() {
        return properties.senderName();
    }

    public Duration timeout() {
        return properties.connection().timeout();
    }
}

利用側はapp.notification.connection.timeoutという文字列を知らなくても、Durationとして値を扱えます。ただし、現段階では必須性を検証していないため、設定の欠落によるnullには備えられていません。後の検証例で制約を追加します。

Beanやコンストラクタインジェクションの仕組みを確認したい場合は、次の記事で説明しています。

階層構造と型を持つ設定値を扱う

ネストした設定とリストをJava側に対応させる

YAMLのconnection配下は、Java側のConnectionという入れ子の型に対応します。
connection.timeoutを読むときは、properties.connection().timeout()と順にたどります。
内側のConnectionを独立したBeanとして登録する必要はありません。

channelsのような複数の値はList<String>で受け取れます。型の宣言によって「文字列を複数持つ設定」だと分かりますが、文字列なら何でも適切とは限りません。

たとえば利用可能な通知方式だけを許可したい場合は、列挙型や追加の制約を検討します。

設定キーの表記とDurationなどの型変換を確認する

@ConfigurationPropertiesには、設定キーとJava側の名前の表記差を吸収するrelaxed bindingがあります。主例では、設定キーをsender-name、Java側をsenderNameとして対応付けています。設定ファイルのキーは、小文字をハイフンで区切るケバブケースにそろえると読みやすくなります。

タイムアウトにはDurationを使い、YAMLでは2sと単位を付けています。500msなら500ミリ秒、2sなら2秒という意図を表せます。ISO-8601形式のPT2Sも表現方法の一つです。

単位なしの数値は、Durationへの束縛では標準でミリ秒として扱われますが、@DurationUnitによって変更できます。設定を読む人が単位を推測しなくて済むよう、この記事の例では単位を明記しています。

@Valueとの違いと適切な使い分け

単独の値か、関連する設定のまとまりかで選ぶ

@Valueは、@Value("${app.notification.sender-name}")のように、個別の値を注入したいときに使えます。利用する設定が一つだけなら、短く書ける点が利点です。

一方、送信元、再試行回数、通知方式、タイムアウトのように関連する項目が増えるなら、@ConfigurationPropertiesで一つの型にまとめると、設定の全体像と制約を同じ場所で確認できます。

型変換・階層構造・検証・SpELの観点で比較する

観点@ConfigurationProperties@Value
主な用途関連する設定をオブジェクトにまとめる個別の値や式の結果を注入する
型変換Binderによって指定した型へ束縛する注入先の型への変換も可能
階層構造入れ子のオブジェクトやリストへ対応付ける個々のキーを指定する形が中心
検証@ValidatedとBean Validationを組み合わせる同じ設定束縛時の検証が自動で適用されるわけではない
SpEL設定値をSpELとして評価する用途には使わない#{…}でSpELを利用できる

「@Valueは文字列しか扱えない」という理解は誤りです。数値などへの型変換は@Valueでも行えます。ただし、両者で変換の仕組みや対応する表現がすべて同じとは限りません。たとえば主例の2sという値を、そのまま別の注入方式へ移して同じ結果になるとは判断しないでください。

選ぶ基準は、型変換の有無だけでなく、関連項目をまとめたいか、階層構造や制約を一か所で管理したいかです。

@ValidatedとJakarta Bean Validationで設定を検証する

検証に必要な依存関係とアノテーションをそろえる

設定の検証には、制約アノテーションに加えてBean Validationの実装が必要です。Mavenでは、Spring BootのparentまたはBOMで3.5系の依存関係管理を設定したうえで、pom.xmlのdependenciesに次を追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

設定クラスにはSpringの@Validatedを付け、各項目にはjakarta.validation配下の制約を付けます。Spring Boot 3系の例として、javax.validationのimportと混在させないようにします。

必須値・数値範囲・ネストした設定の制約を定義する

先ほどのNotificationProperties.javaを次の内容に置き換えます。YAML、登録方法、Serviceはそのまま使う構成です。

package com.example.demo.config;

import java.time.Duration;
import java.util.List;

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

@Validated
@ConfigurationProperties(prefix = "app.notification")
public record NotificationProperties(
        @NotBlank String senderName,
        @NotNull @Min(0) @Max(5) Integer retryCount,
        @NotEmpty List<@NotBlank String> channels,
        @NotNull @Valid Connection connection) {

    public record Connection(@NotNull Duration timeout) {
    }
}

制約は、値の種類と要件に合わせて選びます。この例では次の役割を持たせています。

  • @NotBlank:送信元の名前に、null・空文字・空白だけの文字列を認めない。
  • @NotNull @Min(0) @Max(5):再試行回数を必須にし、0〜5の範囲に制限する。
  • @NotEmpty:通知方式のリストにnullと空リストを認めない。要素の@NotBlankで各文字列も検証する。
  • @NotNull @Valid:接続設定を必須にし、その内部の制約も検証する。

@Minと@Maxはnullを不正値として扱わないため、必須にするなら@NotNullも必要です。また、@Validは入れ子のオブジェクトへ検証を進めるための指定であり、そのオブジェクトが存在することまでは要求しません。主例ではconnectionの欠落を@NotNullで検出します。

timeoutに付けた@NotNullが保証するのは値の存在だけです。正の時間であることや上限まで求める場合は、Durationに対応した追加の検証が必要です。数値向けの@MinをそのままDurationに付けることはできません。

型不一致・未登録・未検証を確認する

設定を読み込めないときは、値の変換、Bean登録、制約の検証を分けて確認します。

型不一致:設定値の形式とJava側の型を照合する

まず、問題になった設定キーと、その受け取り先の型を対応させます。たとえばretry-count: manyはIntegerへの変換の問題です。一方、retry-count: -1は整数に変換した後の範囲検証の問題になります。

Durationなら値の単位や形式、リストならYAMLのインデントと要素の型も確認します。修正したファイルと実際に読み込まれた設定が違う可能性がある場合は、設定の供給元の確認へ進みます。

profilesや上書きの優先順位、秘密情報の扱いについては、次の記事を参照してください。

未登録:登録アノテーションとスキャン範囲を確認する

設定オブジェクトをコンストラクタで受け取れない場合は、@ConfigurationPropertiesの有無だけでなく、登録経路を確認します。

  • @ConfigurationPropertiesScanを付けたクラスが読み込まれ、設定クラスが探索範囲に入っているか。
  • 明示登録なら、@EnableConfigurationProperties(NotificationProperties.class)を指定しているか。
  • 利用側のServiceもSpring管理の対象になっているか。
  • 設定クラスを自分でnewして、Spring Bootの束縛を経由せずに使っていないか。

Beanが存在するのに値が入っていない場合は、prefixやキーの綴り、設定の読み込み元を確認します。Bean未登録と、登録されたBeanへの設定値の未束縛は、分けて調べると原因を絞れます。

未検証:検証実装・@Validated・制約・@Validを確認する

不正な値を入れても制約違反として扱われない場合は、次の順で確認します。

  1. spring-boot-starter-validationなどによって、実行時に検証実装が利用できるか。
  2. Spring Bootが登録・束縛する設定クラスに@Validatedが付いているか。
  3. 対象項目に、型と要件に合うjakarta.validationの制約が付いているか。
  4. 入れ子の内部を検証する箇所に@Validがあり、必須なら@NotNullも付いているか。

確認用の入力は、一つずつ変えると判断しやすくなります。主例なら、retry-countを範囲外にする、sender-nameを空文字にする、connection全体を省略する、といったケースを個別に確認します。

まとめ

@ConfigurationPropertiesを使うと、関連する設定を一つの型にまとめ、入れ子の構造やリストもJava側で表現できます。基本の流れは、設定クラスを定義し、スキャンまたは明示指定で登録し、利用側のコンストラクタで受け取ることです。

個別の値を注入する@Valueにも型変換はあります。関連する設定の構造と制約をまとめて管理したいかどうかを、使い分けの軸にしましょう。

必須値や範囲まで保証するには、検証実装、@Validated、各項目の制約をそろえます。入れ子では@Validと必須性の指定を区別し、不具合があれば型変換・登録・検証のどの段階に問題があるかを確認してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次