4.1. 入力チェック

目次

4.1.1. Overview

ユーザーが入力した値が不正かどうかを検証することは必須である。 入力値の検証は大きく分けて、

  1. 長さや形式など、文脈によらず入力値だけを見て、それが妥当かどうかを判定できる検証
  2. システムの状態によって入力値が妥当かどうかが変わる検証

がある。

1.の例としては必須チェックや、桁数チェックがあり、2.の例としては 登録済みのE-mailかどうかのチェックや、注文数が在庫数以内であるかどうかのチェックが挙げられる。

本節では、基本的には前者のことを説明し、このチェックのことを「入力チェック」を呼ぶ。 後者のチェックは「業務ロジックチェック」と呼ぶ。業務ロジックチェックについては ドメイン層の実装を参照されたい。

本ガイドラインでは、基本的に入力チェックをアプリケーション層で行い、 業務ロジックチェックは、ドメイン層で行うことをポリシーとする。

Webアプリケーションの入力チェックには、サーバサイドで行うチェックと、クライアントサイド(JavaScript)で行うチェックがある。 サーバーサイドのチェックは必須であるが、クライアントサイドでも同じチェックを実施すると、 サーバー通信なしでチェック結果が分かるため、ユーザビリティが向上する。

Warning

JavaScriptによるクライアントサイドの処理は、改ざん可能であるため、サーバーサイドのチェックは、必ず行うこと。 クライアントサイドのみでチェックを行い、サーバーサイドでチェックを省略した場合は、システムが危険な状態に晒されていることになる。

4.1.1.1. 入力チェックの分類

入力チェックは、単項目チェック、相関項目チェックに分類される。

種類 説明 実現方法
単項目チェック
単一のフィールドで完結するチェック
入力必須チェック
桁チェック
型チェック
Bean Validation (実装ライブラリとしてHibernate Validatorを使用)
相関項目チェック
複数のフィールドを比較するチェック
パスワードと確認用パスワードの一致チェック
org.springframework.validation.Validatorインタフェースを実装したValidationクラス
または Bean Validation

Spring は、Java標準であるBean Validationをサポートしている。 単項目チェックには、このBean Validationを利用する。 相関項目チェックの場合は、Bean ValidationまたはSpringが提供しているorg.springframework.validation.Validatorインタフェースを利用する。

4.1.2. How to use

4.1.2.1. 依存ライブラリの追加

Bean Validation 3.0(Hibernate Validator 8.x)以上を使用する場合、Bean ValidationのAPI仕様クラス(jakarta.validationパッケージのクラス)が格納されているjarファイルとHibernate Validatorのjarファイルに加えて、

  • Expression Language 5.0以上のAPI仕様クラス (jakarta.elパッケージのクラス)
  • Expression Language 5.0以上のリファレンス実装クラス

が格納されているライブラリが必要となる。

アプリケーションサーバにデプロイして動かす場合は、 これらのライブラリはアプリケーションサーバから提供されているため、 依存ライブラリの追加は不要である。 ただし、スタンドアロン環境(JUnitなど)で動かす場合は、これらのライブラリを依存ライブラリとして追加する必要がある。

スタンドアロン環境でBean Validation 3.0以上を動かす際に必要となるライブラリの追加例を以下に示す。

<!-- (1) -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <scope>test</scope> <!-- (2) -->
</dependency>
項番 説明
(1)

スタンドアロン環境で動かすプロジェクトの pom.xml ファイルに、 Expression Language用のクラスが格納されているライブラリを追加する。

上記例では、組込み用のApache Tomcat向けに提供されているライブラリを指定している。 tomcat-embed-elのjarファイルには、Expression LanguageのAPI仕様クラスとリファレンス実装クラスの両方が格納されている。

(2)
JUnitを実行するために依存ライブラリが必要になる場合は、スコープは testが適切である。

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

4.1.2.2. 単項目チェック

単項目チェックを実装するには、

  • フォームクラスのフィールドに、Bean Validation用のアノテーションを付与する
  • Controllerに、検証するための@Validatedアノテーションを付与する
  • JSPに、検証エラーメッセージを表示するためのタグを追加する

が必要である。

Note

spring-mvc.xmlに<mvc:annotation-driven>の設定が行われていれば、Bean Validationは有効になる。

4.1.2.2.1. 基本的な単項目チェック

「新規ユーザー登録」処理を例に用いて、実装方法を説明する。ここでは「新規ユーザー登録」のフォームに、以下のチェックルールを設ける。

フィールド名 ルール
name
java.lang.String
入力必須
1文字以上
20文字以下
email
java.lang.String
入力必須
1文字以上
50文字以下
E-mail形式
age
java.lang.Integer
入力必須
1以上
200以下
  • フォームクラス

    フォームクラスの各フィールドに、Bean Validationのアノテーションを付ける。

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    public class UserForm implements Serializable {
    
      private static final long serialVersionUID = 1L;
    
      @NotNull // (1)
      @Size(min = 1, max = 20) // (2)
      private String name;
    
      @NotNull
      @Size(min = 1, max = 50)
      @Email // (3)
      private String email;
    
      @NotNull // (4)
      @Min(0) // (5)
      @Max(200) // (6)
      private Integer age;
    
      // omitted setter/getter
    }
    
    項番 説明
    (1)
    対象のフィールドがnullでないことを示すjakarta.validation.constraints.NotNullを付ける。

    Spring MVCでは、文字列の入力フィールドに未入力の状態でフォームを送信した場合、
    デフォルトではフォームオブジェクトにnullではなく、空文字がバインドされる
    この@NotNullは、そもそもリクエストパラメータとしてnameが存在することをチェックする。
    (2)
    対象のフィールドの文字列長(またはコレクションのサイズ)が指定したサイズの範囲内にあることを示すjakarta.validation.constraints.Sizeを付ける。

    上記の通り、Spring MVCではデフォルトで、未入力の文字列フィールドには、空文字がバインドされるため、
    1文字以上というルールが入力必須を表す。
    (3)
    対象のフィールドがE-mail形式であることを示すjakarta.validation.constraints.Emailを付ける。

    E-mail形式の要件が@Email のチェックと合致しない場合は、jakarta.validation.constraints.Patternを用いて、正規表現を指定する必要がある。
    @Email については、Bean Validationのチェックルールを参照されたい。
    (4)
    数値の入力フィールドに未入力の状態でフォームを送信した場合、フォームオブジェクトにnull がバインドされるため、@NotNullageの入力必須条件を表す。
    (5)
    対象のフィールドが指定した数値の以上であることを示すjakarta.validation.constraints.Minを付ける。
    (6)
    対象のフィールドが指定した数値の以下であることを示すjakarta.validation.constraints.Maxを付ける。

    Tip

    Bean Validation標準のアノテーション、Hibernate Validationが用意しているアノテーションについては、Bean ValidationのチェックルールHibernate Validatorのチェックルールを参照されたい。

    Tip

    入力フィールドが未入力の場合に、空文字ではなくnullにバインドする方法に関しては、文字列フィールドが未入力の場合にnullをバインドするを参照されたい。

  • Controllerクラス

    入力チェック対象のフォームクラスに、@Validatedを付ける。

    package com.example.sample.app.validation;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("user")
    public class UserController {
    
      @ModelAttribute
      public UserForm setupForm() {
        return new UserForm();
      }
    
      @GetMapping(value = "create", params = "form")
      public String createForm() {
        return "user/createForm"; // (1)
      }
    
      @PostMapping(value = "create", params = "confirm")
      public String createConfirm(@Validated /* (2) */ UserForm form, BindingResult /* (3) */ result) {
        if (result.hasErrors()) { // (4)
          return "user/createForm";
        }
        return "user/createConfirm";
      }
    
      @PostMapping(value = "create")
      public String create(@Validated UserForm form, BindingResult result) { // (5)
        if (result.hasErrors()) {
          return "user/createForm";
        }
        // omitted business logic
        return "redirect:/user/create?complete";
      }
    
      @GetMapping(value = "create", params = "complete")
      public String createComplete() {
        return "user/createComplete";
      }
    }
    
    項番 説明
    (1)
    「新規ユーザー登録」フォーム画面を表示する。
    (2)
    フォームにつけたアノテーションで入力チェックをするために、フォームの引数に org.springframework.validation.annotation.Validatedを付ける。
    (3)
    (2)のチェック結果を格納するorg.springframework.validation.BindingResultを、引数に加える。
    このBindingResultは、フォームの直後に記述する必要がある。

    直後に指定されていない場合は、検証後に結果をバインドできず、org.springframework.validation.BindExceptionがスローされる。
    (4)
    (2)のチェック結果は、BindingResult.hasErrors()メソッドで判定できる。
    hasErrors()の結果がtrueの場合は、入力値に問題があるため、フォーム表示画面に戻す。
    (5)
    入力内容確認画面から新規作成処理にリクエストを送る際にも、入力チェックを必ず再実行すること
    途中でデータを改ざんすることは可能であるため、必ず業務処理の直前で入力チェックは必要である。

    @Validatedは、Bean Validation標準ではなくSpringの独自アノテーションである。

    Bean Validation標準のjakarta.validation.Validアノテーションも使用できるが、@Validated@Validに比べてバリデーションのグループを指定できる点で優れているため、本ガイドラインではControllerの引数には@Validatedを使用することを推奨する。

  • JSP

    <form:errors>タグで、入力エラーがある場合にエラーメッセージを表示できる。

    <!DOCTYPE html>
    <html>
    <%-- WEB-INF/views/user/createForm.jsp --%>
    <body>
        <form:form modelAttribute="userForm" method="post"
            action="${pageContext.request.contextPath}/user/create">
            <form:label path="name">Name:</form:label>
            <form:input path="name" />
            <form:errors path="name" /><%--(1) --%>
            <br>
            <form:label path="email">Email:</form:label>
            <form:input path="email" />
            <form:errors path="email" />
            <br>
            <form:label path="age">Age:</form:label>
            <form:input path="age" />
            <form:errors path="age" />
            <br>
            <form:button name="confirm">Confirm</form:button>
        </form:form>
    </body>
    </html>
    
    項番 説明
    (1)
    <form:errors>タグのpath属性に、対象のフィールド名を指定する。
    この例では、フィールド毎に入力フィールドの横にエラーメッセージを表示する。

フォームは、以下のように表示される。

../../_images/validations-first-sample1.png

このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。

../../_images/validations-first-sample2.png

NameとEmailが空文字であることに対するエラーメッセージと、Ageがnullであることに対するエラーメッセージが表示されている。

Note

Bean Validationでは、通常、入力値がnullの場合は正常な値とみなす。ただし、 以下のアノテーションを除く。

  • jakarta.validation.constraints.NotNull
  • jakarta.validation.constraints.NotEmpty
  • jakarta.validation.constraints.NotBlank

上記の例では、Ageの値はnullであるため、@Min@Maxによるチェックは正常とみなされ、 エラーメッセージは出力されていない。

次に、フィールドに何らかの値を入力してフォームを送信する。

../../_images/validations-first-sample3.png
Nameの入力値は、チェック条件を満たすため、エラーメッセージが表示されない。
E-mailの入力値は文字列長に関する条件は満たすが、E-mail形式ではないため、エラーメッセージが表示される。
Ageの入力値は最大値を超えているため、エラーメッセージが表示される。

エラー時にスタイルを変更したい場合は、前述のフォームを、以下のように変更する。

<form:form modelAttribute="userForm" method="post"
    class="form-horizontal"
    action="${pageContext.request.contextPath}/user/create">
    <form:label path="name" cssErrorClass="error-label">Name:</form:label><%-- (1) --%>
    <form:input path="name" cssErrorClass="error-input" /><%-- (2) --%>
    <form:errors path="name" cssClass="error-messages" /><%-- (3) --%>
    <br>
    <form:label path="email" cssErrorClass="error-label">Email:</form:label>
    <form:input path="email" cssErrorClass="error-input" />
    <form:errors path="email" cssClass="error-messages" />
    <br>
    <form:label path="age" cssErrorClass="error-label">Age:</form:label>
    <form:input path="age" cssErrorClass="error-input" />
    <form:errors path="age" cssClass="error-messages" />
    <br>
    <form:button name="confirm">Confirm</form:button>
</form:form>
項番 説明
(1)
エラー時に<label>タグへ加えるクラス名を、cssErrorClass属性で指定する。
(2)
エラー時に<input>タグへ加えるクラス名を、cssErrorClass属性で指定する。
(3)
エラーメッセージに加えるクラス名を、cssClass属性で指定する。

このJSPに対して、例えば以下のCSSを適用すると、

.form-horizontal input {
    display: block;
    float: left;
}

.form-horizontal label {
    display: block;
    float: left;
    text-align: right;
    float: left;
}

.form-horizontal br {
    clear: left;
}

.error-label {
    color: #b94a48;
}

.error-input {
    border-color: #b94a48;
    margin-left: 5px;
}

.error-messages {
    color: #b94a48;
    display: block;
    padding-left: 5px;
    overflow-x: auto;
}

エラー画面は、以下のように表示される。

../../_images/validations-has-errors1.png

画面の要件に応じてCSSをカスタマイズすればよい。

エラーメッセージを、入力フィールドの横に一件一件出力する代わりに、 まとめて出力することもできる。

<form:form modelAttribute="userForm" method="post"
    action="${pageContext.request.contextPath}/user/create">
    <form:errors path="*" element="div" cssClass="error-message-list" /><%-- (1) --%>

    <form:label path="name" cssErrorClass="error-label">Name:</form:label>
    <form:input path="name" cssErrorClass="error-input" />
    <br>
    <form:label path="email" cssErrorClass="error-label">Email:</form:label>
    <form:input path="email" cssErrorClass="error-input" />
    <br>
    <form:label path="age" cssErrorClass="error-label">Age:</form:label>
    <form:input path="age" cssErrorClass="error-input" />
    <br>
    <form:button name="confirm">Confirm</form:button>
</form:form>
項番 説明
(1)
<form:form>タグ内で、<form:errors>path属性に”*” を指定することで、
<form:form>modelAttribute属性に指定したModelに関する全エラーメッセージを出力できる。
element属性に、これらのエラーメッセージを包含するタグ名を指定できる。デフォルトでは、spanであるが、
ここではエラーメッセージ一覧をブロック要素として出力するために、divを指定する。
また、CSSのクラスを cssClass属性に指定する。

例として、以下のCSSクラスを適用した場合の、エラーメッセージ出力例を示す。

.form-horizontal input {
    display: block;
    float: left;
}

.form-horizontal label {
    display: block;
    float: left;
    text-align: right;
    float: left;
}

.form-horizontal br {
    clear: left;
}

.error-label {
    color: #b94a48;
}

.error-input {
    border-color: #b94a48;
    margin-left: 5px;
}

.error-message-list {
    color: #b94a48;
    padding:5px 10px;
    background-color: #fde9f3;
    border:1px solid #c98186;
    border-radius:5px;
    margin-bottom: 10px;
}
../../_images/validations-has-errors2.png
デフォルトでは、エラーメッセージにフィールド名は含まれず、どのフィールドのエラーメッセージなのかが分かりにくい。
そのため、エラーメッセージを一覧で表示する場合は、エラーメッセージの中にフィールド名を含めるようにメッセージを定義する必要がある。
エラーメッセージの定義方法については、「エラーメッセージの定義」を参照されたい。

Note

エラーメッセージを一覧で表示する際の注意点

エラーメッセージの出力順序は順不同であり、標準機能で出力順序を制御することはできない。 そのため、出力順序を制御する(一定に保つ)必要がある場合は、エラー情報をソートするなどの拡張実装が必要となる。

「エラーメッセージを一覧で表示する」方式では、

  • フィールド単位のエラーメッセージ定義
  • エラーメッセージの出力順序を制御するための拡張実装

が必要となるため、「入力フィールドの横にエラーメッセージを表示する」方式に比べて対応コストが高くなる。 本ガイドラインでは、画面要件による制約がない場合は「入力フィールドの横にエラーメッセージを表示する」方式を推奨する。

なお、エラーメッセージの出力順序を制御するための拡張方法としては、 Spring Frameworkから提供されているorg.springframework.validation.beanvalidation.LocalValidatorFactoryBeanの継承クラスを作成し、 processConstraintViolationsメソッドをオーバーライドしてエラー情報をソートする方法などが考えられる。

Note

@GroupSequenceアノテーションについて

チェック順番を制御するための仕組みとして@GroupSequenceアノテーションが提供されているが、この仕組みは以下のような動作になるため、エラーメッセージの出力順序を制御するための仕組みではないという点を補足しておく。

  • エラーが発生した場合に後続のグループのチェックが実行されない。
  • 同一グループ内のチェックで複数のエラー(複数の項目でエラー)が発生するとエラーメッセージの出力順序は順不同になる。

Note

エラーメッセージをまとめて表示する際に、<form:form>タグの外に表示したい場合は以下のように<spring:nestedPath>タグを使用する。

<spring:nestedPath path="userForm">
    <form:errors path="*" element="div"
        cssClass="error-message-list" />
</spring:nestedPath>
<hr>
<form:form modelAttribute="userForm" method="post"
    action="${pageContext.request.contextPath}/user/create">
    <form:label path="name" cssErrorClass="error-label">Name:</form:label>
    <form:input path="name" cssErrorClass="error-input" />
    <br>
    <form:label path="email" cssErrorClass="error-label">Email:</form:label>
    <form:input path="email" cssErrorClass="error-input" />
    <br>
    <form:label path="age" cssErrorClass="error-label">Age:</form:label>
    <form:input path="age" cssErrorClass="error-input" />
    <br>
    <form:button name="confirm">Confirm</form:button>
</form:form>

4.1.2.2.2. 日時フォーマットのチェック

日時フォーマットのチェックを行う場合には、Bean Validationの仕組みではなく、Springが提供する日時のフォーマットを指定する@DateTimeFormatアノテーションの使用を推奨する。
@DateTimeFormatアノテーションの使用方法については、フィールド単位の日時型変換を参照されたい。
Bean Validationの@Patternアノテーションを使用することでも日時フォーマットのチェックは可能である。
しかし、@Patternアノテーションを使用すると、日時フォーマットを正規表現で記述する必要があり、存在しない日時をチェックする場合には、記述が煩雑化する。
そのため、@Patternアノテーションよりも@DateTimeFormatアノテーションのほうが実装はシンプルになる。
@DateTimeFormatアノテーションはSpringが提供する型変換の仕組みのひとつであるので、入力エラーの場合には、Bean Validationのエラーメッセージではなく、型のミスマッチが発生した時にスローされる例外(TypeMismatchException)の例外メッセージがそのまま画面へ表示される。
例外メッセージが画面に表示されることを避けるため、型のミスマッチが発生した際のエラーメッセージをプロパティファイルに設定する必要がある。
詳細は型のミスマッチを参照されたい。

4.1.2.2.3. ネストしたBeanの単項目チェック

ネストしたBeanをBean Validationで検証する方法を説明する。

ECサイトにおける「注文」処理の例を考える。「注文」フォームでは、以下のチェックルールを設ける。

フィールド名 ルール 説明
coupon
java.lang.String
5文字以下
半角英数字
クーポンコード
receiverAddress.name
java.lang.String
入力必須
1文字以上
50文字以下
お届け先氏名
receiverAddress.postcode
java.lang.String
入力必須
1文字以上
10文字以下
お届け先郵便番号
receiverAddress.address
java.lang.String
入力必須
1文字以上
100文字以下
お届け先住所
senderAddress.name
java.lang.String
入力必須
1文字以上
50文字以下
請求先氏名
senderAddress.postcode
java.lang.String
入力必須
1文字以上
10文字以下
請求先郵便番号
senderAddress.address
java.lang.String
入力必須
1文字以上
100文字以下
請求先住所

receiverAddresssenderAddressは、同じ項目であるため、同じフォームクラスを使用する。

  • フォームクラス

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import jakarta.validation.Valid;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Pattern;
    import jakarta.validation.constraints.Size;
    
    public class OrderForm implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @Size(max = 5)
        @Pattern(regexp = "[a-zA-Z0-9]*")
        private String coupon;
    
        @NotNull // (1)
        @Valid // (2)
        private AddressForm receiverAddress;
    
        @NotNull
        @Valid
        private AddressForm senderAddress;
    
        // omitted setter/getter
    }
    
    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    public class AddressForm implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @NotNull
        @Size(min = 1, max = 50)
        private String name;
    
        @NotNull
        @Size(min = 1, max = 10)
        private String postcode;
    
        @NotNull
        @Size(min = 1, max = 100)
        private String address;
    
        // omitted setter/getter
    }
    
    項番 説明
    (1)
    子フォーム自体が必須であることを示す。
    この設定がない場合、receiverAddressnullが設定されても、正常とみなされる。
    (2)
    ネストしたBeanのBean Validationを有効にするために、jakarta.validation.Validアノテーションを付与する。
  • Controllerクラス

    前述のControllerと違いはない。

    package com.example.sample.app.validation;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @RequestMapping("order")
    @Controller
    public class OrderController {
    
        @ModelAttribute
        public OrderForm setupForm() {
            return new OrderForm();
        }
    
        @GetMapping(value = "order", params = "form")
        public String orderForm() {
            return "order/orderForm";
        }
    
        @PostMapping(value = "order", params = "confirm")
        public String orderConfirm(@Validated OrderForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "order/orderForm";
            }
            return "order/orderConfirm";
        }
    }
    
  • JSP

    <!DOCTYPE html>
    <html>
    <%-- WEB-INF/views/order/orderForm.jsp --%>
    <head>
    <style type="text/css">
      /* omitted (same as previous sample) */
    </style>
    </head>
    <body>
        <form:form modelAttribute="orderForm" method="post"
            class="form-horizontal"
            action="${pageContext.request.contextPath}/order/order">
            <form:label path="coupon" cssErrorClass="error-label">Coupon Code:</form:label>
            <form:input path="coupon" cssErrorClass="error-input" />
            <form:errors path="coupon" cssClass="error-messages" />
            <br>
        <fieldset>
            <legend>Receiver</legend>
            <%-- (1) --%>
            <form:errors path="receiverAddress"
                cssClass="error-messages" />
            <%-- (2) --%>
            <form:label path="receiverAddress.name"
                cssErrorClass="error-label">Name:</form:label>
            <form:input path="receiverAddress.name"
                cssErrorClass="error-input" />
            <form:errors path="receiverAddress.name"
                cssClass="error-messages" />
            <br>
            <form:label path="receiverAddress.postcode"
                cssErrorClass="error-label">Postcode:</form:label>
            <form:input path="receiverAddress.postcode"
                cssErrorClass="error-input" />
            <form:errors path="receiverAddress.postcode"
                cssClass="error-messages" />
            <br>
            <form:label path="receiverAddress.address"
                cssErrorClass="error-label">Address:</form:label>
            <form:input path="receiverAddress.address"
                cssErrorClass="error-input" />
            <form:errors path="receiverAddress.address"
                cssClass="error-messages" />
        </fieldset>
        <br>
        <fieldset>
            <legend>Sender</legend>
            <form:errors path="senderAddress"
                cssClass="error-messages" />
            <form:label path="senderAddress.name"
                cssErrorClass="error-label">Name:</form:label>
            <form:input path="senderAddress.name"
                cssErrorClass="error-input" />
            <form:errors path="senderAddress.name"
                cssClass="error-messages" />
            <br>
            <form:label path="senderAddress.postcode"
                cssErrorClass="error-label">Postcode:</form:label>
            <form:input path="senderAddress.postcode"
                cssErrorClass="error-input" />
            <form:errors path="senderAddress.postcode"
                cssClass="error-messages" />
            <br>
            <form:label path="senderAddress.address"
                cssErrorClass="error-label">Address:</form:label>
            <form:input path="senderAddress.address"
                cssErrorClass="error-input" />
            <form:errors path="senderAddress.address"
                cssClass="error-messages" />
        </fieldset>
    
            <form:button name="confirm">Confirm</form:button>
        </form:form>
    </body>
    </html>
    
    項番 説明
    (1)
    不正な操作により、receiverAddress.namereceiverAddress.postcodereceiverAddress.addressのすべて
    がリクエストパラメータとして送信されない場合、receiverAddressnullとみなされ、この位置にエラーメッセージが表示される。
    (2)
    子フォームのフィールドは、親フィールド名.子フィールド名で指定する。

フォームは、以下のように表示される。

../../_images/validations-nested1.png

このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。

../../_images/validations-nested2.png

ネストしたBeanのバリデーションはコレクションに対しても有効である。

最初に説明した「ユーザー登録」フォームに住所を3件まで登録できるようにフィールドを追加する。 住所には、前述のAddressFormを利用する。

  • フォームクラス AddressFormのリストを、フィールドに追加する。

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    import java.util.List;
    
    import jakarta.validation.Valid;
    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    public class UserForm implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        @NotNull
        @Size(min = 1, max = 20)
        private String name;
    
        @NotNull
        @Size(min = 1, max = 50)
        @Email
        private String email;
    
        @NotNull
        @Min(0)
        @Max(200)
        private Integer age;
    
        @NotNull
        @Size(min = 1, max = 3) // (1)
        @Valid
        private List<AddressForm> addresses;
    
        // omitted setter/getter
    }
    
    項番 説明
    (1)
    コレクションのサイズチェックにも、@Sizeアノテーションを使用できる。
  • JSP

    <!DOCTYPE html>
    <html>
    <%-- WEB-INF/views/user/createForm.jsp --%>
    <head>
    <style type="text/css">
      /* omitted (same as previous sample) */
    </style>
    </head>
    <body>
    
        <form:form modelAttribute="userForm" method="post"
            class="form-horizontal"
            action="${pageContext.request.contextPath}/user/create">
            <form:label path="name" cssErrorClass="error-label">Name:</form:label>
            <form:input path="name" cssErrorClass="error-input" />
            <form:errors path="name" cssClass="error-messages" />
            <br>
            <form:label path="email" cssErrorClass="error-label">Email:</form:label>
            <form:input path="email" cssErrorClass="error-input" />
            <form:errors path="email" cssClass="error-messages" />
            <br>
            <form:label path="age" cssErrorClass="error-label">Age:</form:label>
            <form:input path="age" cssErrorClass="error-input" />
            <form:errors path="age" cssClass="error-messages" />
            <br>
            <form:errors path="addresses" cssClass="error-messages" /><%-- (1) --%>
            <c:forEach items="${userForm.addresses}" varStatus="status"><%-- (2) --%>
                <fieldset class="address">
                    <legend>Address${f:h(status.index + 1)}</legend>
                    <form:label path="addresses[${status.index}].name"
                        cssErrorClass="error-label">Name:</form:label><%-- (3) --%>
                    <form:input path="addresses[${status.index}].name"
                        cssErrorClass="error-input" />
                    <form:errors path="addresses[${status.index}].name"
                        cssClass="error-messages" />
                    <br>
                    <form:label path="addresses[${status.index}].postcode"
                        cssErrorClass="error-label">Postcode:</form:label>
                    <form:input path="addresses[${status.index}].postcode"
                        cssErrorClass="error-input" />
                    <form:errors path="addresses[${status.index}].postcode"
                        cssClass="error-messages" />
                    <br>
                    <form:label path="addresses[${status.index}].address"
                        cssErrorClass="error-label">Address:</form:label>
                    <form:input path="addresses[${status.index}].address"
                        cssErrorClass="error-input" />
                    <form:errors path="addresses[${status.index}].address"
                        cssClass="error-messages" />
                    <c:if test="${status.index > 0}">
                        <br>
                        <button class="remove-address-button">Remove</button>
                    </c:if>
                </fieldset>
                <br>
            </c:forEach>
            <button id="add-address-button">Add address</button>
            <br>
            <form:button name="confirm">Confirm</form:button>
        </form:form>
        <script type="text/javascript"
            src="${pageContext.request.contextPath}/resources/vendor/js/jquery-1.10.2.min.js"></script>
        <script type="text/javascript"
            src="${pageContext.request.contextPath}/resources/app/js/AddressesView.js"></script>
    </body>
    </html>
    
    項番 説明
    (1)
    addressesフィールドに対するエラーメッセージを表示する。
    (2)
    子フォームのコレクションを、<c:forEach>タグを使ってループで処理する。
    (3)
    コレクション中の子フォームのフィールドは、親フィールド名[インデックス].子フィールド名で指定する。
  • Controllerクラス

    package com.example.sample.app.validation;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("user")
    public class UserController {
    
        @ModelAttribute
        public UserForm setupForm() {
            UserForm form = new UserForm();
            List<AddressForm> addresses = new ArrayList<AddressForm>();
            addresses.add(new AddressForm());
            form.setAddresses(addresses); // (1)
            return form;
        }
    
        @GetMapping(value = "create", params = "form")
        public String createForm() {
            return "user/createForm";
        }
    
        @PostMapping(value = "create", params = "confirm")
        public String createConfirm(@Validated UserForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "user/createForm";
            }
            return "user/createConfirm";
        }
    }
    
    項番 説明
    (1)
    「ユーザー登録」フォーム初期表示時に、一件の住所フォームを表示させるために、フォームオブジェクトを編集する。
  • JavaScript

    動的にアドレス入力フィールドを追加するためのJavaScriptも記載するが、このコードの説明は、本質的ではないため割愛する。

    // webapp/resources/app/js/AddressesView.js
    
    function AddressesView() {
      this.addressSize = $('fieldset.address').size();
    };
    
    AddressesView.prototype.addAddress = function() {
      var $address = $('fieldset.address');
      var newHtml = addressTemplate(this.addressSize++);
      $address.last().next().after($(newHtml));
    };
    
    AddressesView.prototype.removeAddress = function($fieldset) {
      $fieldset.next().remove(); // remove <br>
      $fieldset.remove(); // remove <fieldset>
    };
    
    function addressTemplate(number) {
      return '\
    <fieldset class="address">\
        <legend>Address' + (number + 1) + '</legend>\
        <label for="addresses' + number + '.name">Name:</label>\
        <input id="addresses' + number + '.name" name="addresses[' + number + '].name" type="text" value=""/><br>\
        <label for="addresses' + number + '.postcode">Postcode:</label>\
        <input id="addresses' + number + '.postcode" name="addresses[' + number + '].postcode" type="text" value=""/><br>\
        <label for="addresses' + number + '.address">Address:</label>\
        <input id="addresses' + number + '.address" name="addresses[' + number + '].address" type="text" value=""/><br>\
        <button class="remove-address-button">Remove</button>\
    </fieldset>\
    <br>\
    ';
    }
    
    $(function() {
      var addressesView = new AddressesView();
    
      $('#add-address-button').on('click', function(e) {
        e.preventDefault();
        addressesView.addAddress();
      });
    
      $(document).on('click', '.remove-address-button', function(e) {
        if (this === e.target) {
          e.preventDefault();
          var $this = $(this); // this button
          var $fieldset = $this.parent(); // fieldset
          addressesView.removeAddress($fieldset);
        }
      });
    
    });
    

フォームは、以下のように表示される。

../../_images/validations-nested-collection1.png

「Add address」ボタンを2回押して、住所フォームを2件追加する。

../../_images/validations-nested-collection2.png

このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。

../../_images/validations-nested-collection3.png

4.1.2.2.4. コレクション内の値のチェック

複数選択可能な画面項目(チェックボックスや複数選択ドロップダウンなど)を扱う際は、フォームクラスで画面項目を String等の基本型のコレクションとして扱うことが一般的である。

ここでは、Bean Validation 2.0の標準アノテーションである@NotEmpty及び共通ライブラリが提供する@ExistInCodeListを例に、コレクション内の値の入力チェックを行う例を示す。

  • フォームクラス

    package com.example.sample.app.validation;
    
    import java.util.List;
    
    import jakarta.validation.constraints.NotEmpty;
    
    import org.terasoluna.gfw.common.codelist.ExistInCodeList;
    
    public class SampleForm {
        @NotEmpty
        private List<@NotEmpty @ExistInCodeList(codeListId = "CL_ROLE") String> roles; // (1)
    
        public List<String> getRoles() {
            return roles;
        }
    
        public void setRoles(List<String> roles) {
            this.roles = roles;
        }
    }
    
    項番 説明
    (1)
    入力チェック対象となるコレクションの型引数に対して@NotEmptyアノテーション及び@ExistInCodeListアノテーションを設定する。
    @ExistInCodeListアノテーションのcodeListIdパラメータにチェック元となるコードリストを指定する。

  • JSP

    <form:form modelAttribute="sampleForm">
        <!-- (1) -->
        <form:checkboxes path="roles" items="${CL_ROLE}"/>
        <form:errors path="roles*"/>
        <form:button>Submit</form:button>
    </form:form>
    
    項番 説明
    (1)
    <form:checkboxes>を実装する。

4.1.2.2.5. バリデーションのグループ化

バリデーショングループを作成し、一つのフィールドに対して、グループごとに入力チェックルールを指定することができる。

前述の「新規ユーザー登録」の例で、ageフィールドに「成年であること」というルールを追加する。 「成年かどうか」は国によってルールが違うため、countryフィールドも追加する。

Bean Validationでグループを指定する場合、アノテーションのgroup属性に、グループを示す任意のjava.lang.Classオブジェクトを設定する。

ここでは、以下の3グループ(interface)を作成する。

グループ 成人条件
Chinese 18歳以上
Japanese 20歳以上
Singaporean 21歳以上

このグループを使ってバリデーションを実行する例を示す。

  • フォームクラス

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    import java.util.List;
    
    import jakarta.validation.Valid;
    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    public class UserForm implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        // (1)
        public static interface Chinese {
        };
    
        public static interface Japanese {
        };
    
        public static interface Singaporean {
        };
    
        @NotNull
        @Size(min = 1, max = 20)
        private String name;
    
        @NotNull
        @Size(min = 1, max = 50)
        @Email
        private String email;
    
        @NotNull
        @Min(value = 18, groups = Chinese.class) // (2)
        @Min(value = 20, groups = Japanese.class)
        @Min(value = 21, groups = Singaporean.class)
        @Max(200)
        private Integer age;
    
        @NotNull
        @Size(min = 2, max = 2)
        private String country; // (3)
    
        // omitted setter/getter
    }
    
    項番 説明
    (1)
    グループクラスを指定するために、各グループをインタフェースで定義する。
    (2)
    グループごとにルールを定義する。グループを指定するために、groups属性に対象のグループクラスを指定する。
    groups属性を省略した場合、jakarta.validation.groups.Defaultグループが使用される。
    (3)
    グループを振り分けるための、フィールドを追加する。
  • JSP

    JSPに大きな変更はない。

    <form:form modelAttribute="userForm" method="post"
        class="form-horizontal"
        action="${pageContext.request.contextPath}/user/create">
        <form:label path="name" cssErrorClass="error-label">Name:</form:label>
        <form:input path="name" cssErrorClass="error-input" />
        <form:errors path="name" cssClass="error-messages" />
        <br>
        <form:label path="email" cssErrorClass="error-label">Email:</form:label>
        <form:input path="email" cssErrorClass="error-input" />
        <form:errors path="email" cssClass="error-messages" />
        <br>
        <form:label path="age" cssErrorClass="error-label">Age:</form:label>
        <form:input path="age" cssErrorClass="error-input" />
        <form:errors path="age" cssClass="error-messages" />
        <br>
        <form:label path="country" cssErrorClass="error-label">Country:</form:label>
        <form:select path="country" cssErrorClass="error-input">
            <form:option value="cn">China</form:option>
            <form:option value="jp">Japan</form:option>
            <form:option value="sg">Singapore</form:option>
        </form:select>
        <form:errors path="country" cssClass="error-messages" />
        <br>
        <form:button name="confirm">Confirm</form:button>
    </form:form>
    
  • Controllerクラス

    @Validatedに、対象のグループを設定することで、バリデーションルールを変更できる。

    package com.example.sample.app.validation;
    
    import jakarta.validation.groups.Default;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import com.example.sample.app.validation.UserForm.Chinese;
    import com.example.sample.app.validation.UserForm.Japanese;
    import com.example.sample.app.validation.UserForm.Singaporean;
    
    @Controller
    @RequestMapping("user")
    public class UserController {
    
        @ModelAttribute
        public UserForm setupForm() {
            UserForm form = new UserForm();
            return form;
        }
    
        @GetMapping(value = "create", params = "form")
        public String createForm() {
            return "user/createForm";
        }
    
        String createConfirm(UserForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "user/createForm";
            }
            return "user/createConfirm";
        }
    
        @PostMapping(value = "create", params = { "confirm", "country=cn" }) // (1)
        public String createConfirmForChinese(@Validated({Chinese.class, Default.class }) // (2)
                UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    
        @PostMapping(value = "create", params = { "confirm", "country=jp" })
        public String createConfirmForJapanese(@Validated({ Japanese.class,
                Default.class }) UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    
        @PostMapping(value = "create", params = { "confirm", "country=sg" })
        public String createConfirmForSingaporean(@Validated({ Singaporean.class,
                Default.class }) UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    }
    
    項番 説明
    (1)
    グループを振り分けるためのパラメータの条件を、param属性に追加する。
    (2)
    @Min以外のアノテーションは、Defaultグループに属しているため、Defaultの指定も必要である。

この例では、各入力値の組み合わせに対するチェック結果は、以下の表の通りである。

ageの値 countryの値 入力チェック結果 エラーメッセージ
17
cn
NG
must be greater than or equal to 18

jp
NG
must be greater than or equal to 20

sg
NG
must be greater than or equal to 21
18
cn
OK


jp
NG
must be greater than or equal to 20

sg
NG
must be greater than or equal to 21
20
cn
OK


jp
OK


sg
NG
must be greater than or equal to 21
21
cn
OK


jp
OK


sg
OK

Warning

このControllerの実装は、countryの値が、”cn”、”jp”、”sg”のいずれでもない場合のハンドリングが行われておらず、不十分である。 countryの値が、想定外の場合に、400エラーが返却される。

次にチェック対象の国が増えたため、成人条件18歳以上をデフォルトルールとしたい場合を考える。

ルールは、以下のようになる。

グループ 成人条件
Japanese 20歳以上
Singaporean 21歳以上
上記以外の国(Default) 18歳以上
  • フォームクラス

    Defaultグループに意味を持たせるため、@Min以外のアノテーションにも、明示的に全グループを指定する必要がある。

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    import java.util.List;
    
    import jakarta.validation.Valid;
    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    import jakarta.validation.groups.Default;
    
    public class UserForm implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        public static interface Japanese {
        };
    
        public static interface Singaporean {
        };
    
        @NotNull(groups = { Default.class, Japanese.class, Singaporean.class }) // (1)
        @Size(min = 1, max = 20, groups = { Default.class, Japanese.class,
                Singaporean.class })
        private String name;
    
        @NotNull(groups = { Default.class, Japanese.class, Singaporean.class })
        @Size(min = 1, max = 50, groups = { Default.class, Japanese.class,
                Singaporean.class })
        @Email(groups = { Default.class, Japanese.class, Singaporean.class })
        private String email;
    
        @NotNull(groups = { Default.class, Japanese.class, Singaporean.class })
        @Min(value = 18, groups = Default.class) // (2)
        @Min(value = 20, groups = Japanese.class)
        @Min(value = 21, groups = Singaporean.class)
        @Max(value = 200, groups = { Default.class, Japanese.class, Singaporean.class })
        private Integer age;
    
        @NotNull(groups = { Default.class, Japanese.class, Singaporean.class })
        @Size(min = 2, max = 2, groups = { Default.class, Japanese.class,
                Singaporean.class })
        private String country;
    
        // omitted setter/getter
    }
    
    項番 説明
    (1)
    @Min以外のアノテーションにも、全グループを設定する。
    (2)
    Defaultグループに対するルールを設定する。
  • JSP

    JSPに変更はない

  • Controllerクラス

    package com.example.sample.app.validation;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import com.example.sample.app.validation.UserForm.Japanese;
    import com.example.sample.app.validation.UserForm.Singaporean;
    
    @Controller
    @RequestMapping("user")
    public class UserController {
    
        @ModelAttribute
        public UserForm setupForm() {
            UserForm form = new UserForm();
            return form;
        }
    
        @GetMapping(value = "create", params = "form")
        public String createForm() {
            return "user/createForm";
        }
    
        String createConfirm(UserForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "user/createForm";
            }
            return "user/createConfirm";
        }
    
        @PostMapping(value = "create", params = { "confirm" })
        public String createConfirmForDefault(@Validated /* (1) */ UserForm form,
                BindingResult result) {
            return createConfirm(form, result);
        }
    
        @PostMapping(value = "create", params = { "confirm", "country=jp" })
        public String createConfirmForJapanese(
                @Validated(Japanese.class)  /* (2) */ UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    
        @PostMapping(value = "create", params = { "confirm", "country=sg" })
        public String createConfirmForSingaporean(
                @Validated(Singaporean.class) UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    }
    
    項番 説明
    (1)
    countryフィールド指定がない場合に、Defaultグループが使用されるように設定する。
    (2)
    countryフィールド指定がある場合に、Defaultグループが含まれないように設定する。

バリデーショングループを使用する方法について、2パターン説明した。

前者はDefaultグループをControllerクラスで使用し、後者はDefaultグループをフォームクラスで使用した。

パターン メリット デメリット 使用の判断ポイント
DefaultグループをControllerクラスで使用 グループ化する必要のないルールは、group属性を設定する必要がない。 グループの全パターンを定義する必要があるので、グループパターンが多いと、定義が困難になる。 グループパターンが、数種類の場合に使用すべき(新規作成グループ、更新グループ、削除グループ等)
Defaultグループをフォームクラスで使用 デフォルトに属さないグループのみ定義すればよいため、パターンが多くても対応できる。 グループ化する必要のないルールにも、group属性を設定する必要があり、管理が煩雑になる。 グループパターンにデフォルト値を設定できる(グループの大多数に共通項がある)場合に使用すべき

使用の判断ポイントのどちらにも当てはまらない場合は、Bean Validationの使用が不適切であることが考えられる。設計を見直したうえで、Spring Validatorの使用または業務ロジックチェックでの実装を検討すること。

Note

これまでの例ではバリデーショングループの切り替えは、リクエストパラメータ等、@GetMapping@PostMappingで指定できるパラメータによって行った。この方法では認証オブジェクトが有する権限情報など、@GetMapping@PostMappingでは扱えない情報でグループを切り替えることはできない。

この場合は、@Validatedアノテーションを使用せず、org.springframework.validation.SmartValidatorを使用し、Controllerのハンドラメソッド内でグループを指定したバリデーションを行えばよい。

@Controller
@RequestMapping("user")
public class UserController {

    @Inject
    SmartValidator smartValidator; // (1)

    // omitted

    @PostMapping(value = "create", params = "confirm")
    public String createConfirm(/* (2) */ UserForm form, BindingResult result) {
        // (3)
        Class<?> validationGroup = Default.class;
        // logic to determine validation group
        // if (xxx) {
        //     validationGroup = Xxx.class;
        // }
        smartValidator.validate(form, result, validationGroup); // (4)
        if (result.hasErrors()) {
            return "user/createForm";
        }
        return "user/createConfirm";
    }

}
項番 説明
(1)
SmartValidatorをインジェクションする。SmartValidator<mvc:annotation-driven>の設定が行われていれば使用できるため、別途Bean定義不要である。
(2)
@Validatedアノテーションは使わない。
(3)
バリデーショングループを決定する。
バリデーショングループを決定するロジックは、Helperクラスに委譲して、Controller内のロジックをシンプルな状態に保つことを推奨する。
(4)
SmartValidatorvalidateメソッドを使用して、グループを指定したバリデーションを実行する。
グループの指定は可変長引数になっており、複数指定できる。

基本的にはControllerにロジックを書くべきではないため、@GetMapping@PostMappingの属性でルールを切り替えられるのであればSmartValidatorは使わない方がよい。

4.1.2.3. 相関項目チェック

複数フィールドにまたがる相関項目チェックには、 Spring Validator(org.springframework.validation.Validatorインタフェースを実装したValidator)、 または、Bean Validationを用いる。

それぞれ説明するが、先にそれぞれの特徴と推奨する使い分けを述べる。

方式 特徴 用途
Spring Validator
特定のクラスに対する入力チェックの作成が容易である。
Controllerでの利用が不便。
特定のフォームに依存した業務要件固有の入力チェック実装
Bean Validation
入力チェックの作成はSpring Validatorほど容易でない。
Controllerでの利用が容易。
特定のフォームに依存しない、開発プロジェクト共通の入力チェック実装

4.1.2.3.1. Spring Validatorによる相関項目チェック実装

「パスワードリセット」処理を例に実装方法を説明する。
以下のルールを実装する。ここでは「パスワードリセット」のフォームに以下のチェックルールを設ける。
フィールド名 ルール 説明
password
java.lang.String
入力必須
8文字以上
パスワード
confirmPassword
java.lang.String
passwordが入力されている場合、passwordと同じ値であること
確認用パスワード

「passwordが入力されている場合、passwordと同じ値であること」というルールはpasswordフィールドとconfirmPasswordフィールドの両方の情報が必要であるため、相関項目チェックルールである。

  • フォームクラス

    相関項目チェックルール以外は、これまで通りBean Validationのアノテーションで実装する。

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    public class PasswordResetForm implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @NotNull
        @Size(min = 8)
        private String password;
    
        private String confirmPassword;
    
        // omitted setter/getter
    }
    

    Note

    パスワードは、通常ハッシュ化してデータベースに保存するため、最大値のチェックは行わなくても良い。

  • Validatorクラス

    org.springframework.validation.Validatorインタフェースを実装して、相関項目チェックルールを実現する。

    package com.example.sample.app.validation;
    
    import java.util.Objects;
    
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.validation.Errors;
    import org.springframework.validation.Validator;
    
    @Component // (1)
    public class PasswordEqualsValidator implements Validator {
    
        @Override
        public boolean supports(Class<?> clazz) {
            return PasswordResetForm.class.isAssignableFrom(clazz); // (2)
        }
    
        @Override
        public void validate(Object target, Errors errors) {
    
            PasswordResetForm form = (PasswordResetForm) target;
            String password = form.getPassword();
            String confirmPassword = form.getConfirmPassword();
    
            if (!StringUtils.hasLength(password)) { // (3)
                return;
            }
    
            if (!Objects.equals(password, confirmPassword)) { // (4)
                errors.rejectValue(/* (5) */ "confirmPassword",
                /* (6) */ "PasswordEqualsValidator.passwordResetForm.password",
                /* (7) */ "password and confirm password must be same.");
            }
        }
    }
    
    項番 説明
    (1)
    @Componentを付与し、Validatorをコンポーネントスキャン対象にする。
    (2)
    このValidatorのチェック対象であるかどうかを判別する。ここでは、PasswordResetFormクラスをチェック対象とする。
    (3)
    passwordフィールドが未入力の場合は、このValidatorで相関チェックは行わない。
    相関チェックを必ず行う必要がある場合は、この判定ロジックは不要である。
    (4)
    チェックロジックを実装する。
    (5)
    エラー対象のフィールド名を指定する。
    (6)
    エラーメッセージのコード名を指定する。ここではコードを、
    “バリデータ名.フォーム属性名.プロパティ名”
    とする。メッセージ定義はapplication-messages.propertiesに定義するメッセージを参照されたい。
    (7)
    エラーメッセージをコードで解決できなかった場合に使用する、デフォルトメッセージを設定する。

    Note

    Spring Validator実装クラスは、使用するControllerと同じパッケージに配置することを推奨する。

  • Controllerクラス

    package com.example.sample.app.validation;
    
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.WebDataBinder;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.InitBinder;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import jakarta.inject.Inject;
    
    @Controller
    @RequestMapping("password")
    public class PasswordResetController {
        @Inject
        PasswordEqualsValidator passwordEqualsValidator; // (1)
    
        @ModelAttribute
        public PasswordResetForm setupForm() {
            return new PasswordResetForm();
        }
    
        @InitBinder
        public void initBinder(WebDataBinder binder) {
            binder.addValidators(passwordEqualsValidator); // (2)
        }
    
        @GetMapping(value = "reset", params = "form")
        public String resetForm() {
            return "password/resetForm";
        }
    
        @PostMapping(value = "reset")
        public String reset(@Validated PasswordResetForm form, BindingResult result) { // (3)
            if (result.hasErrors()) {
                return "password/resetForm";
            }
            return "redirect:/password/reset?complete";
        }
    
        @GetMapping(value = "reset", params = "complete")
        public String resetComplete() {
            return "password/resetComplete";
        }
    }
    
    項番 説明
    (1)
    使用するSpring Validatorを、インジェクションする。
    (2)
    @InitBinderアノテーションがついたメソッド内で、WebDataBinder.addValidatorsメソッドにより、Validatorを追加する。
    これにより、@Validatedアノテーションでバリデーションをする際に、追加したValidatorも呼び出される。
    (3)
    入力チェックの実装は、これまで通りである。
  • JSP

    JSPに特筆すべき点はない。

    <!DOCTYPE html>
    <html>
    <%-- WEB-INF/views/password/resetForm.jsp --%>
    <head>
    <style type="text/css">
    /* omitted */
    </style>
    </head>
    <body>
        <form:form modelAttribute="passwordResetForm" method="post"
            class="form-horizontal"
            action="${pageContext.request.contextPath}/password/reset">
            <form:label path="password" cssErrorClass="error-label">Password:</form:label>
            <form:password path="password" cssErrorClass="error-input" />
            <form:errors path="password" cssClass="error-messages" />
            <br>
            <form:label path="confirmPassword" cssErrorClass="error-label">Password (Confirm):</form:label>
            <form:password path="confirmPassword"
                cssErrorClass="error-input" />
            <form:errors path="confirmPassword" cssClass="error-messages" />
            <br>
            <form:button>Reset</form:button>
        </form:form>
    </body>
    </html>
    

passwordフィールドと、confirmPasswordフィールドに、別の値を入力してフォームを送信した場合は、以下のようにエラーメッセージが表示される。

../../_images/validations-correlation-check1.png

Note

<form:password>タグを使用すると、再表示時に、データがクリアされる。

Note

相関チェック対象の複数フィールドに対してエラー情報を設定することも可能である。 ただし、必ずエラーメッセージの表示とスタイル適用がセットで行われ、いずれか片方のみを行うことはできない。

相関チェックエラーとなった両方のフィールドにスタイル適用したいが、エラーメッセージは1つだけ表示したいような場合は、 エラーメッセージに空文字を設定することで実現することが可能である。 以下に、passwordフィールドとconfirmPasswordフィールドにスタイルを適用し、confirmPasswordフィールドのみにエラーメッセージを表示する例を示す。

package com.example.sample.app.validation;

import java.util.Objects;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

@Component
public class PasswordEqualsValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return PasswordResetForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

        // omitted
        if (!Objects.equals(password, confirmPassword)) {
            // register a field error for confirmPassword
            errors.rejectValue("confirmPassword",
                      "PasswordEqualsValidator.passwordResetForm.confirmPassword",
                      "password and confirm password must be same.");

            // register a field error for password
            errors.rejectValue("password", // (1)
                   "PasswordEqualsValidator.passwordResetForm.password", // (2)
                   ""); // (3)
        }
    }
}
項番 説明
(1)
passwordフィールドのエラーを登録する。
(2)
エラーメッセージのコード名を指定する。この際、対応するエラーメッセージに空文字を指定する。
メッセージ定義はapplication-messages.propertiesに定義するメッセージを参照されたい。
(3)
エラーメッセージをコードで解決できなかった場合に使用する、デフォルトメッセージを設定する。
上記の例では空文字を設定している。

Warning

@InitBinderアノテーションを付与したメソッドでValidatorが登録されると、Validatorの supportsメソッドでValidatorのサポート対象の型かどうか判定される。このとき、サポート対象でない場合はjava.lang.IllegalStateExceptionが発生する点に注意されたい。

@InitBinderアノテーションを付与したメソッドは、Modelに独自の型のオブジェクトが追加された際に必ず実行されるが、 @InitBinder("xxx")でモデル名を指定することで、適用範囲を限定することが可能である。

例えば以下のようなケースが該当するため、注意されたい。

  • 一つのControllerで複数のフォームを扱う場合(複数のフォームオブジェクトを @ModelAttributeアノテーションを付与したメソッドで登録する場合や、ハンドラメソッドの引数として受け取る場合)
  • フォームオブジェクトに限らず、ハンドラメソッドの引数として受け取ったModelに、ResultMessagesオブジェクトやPageオブジェクトなどの独自の型のオブジェクトを登録する場合
  • 同様にRedirectAttributesに独自の型のオブジェクトを登録する場合

以下に、一つのControllerで複数のフォームを扱う場合の実装例を示す。

@Controller
@RequestMapping("xxx")
public class XxxController {
    // omitted
    @ModelAttribute("aaa")
    public AaaForm() {
        return new AaaForm();
    }

    @ModelAttribute("bbb")
    public BbbForm() {
        return new BbbForm();
    }

    @InitBinder("aaa")
    public void initBinderForAaa(WebDataBinder binder) {
        // add validators for AaaForm
        binder.addValidators(aaaValidator);
    }

    @InitBinder("bbb")
    public void initBinderForBbb(WebDataBinder binder) {
        // add validators for BbbForm
        binder.addValidators(bbbValidator);
    }
    // omitted
}

Note

相関項目チェックルールのチェック内容をバリデーショングループに応じて変更したい場合(例えば、特定のバリデーショングループが指定された場合だけ相関項目チェックを実施したい場合など)は、 org.springframework.validation.Validatorインターフェイスを実装する代わりに、 org.springframework.validation.SmartValidatorインターフェイスを実装し、validateメソッド内で処理を切り替えるとよい。

package com.example.sample.app.validation;

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;

@Component
public class PasswordEqualsValidator implements SmartValidator { // Implements SmartValidator instead of Validator interface

    @Override
    public boolean supports(Class<?> clazz) {
        return PasswordResetForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        validate(target, errors, new Object[] {});
    }

    @Override
    public void validate(Object target, Errors errors, Object... validationHints) {
        // Check validationHints(groups) and apply validation logic only when 'Update.class' is specified
        if (ArrayUtils.contains(validationHints, Update.class)) {
            PasswordResetForm form = (PasswordResetForm) target;
            String password = form.getPassword();
            String confirmPassword = form.getConfirmPassword();

            // omitted...
        }
    }
}

4.1.2.3.2. Bean Validationによる相関項目チェック実装

Bean Validationによって、相関項目チェックの実装するためには、独自バリデーションルールの追加を行う必要がある。

How to extendにて説明する。

4.1.2.4. エラーメッセージの定義

入力チェックエラーメッセージを変更する方法を説明する。

Spring MVCによるBean Validationのエラーメッセージは、以下の順で解決される。

  1. org.springframework.context.MessageSourceに定義されているメッセージの中に、ルールに合致するものがあればそれをエラーメッセージとして使用する (Springのルール)。
    Springのデフォルトのルールについては、「DefaultMessageCodesResolverのJavaDoc」を参照されたい。
  2. 1.でメッセージが見つからない場合、アノテーションのmessage属性に、指定されたメッセージからエラーメッセージを取得する (Bean Validationのルール)

  1. message属性に指定されたメッセージが、”{メッセージキー}”形式でない場合、そのテキストをエラーメッセージとして使用する。
  2. message属性に指定されたメッセージが、”{メッセージキー}”形式の場合、クラスパス直下のValidationMessages.propertiesから、メッセージキーに対応するメッセージを探す。
  3. メッセージキーに対応するメッセージが定義されている場合は、そのメッセージを使用する
  4. メッセージキーに対応するメッセージが定義されていない場合は、”{メッセージキー}”をそのままエラーメッセージとして使用する

基本的にエラーメッセージは、propertiesファイルに定義することを推奨する。

定義する箇所は、以下の2パターン存在する。

  • org.springframework.context.MessageSourceが読み込むpropertiesファイル
  • クラスパス直下のValidationMessages.properties

以下の説明では、applicationContext.xmlに次の設定があることを前提とし、前者を”application-messages.properties”、後者を”ValidationMessages.properties”と呼ぶ。

<bean id="messageSource"
    class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
        <list>
            <value>i18n/application-messages</value>
        </list>
    </property>
</bean>
../../_images/validations-message-properties-position-image.png

Warning

ValidationMessages.propertiesファイルは、クラスパスの直下に複数存在させてはいけない。

クラスパスの直下に複数のValidationMessages.propertiesファイルが存在する場合、 いずれか1つのファイルが読み込まれ、他のファイルが読み込まれないため、適切なメッセージが表示されない可能性がある。

  • マルチプロジェクト構成を採用する場合は、ValidationMessages.propertiesファイルを複数のプロジェクトに配置しないように注意すること。
  • Bean Validation用の共通部品をjarファイルとして配布する際に、ValidationMessages.propertiesファイルをjarファイルの中に含めないように注意すること。

ブランクプロジェクト からプロジェクトを生成した場合は、 xxx-web/src/main/resourcesの直下にValidationMessages.propertiesが格納されている。


本ガイドラインでは、以下のように定義を分けることを推奨する。

プロパティファイル名 定義する内容
ValidationMessages.properties
システムで定めたBean Validationのデフォルトエラーメッセージ
application-messages.properties
個別で上書きしたいBean Validationのエラーメッセージ
Spring Validatorで実装した入力チェックのエラーメッセージ

ValidationMessages.propertiesを用意しない場合は、Hibernate Validatorが用意するデフォルトメッセージが使用される。

MessageSourceと連携することで、日本語メッセージをNative to Asciiせずに直接扱うことができる。 詳細は、Native to Asciiを行わないメッセージの読み込みを参照されたい。

4.1.2.4.1. ValidationMessages.propertiesに定義するメッセージ

クラスパス直下(通常src/main/resources)のValidationMessages.properties内の、 Bean Validationのアノテーションのmessage属性に指定されたメッセージキーに対して、メッセージを定義する。

基本的な単項目チェックの初めに使用した、以下のフォームを用いて説明する。

  • フォームクラス(再掲)

    public class UserForm implements Serializable {
    
        @NotNull
        @Size(min = 1, max = 20)
        private String name;
    
        @NotNull
        @Size(min = 1, max = 50)
        @Email
        private String email;
    
        @NotNull
        @Min(0)
        @Max(200)
        private Integer age;
    
        // omitted getter/setter
    }
    
  • ValidationMessages.properties

    @NotNull, @Size, @Min, @Max, @Emailのエラーメッセージを変更する。

    jakarta.validation.constraints.NotNull.message=is required.
    # (1)
    jakarta.validation.constraints.Size.message=size is not in the range {min} through {max}.
    jakarta.validation.constraints.Min.message=can not be less than {value}.
    jakarta.validation.constraints.Max.message=can not be greater than {value}.
    jakarta.validation.constraints.Email.message=is an invalid e-mail address.
    
    項番 説明
    (1)
    アノテーションに指定した属性値は、{属性名}で埋め込むことができる。

この設定を加えた状態で、すべての入力フィールドを未入力のままフォームを送信すると、以下のように変更したエラーメッセージが、表示される。

../../_images/validations-customize-message1.png

Warning

Bean Validation標準のアノテーションやHibernate Validator独自のアノテーションにはmessage属性に{アノテーションのFQCN.message}という値が設定されているため、

アノテーションのFQCN.message=メッセージ

という形式でプロパティファイルにメッセージを定義すればよいが、すべてのアノテーションが、この形式になっているわけではないので、 対象のアノテーションのJavadocまたはソースコードを確認すること。

エラーメッセージに、フィールド名を含める場合は、以下のように、メッセージに{0}を加える。

  • ValidationMessages.properties

    @NotNull@Size@Min@Max@Emailのエラーメッセージを変更する。

    jakarta.validation.constraints.NotNull.message="{0}" is required.
    jakarta.validation.constraints.Size.message=The size of "{0}" is not in the range {min} through {max}.
    jakarta.validation.constraints.Min.message="{0}" can not be less than {value}.
    jakarta.validation.constraints.Max.message="{0}" can not be greater than {value}.
    jakarta.validation.constraints.Email.message="{0}" is an invalid e-mail address.
    

エラーメッセージは、以下のように変更される。

../../_images/validations-customize-message2.png

このままでは、フォームクラスのプロパティ名が表示されてしまい、ユーザーフレンドリではない。 適切なフィールド名を表示したい場合は、application-messages.propertiesに

フォームのプロパティ名=表示するフィールド名

形式でフィールド名を定義すればよい。

これまでの例に、以下の設定を追加する。

  • application-messages.properties

    name=Name
    email=Email
    age=Age
    

エラーメッセージは、以下のように変更される。

../../_images/validations-customize-message3.png

Note

{0}でフィールド名を埋め込めるのは、Bean Validationの機能ではなく、Springの機能である。 したがって、フィールド名変更の設定は、Spring管理下のapplication-messages.properties(ResourceBundleMessageSource)に定義する必要がある。

Tip

Bean Validation 1.1より、 ValidationMessages.properties に指定するメッセージの中にExpression Language(以降、「EL式」と呼ぶ)を使用する事ができるようになった。 Hibernate Validator 8.xでは、Expression Language 5.0以上をサポートしている。

実行可能なEL式のバージョンは、アプリケーションサーバのバージョンによって異なる。 そのため、EL式を使用する場合は、アプリケーションサーバがサポートしているEL式のバージョンを確認した上で使用すること。

以下に、Hibernate Validatorがデフォルトで用意している ValidationMessages.properties に定義されているメッセージを例に、EL式の使用例を示す。

  # ...
  # (1)
jakarta.validation.constraints.DecimalMax.message  = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
  # ...
項番 説明
(1)

メッセージの中の 「${inclusive == true ? 'or equal to ' : ''}」の部分がEL式である。

上記のメッセージ定義から実際に生成されるメッセージのパターンは、

  • must be less than or equal to {value}
  • must be less than {value}

の2パターンとなる。({value}の部分には、@DecimalMaxアノテーションの value属性に指定した値が埋め込まれる)

前者は@DecimalMaxアノテーションの inclusive属性に trueを指定した場合(又は指定しなかった場合)、 後者は@DecimalMaxアノテーションの inclusive属性に falseを指定した場合に生成される。

Bean ValidationにおけるEL式の扱いについては、Hibernate Validator Reference Guide(Interpolation with message expressions)を参照されたい。

また、ValidationMessages.properties に指定するメッセージに ${validatedValue}を使用することで、エラーメッセージにチェック対象の値を含むことができる。

以下に、 ${validatedValue}の使用例を示す。

  # ...
  # (1)
jakarta.validation.constraints.Pattern.message = The value entered "${validatedValue}" is invalid.
  # ...
項番 説明
(1)
上記のメッセージ定義から実際に生成されるメッセージは、 ${validatedValue}の部分にフォームに入力した値が埋め込まれる。
入力値に機密情報を含む場合、機密情報がメッセージに表示されないようにするため、 ${validatedValue}を使用しないように注意すること。


4.1.2.4.2. application-messages.propertiesに定義するメッセージ

ValidationMessages.propertiesでシステムが利用するデフォルトのメッセージを定義したが、 画面によっては、デフォルトメッセージから変更したい場合が出てくる。

その場合、application-messages.propertiesに、以下の形式でメッセージを定義する。

アノテーション名.フォーム属性名.プロパティ名=対象のメッセージ

ValidationMessages.propertiesに定義するメッセージの設定がある前提で、以下の設定でemailageフィールドのメッセージを上書きする。

  • application-messages.properties

    # override messages
    # for email field
    Size.userForm.email=The size of "{0}" must be between {2} and {1}.
    # for age field
    NotNull.userForm.age="{0}" is compulsory.
    Min.userForm.age="{0}" must be greater than or equal to {1}.
    Max.userForm.age="{0}" must be less than or equal to {1}.
    
    # filed names
    name=Name
    email=Email
    age=Age
    

アノテーションの属性値は、{1}以降に埋め込まれる。なお、属性値のインデックス位置は、アノテーションの属性名のアルファベット順(昇順)となる。

例えば、@Sizeのインデックス位置は、

  • {0} : プロパティ名 (物理名又は論理名)
  • {1} : max属性の値
  • {2} : min属性の値
となる。
仕様の詳細についてはSpringValidatorAdapterのJavaDocを参照されたい。

エラーメッセージは以下のように変更される。

../../_images/validations-customize-message4.png

Note

application-messages.propertiesのメッセージキーの形式はこれ以外にも用意されているが、デフォルトメッセージを一部上書きする目的で使用するのであれば、基本的に、アノテーション名.フォーム属性名.プロパティ名形式でよい。


4.1.3. How to extend

Bean Validationは標準で用意されているチェックルール以外に、独自ルール用アノテーションを作成する仕組みをもつ。

作成方法は大きく分けて、以下の観点で分かれる。

  • 既存ルールの組み合わせ
  • 新規ルールの作成

基本的には、以下の雛形を使用して、ルール毎にアノテーションを作成する。

package com.example.common.validation;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import com.example.common.validation.Xxx.List;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface Xxx {
    String message() default "{com.example.common.validation.Xxx.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Xxx[] value();
    }
}

4.1.3.1. 既存ルールを組み合わせたBean Validationアノテーションの作成

システム共通で、

  • 文字列は半角英数字の文字種に限定したい
  • 数値は8桁までの正の数に限定したい
または、ドメイン共通で、
  • 「ユーザーID」は、4文字以上20文字以下の半角英字に制限したい
  • 「年齢」は、1歳以上150歳以下に制限したい
という制約がある場合を考える。
これらは既存ルールの@Pattern@Size@Min@Max等を組み合わせることでも実現できるが、
同じルールを複数の箇所で使用すると、設定内容が分散してしまい、メンテナンス性が悪化する。

複数のルールを組み合わせて一つのルールを作成することができる。 独自アノテーションを作成すると、正規表現パターンや、最大値・最小値などの値を共通化できるだけでなく、エラーメッセージも共通化できるというメリットがある。 これにより、再利用性や保守性が高まる。複数のルールの組み合わせではなくても、一つのルールの属性を特定するだけでも効果的である。

以下に、実装例を示す。

  • 半角英数字の文字種に限定する@Alphanumericアノテーションの実装例

    package com.example.common.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.CONSTRUCTOR;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.common.validation.AlphaNumeric.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import jakarta.validation.ReportAsSingleViolation;
    import jakarta.validation.constraints.Pattern;
    
    @Documented
    @Constraint(validatedBy = {}) // (1)
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Repeatable(List.class)
    @ReportAsSingleViolation // (2)
    @Pattern(regexp = "[a-zA-Z0-9]*") // (3)
    public @interface AlphaNumeric {
        String message() default "{com.example.common.validation.AlphaNumeric.message}"; // (4)
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            AlphaNumeric[] value();
        }
    }
    
    項番 説明
    (1)
    既存のアノテーションを利用して実装を行う場合、validatedByは空にしておく必要がある。
    (2)
    エラーメッセージをまとめ、エラー時はこのアノテーションによるメッセージだけを変えるようにする。
    (3)
    このアノテーションにより使用されるルールを定義する。
    (4)
    エラーメッセージのデフォルト値を定義する。
  • 8桁までの正の数に限定する@MaxDigitsアノテーションの実装例

    package com.example.common.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.CONSTRUCTOR;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.common.validation.MaxDigits.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import jakarta.validation.ReportAsSingleViolation;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.PositiveOrZero;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Repeatable(List.class)
    @ReportAsSingleViolation
    @PositiveOrZero
    @Max(value = 99999999)
    public @interface MaxDigits {
        String message() default "{com.example.common.validation.MaxDigits.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            MaxDigits[] value();
        }
    }
    
  • 「ユーザーID」のフォーマットを規定する@UserIdアノテーションの実装例

    package com.example.sample.domain.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.CONSTRUCTOR;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.sample.domain.validation.UserId.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import jakarta.validation.ReportAsSingleViolation;
    import jakarta.validation.constraints.Pattern;
    import jakarta.validation.constraints.Size;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Repeatable(List.class)
    @ReportAsSingleViolation
    @Size(min = 4, max = 20)
    @Pattern(regexp = "[a-z]*")
    public @interface UserId {
        String message() default "{com.example.sample.domain.validation.UserId.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            UserId[] value();
        }
    }
    
  • 「年齢」の制限を規定する@Ageアノテーションの実装例

    package com.example.sample.domain.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.CONSTRUCTOR;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.sample.domain.validation.Age.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import jakarta.validation.ReportAsSingleViolation;
    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Repeatable(List.class)
    @ReportAsSingleViolation
    @Min(1)
    @Max(150)
    public @interface Age {
        String message() default "{com.example.sample.domain.validation.Age.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            Age[] value();
        }
    }
    

    Note

    1つのアノテーションに複数のルールを設定した場合、それらのAND条件が複合ルールとなる。 Hibernate Validatorでは、OR条件を実現するための@ConstraintCompositionアノテーションが用意されている。

    詳細は、Hibernate Validatorのドキュメントを参照されたい。


4.1.3.2. 新規ルールを実装したBean Validationアノテーションの作成

jakarta.validation.ConstraintValidatorインタフェースを実装し、そのValidatorを使用するアノテーションを作成することで、任意のルールを作成することができる。

用途としては、以下の3通りが挙げられる。

  • 既存のルールの組み合わせでは表現できないルール
  • 相関項目チェックルール
  • 業務ロジックチェック

4.1.3.2.1. 既存のルールの組み合わせでは表現できないルール

@Pattern@Size@Min@Max等を組み合わせても表現できないルールは、jakarta.validation.ConstraintValidator実装クラスに記述する。

例として、IPv4形式のIPアドレス(Internet Protocol address)であることをチェックするルールを挙げる。

  • アノテーション

    package com.example.common.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.CONSTRUCTOR;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.common.validation.IPv4.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import jp.co.ntt.fw.spring.functionaltest.domain.cmmn.validation.IPv4Validator;
    
    @Documented
    @Constraint(validatedBy = { IPv4Validator.class }) // (1)
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Repeatable(List.class)
    public @interface IPv4 {
        String message() default "{com.example.common.validation.IPv4.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            IPv4[] value();
        }
    }
    
    項番 説明
    (1)
    このアノテーションを使用したときに実行されるConstraintValidatorを指定する。複数指定することができる。
  • Validator

    package com.example.common.validation;
    
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    
    public class IPv4Validator implements ConstraintValidator<IPv4, String> { // (1)
    
        @Override
        public void initialize(IPv4 constraintAnnotation) { // (2)
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) { // (3)
            if (value == null) {
                return true; // (4)
            }
            return isIPv4Valid(value); // (5)
        }
    
        // This logic check IPv4 address like 192.168.0.1
        static boolean isIPv4Valid(String ipAddress) {
            String[] octets = ipAddress.split("\\.");
            if (octets.length != 4) {
                return false;
            }
            try {
                for (String octet: octets) {
                    int intOctet = Integer.parseInt(octet);
                    if (intOctet < 0 || 255 < intOctet) {
                        return false;
                    }
                }
            } catch (NumberFormatException e) {
                return false;
            }
            return true;
        }
    }
    
    項番 説明
    (1)
    ジェネリクスのパラメータに、対象のアノテーションとフィールドの型を指定する。
    (2)
    initializeメソッドに、初期化処理を実装する。
    (3)
    isValidメソッドで入力チェック処理を実装する。
    (4)
    入力値が、nullの場合は、正常とみなす。
    (5)
    IPv4形式のIPアドレスであることのチェックを行う。

Tip

ファイルアップロードのBean Validationの例も、ここに分類される。また共通ライブラリでは、この実装として@ExistInCodeListを用意している。

4.1.3.2.2. 相関項目チェックルール

相関項目チェックで説明したように、Bean Validationによって複数のフィールドにまたがる相関項目チェックを実装できる。
Bean Validationで相関項目チェックルールを実装する場合は、汎用的なルールを対象とすることを推奨する。

以下では、「あるフィールドとその確認用フィールドの内容が一致すること」というルールを実現する例を挙げる。

Tip

共通ライブラリでは、2つのフィールドの内容を比較する相関項目チェックの実装として@Compareアノテーションを用意している。

@Compareアノテーションを利用することで、このルールをより簡単に実現することができる。 詳細は共通ライブラリのチェックルールの拡張方法を参照されたい。

ここでは、内容が一致しない場合には確認用フィールドにエラーメッセージを表示する。

  • アノテーション

    相関項目チェック用のアノテーションはクラスレベルに付与できるようにする。

    package com.example.common.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.TYPE;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.common.validation.Confirm.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    
    @Documented
    @Constraint(validatedBy = { ConfirmValidator.class })
    @Target({ TYPE, ANNOTATION_TYPE, TYPE_USE }) // (1)
    @Retention(RUNTIME)
    @Repeatable(List.class)
    public @interface Confirm {
        String message() default "{com.example.common.validation.Confirm.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        /**
         * Field name
         */
        String field(); // (2)
    
        /**
         * ConfirmField name
         */
        String confirmField(); // (2)
    
        @Target({ TYPE, ANNOTATION_TYPE, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            Confirm[] value();
        }
    }
    
    項番 説明
    (1)
    このアノテーションが、クラスまたはアノテーションにのみ付加できるように、対象を絞る。
    (2)
    アノテーションに渡すパラメータを定義する。
  • Validator

    package com.example.common.validation;
    
    import java.util.Objects;
    
    import org.springframework.beans.BeanWrapper;
    import org.springframework.beans.BeanWrapperImpl;
    
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    
    public class ConfirmValidator implements ConstraintValidator<Confirm, Object> {
        private String field;
    
        private String confirmField;
    
        private String message;
    
        public void initialize(Confirm constraintAnnotation) {
            field = constraintAnnotation.field();
            confirmField = constraintAnnotation.confirmField();
            message = constraintAnnotation.message();
        }
    
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            BeanWrapper beanWrapper = new BeanWrapperImpl(value); // (1)
            Object fieldValue = beanWrapper.getPropertyValue(field); // (2)
            Object confirmFieldValue = beanWrapper.getPropertyValue(confirmField);
            boolean matched = Objects.equals(fieldValue, confirmFieldValue);
            if (matched) {
                return true;
            } else {
                context.disableDefaultConstraintViolation(); // (3)
                context.buildConstraintViolationWithTemplate(message)
                        .addPropertyNode(confirmField).addConstraintViolation(); // (4)
                return false;
            }
        }
    
    }
    
    項番 説明
    (1)
    JavaBeanのプロパティにアクセスする際に便利なorg.springframework.beans.BeanWrapperを使用する。
    (2)
    BeanWrapper経由で、フォームオブジェクトからプロパティ値を取得する。
    (3)
    デフォルトのConstraintViolationオブジェクトの生成を無効にする。
    (4)
    独自ConstraintViolationオブジェクトを生成する。
    ConstraintValidatorContext.buildConstraintViolationWithTemplateで出力するメッセージを定義する。
    ConstraintViolationBuilder.addPropertyNodeでエラーメッセージを出力したいフィールド名を指定する。
    詳細は、ConstraintValidatorContextのJavaDocを参照されたい。

Note

Spring Validatorによる相関項目チェックにて紹介したように、Bean Validationにおいても 相関チェック対象の複数フィールドに対してエラー情報を設定する ことが可能である。

以下に、Bean ValidationにてpasswordフィールドとconfirmPasswordフィールドにスタイルを適用し、confirmPasswordフィールドのみにエラーメッセージを表示する例を示す。

// omitted
public class ConfirmValidator implements ConstraintValidator<Confirm, Object> {
    private String field;

    private String confirmField;

    private String message;

    public void initialize(Confirm constraintAnnotation) {
        // omitted
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // omitted
        if (matched) {
            return true;
        } else {
            context.disableDefaultConstraintViolation();

            //new ConstraintViolation to be generated for confirmField
            context.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(confirmField).addConstraintViolation();

            //new ConstraintViolation to be generated for field
            context.buildConstraintViolationWithTemplate("") // (1)
                    .addPropertyNode(field).addConstraintViolation();

            return false;
        }
    }

}
項番 説明
(1)
passwordフィールドのエラーを登録する。この際、エラーメッセージに空文字を設定している。

この@Confirmアノテーションを使用して、前述の「パスワードリセット」処理を再実装すると、以下のようになる。

  • フォームクラス

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import com.example.common.validation.Confirm;
    
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    @Confirm(field = "password", confirmField = "confirmPassword") // (1)
    public class PasswordResetForm implements Serializable {
        private static final long serialVersionUID = 1L;
    
        @NotNull
        @Size(min = 8)
        private String password;
    
        private String confirmPassword;
    
        // omitted geter/setter
    }
    
    項番 説明
    (1)
    クラスレベルに@Confirmアノテーションを付与する。
    これによりConstraintValidator.isValidの引数にはフォームオブジェクトが渡る。
  • Controllerクラス

    Validatorのインジェクションおよび@InitBinderによるValidatorの追加は、不要になる。

    package com.example.sample.app.validation;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("password")
    public class PasswordResetController {
    
        @ModelAttribute
        public PasswordResetForm setupForm() {
            return new PasswordResetForm();
        }
    
        @GetMapping(value = "reset", params = "form")
        public String resetForm() {
            return "password/resetForm";
        }
    
        @PostMapping(value = "reset")
        public String reset(@Validated PasswordResetForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "password/resetForm";
            }
            return "redirect:/password/reset?complete";
        }
    
        @GetMapping(value = "reset", params = "complete")
        public String resetComplete() {
            return "password/resetComplete";
        }
    }
    

4.1.3.2.3. 業務ロジックチェック

業務ロジックチェックは、基本的にはドメイン層のServiceで実装し、結果メッセージはResultMessagesオブジェクトに格納することを推奨している。

一方で、「入力されたユーザー名が既に登録済みかどうか」など、対象の入力フィールドに対する業務ロジックエラーメッセージを、フィールドの横に表示したい場合もある。 このような場合は、ValidatorクラスにServiceクラスをインジェクションして、業務ロジックチェックを実行し、その結果を、ConstraintValidator.isValidの結果に使用すればよい。

「入力されたユーザー名が既に登録済みかどうか」をBean Validationで実現する例を示す。

  • Serviceクラス

    実装クラス(UserServiceImpl)は割愛する。

    package com.example.sample.domain.service.user;
    
    public interface UserService {
    
        boolean isUnusedUserId(String userId);
    
        // omitted other methods
    }
    
  • アノテーション

    package com.example.sample.domain.validation;
    
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.CONSTRUCTOR;
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.ElementType.PARAMETER;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Repeatable;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import com.example.sample.domain.validation.UnusedUserId.List;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    
    @Documented
    @Constraint(validatedBy = { UnusedUserIdValidator.class })
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Repeatable(List.class)
    public @interface UnusedUserId {
        String message() default "{com.example.sample.domain.validation.UnusedUserId.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
        @Retention(RUNTIME)
        @Documented
        @interface List {
            UnusedUserId[] value();
        }
    }
    
  • Validatorクラス

    package com.example.sample.domain.validation;
    
    import com.example.sample.domain.service.user.UserService;
    
    import jakarta.inject.Inject;
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    
    @Component // (1)
    public class UnusedUserIdValidator implements
                                      ConstraintValidator<UnusedUserId, String> {
    
        @Inject // (2)
        UserService userService;
    
        @Override
        public void initialize(UnusedUserId constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            if (value == null) {
                return true;
            }
    
            return userService.isUnusedUserId(value); // (3)
        }
    
    }
    
    項番 説明
    (1)
    Validatorクラスをコンポーネントスキャンの対象にする。
    パッケージがBean定義ファイルの<context:component-scan base-package="..." />の設定に含まれている必要がある。
    (2)
    呼び出すServiceクラスを、インジェクションする。
    (3)
    業務ロジックの結果を返却する。isValidメソッド名で業務ロジックを記述せず、かならずServiceに処理を委譲すること。

4.1.3.3. Method Validation

Bean Validationによってメソッドの実引数と返り値の妥当性を確認する方法を説明する。 説明のために、本節ではこの方法をMethod Validationと呼ぶ。 防衛的プログラミングを行う場合などでは、Controller以外のクラスでメソッドの入出力を確認する必要がある。 このとき、Bean Validationライブラリを利用すれば、Controllerで使用したBean Validationの制約アノテーションを再利用できる。

4.1.3.3.1. アプリケーションの設定

Spring Frameworkが提供するMethod Validationを使用する場合は、 Spring Frameworkから提供されているorg.springframework.validation.beanvalidation.MethodValidationPostProcessorクラスをBean定義する必要がある。

MethodValidationPostProcessorを定義するBean定義ファイルは、Method Validationを使用する箇所によって異なる。

ここでは、本ガイドラインが推奨するマルチプロジェクト環境でMethod Validationを使用するための設定例を示す。

  • アプリケーション層用のプロジェクト(projectName-web)
  • ドメイン層用のプロジェクト(projectName-domain)

の両プロジェクトの設定を変更する必要がある。

  • projectName-domain/src/main/resources/META-INF/spring/projectName-domain.xml
<!-- (1) -->
<bean id="validator"
      class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

<!-- (2) -->
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
    <property name="validator" ref="validator" />
</bean>
  • projectName-web/src/main/resources/META-INF/spring/spring-mvc.xml
<!-- (3) -->
<mvc:annotation-driven validator="validator">
    <!-- ... -->
</mvc:annotation-driven>

<!-- (4) -->
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
    <property name="validator" ref="validator" />
</bean>
項番 説明
(1)
LocalValidatorFactoryBeanをBean定義する。
(2)

MethodValidationPostProcessorをBean定義し、 ドメイン層のクラスのメソッドに対してMethod Validationが実行されるようにする。

validatorプロパティには、(1)で定義したBeanを指定する。

(3)

<mvc:annotation-driven>要素のvalidator属性に、(1)で定義したBeanを指定する。

この設定がない場合は(1)で作成したものとは異なるValidatorインスタンスが生成されてしまう。

(4)

MethodValidationPostProcessorをBean定義し、 アプリケーション層のクラスのメソッドに対してMethod Validationが実行されるようにする。

validatorプロパティには、(1)で定義したBeanを指定する。

Tip

LocalValidatorFactoryBeanは、 Bean Validation(Hibernate Validator)が提供するValidatorクラスとSpring Frameworkを連携するためのラッパーValidatorオブジェクトを生成するためのクラスである。

このクラスによって生成されたラッパーValidatorを使用することで、 Spring Frameworkが提供するメッセージ管理機能(MessageSource)やDIコンテナなどとの連携が行えるようになる。

Tip

Spring Frameworkでは、DIコンテナで管理されているBeanのメソッド呼び出しに対するMethod Validationの実行を、 AOPの仕組みを利用して行っている。

MethodValidationPostProcessorは、Method Validationを実行するためのAOPを適用するためのクラスである。

Note

上記例では、各Beanのvalidatorプロパティに対して、同じValidatorオブジェクト(インスタンス)を設定しているが、 これは必ずしも必須ではない。 ただし、特に理由がない場合は、同じオブジェクト(インスタンス)を設定しておくことを推奨する。


4.1.3.3.2. Method Validation対象のメソッドにするための定義方法

メソッドにMethod Validationを適用するには、 対象のメソッドを含むことを示したアノテーションをクラスレベルに、 Bean Validationの制約アノテーションをメソッドと仮引数にそれぞれ指定する必要がある。

アプリケーションの設定」を行っただけでは、Method Validationを実行するAOPは適用されない。 Method Validationを実行するAOPを適用するためには、 インタフェース又はクラスに@ org.springframework.validation.annotation.Validatedアノテーションを付与する必要がある。

ここでは、インタフェースに対してアノテーションを指定する方法を紹介する。

package com.example.domain.service;

import org.springframework.validation.annotation.Validated;

@Validated // (1)
public interface HelloService {
    // ...
}
項番 説明
(1)

Method Validationの対象となるインタフェースに、Validatedアノテーションを指定する。

上記例では、HelloServiceインタフェースの実装メソッドに対して、 Method Validationを実行するAOPが適用される。

Tip

@Validatedアノテーションのvalue属性にグループインタフェースを指定することで、 指定したグループに属するValidationのみ実行する事も可能である。

また、メソッドレベルにValidatedアノテーションを付与することで、 メソッド毎にバリデーショングループを切り替える事も可能な仕組みとなっている。

バリデーショングループについては、「バリデーションのグループ化」を参照されたい。


次に、Bean Validationの制約アノテーションをメソッドや仮引数へ指定する方法を説明する。 具体的には、

  • メソッドの引数
  • メソッドの引数に指定されたJavaBeanのフィールド

に対してBean Validationの制約アノテーションを、

  • メソッドの返り値
  • メソッドの返り値として返却するJavaBeanのフィールド

に対してBean Validationの制約アノテーションを指定する。

以下に、具体的な指定方法について説明する。 以降の説明では、インタフェースにアノテーションを指定する方法を紹介する。

まず、メソッドのシグネチャとして基本型(プリミティブやプリミティブラッパ型など)を使用するメソッドに対して、 制約アノテーションを指定する方法について説明する。

package com.example.domain.service;

import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotNull;

@Validated
public interface HelloService {

    // (2)
    @NotNull
    String hello(@NotNull /* (1) */ String message);

}
項番 説明
(1)

Bean Validationの制約アノテーションをメソッドの引数アノテーションとして指定する。

@NotNullmessageという引数がNull値を許可しないことを意味する制約である。
引数にNull値が指定された場合、jakarta.validation.ConstraintViolationExceptionが発生する。
(2)

Bean Validationの制約アノテーションをメソッドアノテーションとして指定する。

上記例では、返り値がNull値にならないことを示しており、返り値としてNull値が返却された場合、jakarta.validation.ConstraintViolationExceptionが発生する。


次に、メソッドのシグネチャとしてJavaBeanを使用するメソッドに対して、 Bean Validationの制約アノテーションを指定する方法について説明する。

ここでは、インタフェースに対してアノテーションを指定する方法を紹介する。

Serviceインタフェース

package com.example.domain.service;

import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotNull;

@Validated
public interface HelloService {

    @NotNull // (3)
    @Valid   // (4)
    HelloOutput hello(@NotNull /* (1) */ @Valid /* (2) */ HelloInput input);

}
項番 説明
(1)

Bean Validationの制約アノテーションをメソッドの引数アノテーションとして指定する。

inputという引数(JavaBean)がNull値を許可しない事を示しており、引数にNull値が指定された場合は、jakarta.validation.ConstraintViolationExceptionが発生する。

(2)

@jakarta.validation.Validアノテーションをメソッドの引数アノテーションとして指定する。

@Validアノテーションを付与する事で、引数のJavaBeanのフィールドに指定したBean Validationの制約アノテーションが有効となる。
JavaBeanに指定された制約を満たさない場合はjakarta.validation.ConstraintViolationExceptionが発生する。
(3)

Bean Validationの制約アノテーションをメソッドアノテーションとして指定する。

返り値のJavaBeanがNull値にならないことを示しており、返り値としてNull値が返却された場合は例外が発生する。

(4)

@Validアノテーションをメソッドアノテーションとして指定する。

@Validアノテーションを付与する事で、返り値のJavaBeanのフィールドに指定したBean Validationの制約アノテーションが有効となる。
JavaBeanに指定された制約を満たさない場合はjakarta.validation.ConstraintViolationExceptionが発生する。

以下にJavaBeanの実装サンプルを紹介する。
基本的には、Bean Validationの制約アノテーションを指定するだけだが、JavaBeanが更にJavaBeanをネストしている場合は注意が必要になる。

Input用のJavaBean

package com.example.domain.service;

import java.util.Date;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;

public class HelloInput {

    @NotNull
    @Past
    private Date visitDate;

    @NotNull
    private String visitMessage;

    private String userId;

    // ...

}

Output用のJavaBean

package com.example.domain.service;

import java.util.Date;

import com.example.domain.model.User;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;

public class HelloOutput {

    @NotNull
    @Past
    private Date acceptDate;

    @NotNull
    private String acceptMessage;

    @Valid // (5)
    private User user;

    // ...

}

Output用のJavaBean内でネストしているJavaBean

package com.example.domain.model;

import java.util.Date;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;

public class User {

    @NotNull
    private String userId;

    @NotNull
    private String userName;

    @Past
    private Date dateOfBirth;

    // ...

}
項番 説明
(5)

ネストしたJavaBeanに指定しているBean Validationの制約アノテーションを有効にする場合は、 @Validアノテーションをフィールドアノテーションとして指定する。

@Validアノテーションを付与する事で、ネストしたJavaBeanのフィールドに指定したBean Validationの制約アノテーションが有効となる。
ネストしたJavaBeanに指定された制約を満たさない場合はjakarta.validation.ConstraintViolationExceptionが発生する。

4.1.3.3.3. 制約違反時の例外ハンドリング

制約に違反した場合、jakarta.validation.ConstraintViolationExceptionが発生する。

ConstraintViolationExceptionが発生した場合、スタックトレースから発生したメソッドは特定できるが、 具体的な違反内容が特定できない。

違反内容を特定するためには、ConstraintViolationExceptionをハンドリングしてログ出力を行う例外ハンドリングクラスを作成するとよい。

以下の例外ハンドリングクラスの作成例を示す。

package com.example.app;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import jakarta.validation.ConstraintViolationException;

@ControllerAdvice
public class ConstraintViolationExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(ConstraintViolationExceptionHandler.class);

    // (1)
    @ExceptionHandler
    public String handleConstraintViolationException(ConstraintViolationException e){
        // (2)
        if (logger.isErrorEnabled()) {
            logger.error("ConstraintViolations[\n{}\n]", e.getConstraintViolations());
        }
        return "common/error/systemError";
    }

}
項番 説明
(1)

ConstraintViolationExceptionをハンドリングするための@ExceptionHandlerメソッドを作成する。

メソッドの引数として、ConstraintViolationExceptionを受け取るようにする。

(2)
メソッドの引数で受け取ったConstraintViolationExceptionが保持している違反内容(ConstraintViolationSet)をログに出力する。

Note

@ControllerAdviceアノテーションの詳細については「@ControllerAdviceの実装」を参照されたい。

Warning

ConstraintViolation#getMessageメソッドを使用することでエラーメッセージを取得することができるが、Springの機能によるメッセージ補完は行われないため、エラーメッセージに {0}でフィールド名を埋め込むことはできない。

代わりに、フィールド名はConstraintViolation#getPropertyPathメソッドで取得することが可能である。

Springの機能によるメッセージ補完については、ValidationMessages.propertiesに定義するメッセージのNoteを参照されたい。

ConstraintViolationの詳細については、Hibernate Validatorのリファレンスを参照されたい。


4.1.4. Appendix

4.1.4.1. Hibernate Validatorが用意する入力チェックルール

Hibernate ValidatorはBean Validationで定義されたアノテーションに加え、独自の検証用アノテーションを提供している。
検証に使用することができるアノテーションのリストは、こちらを参照されたい。

4.1.4.1.1. Bean Validationのチェックルール

Bean Validationの標準アノテーション(jakarta.validation.*)を以下に示す。

詳細は、Bean Validation specification(Built-in Constraint definitions)を参照されたい。

アノテーション 対象の型 説明 使用例
@NotNull 任意 対象のフィールドが、nullでないことを検証する。
@NotNull
private String id;
@NotEmpty CollectionMap、Array、任意のCharSequenceインタフェースの実装クラスに適用可能
null、または空でないことを検証する。
@NotNull + @Min(1)の組み合わせでチェックする場合は、@NotEmptyを使用すること。(2.0から追加)
@NotEmpty
private String password;
@NotBlank 任意のCharSequenceインタフェースの実装クラスに適用可能
null、空文字("")、空白のみでないことを検証する。(2.0から追加)
@NotBlank
private String userId;
@Null 任意
対象のフィールドが、nullであることを検証する。
(例:グループ検証での使用)
@Null(groups={Update.class})
private String id;
@Pattern String
対象のフィールドが正規表現にマッチするかどうか
(Hibernate Validator実装では、任意のCharSequenceインタフェースの実装クラスにも適用可能)
@Pattern(regexp = "[0-9]+")
private String tel;
@Min
BigDecimal, BigInteger, byte, short, int, longおよびラッパー
(Hibernate Validator実装では、任意のNumberの継承クラス,CharSequenceインタフェースの実装クラスにも適用可能。ただし、文字列が数値表現の場合に限る。)
値が、最小値以上であるかどうかを検証する。 @Max参照
@Max
BigDecimal, BigInteger, byte, short, int, longおよびラッパー
(Hibernate Validator実装では任意のNumberの継承クラス,CharSequenceインタフェースの実装クラスにも適用可能。ただし、文字列が数値表現の場合に限る。)
値が、最大値以下であるかどうかを検証する。
@Min(1)
@Max(100)
private int quantity;
@DecimalMin BigDecimal, BigInteger, String, byte, short, int, longおよびラッパー (Hibernate Validator実装では任意のNumberの継承クラス,CharSequenceインタフェースの実装クラスにも適用可能)
Decimal型の値が、最小値以上であるかどうかを検証する。
inclusive = falseを指定する事で、最小値より大きいかどうかを検証するように動作を変更する事ができる。
@DecimalMax参照
@DecimalMax BigDecimal, BigInteger, String, byte, short, int, longおよびラッパー (Hibernate Validator実装では任意のNumberの継承クラス,CharSequenceインタフェースの実装クラスにも適用可能)
Decimal型の値が、最大値以下であるかどうかを検証する。
inclusive = falseを指定する事で、最大値より小さいかどうかを検証するように動作を変更する事ができる。
@DecimalMin("0.0")
@DecimalMax("99999.99")
private BigDecimal price;
@Positive BigDecimal, BigInteger, byte, short, int, long, float, doubleおよびラッパー
値が正の数値(0を含まない)であるかどうかを検証する。(2.0から追加)
@Positive
private int deposit;
@PositiveOrZero BigDecimal, BigInteger, byte, short, int, long, float, doubleおよびラッパー
値が正の数値(0を含む)であるかどうかを検証する。(2.0から追加)
@PositiveOrZero
private int deposit;
@Negative BigDecimal, BigInteger, byte, short, int, long, float, doubleおよびラッパー
値が負の数値(0を含まない)であるかどうかを検証する。(2.0から追加)
@Negative
private int deposit;
@NegativeOrZero BigDecimal, BigInteger, byte, short, int, long, float, doubleおよびラッパー
値が正の数値(0を含む)であるかどうかを検証する。(2.0から追加)
@NegativeOrZero
private int deposit;
@Size String(文字列の長さ), Collection(要素のサイズ), Map(要素のサイズ), Array(配列の長さ) (Hibernate Validator実装では、任意のCharSequenceインタフェースの実装クラスにも適用可能)
要素の長さ(要素のサイズ)がminmaxの間のサイズか検証する。
minmaxは省略可能であるが、デフォルトはmin=0,max= Integer.MAX_VALUEとなる。
@Size(min=4, max=64)
private String password;
@Digits BigDecimal, BigInteger, String, byte, short, int, longおよびラッパー
値が指定された範囲内の数値であるかチェックする。
integerに最大整数の桁を指定し、fractionに最大小数桁を指定する。
@Digits(integer=6, fraction=2)
private BigDecimal price;
@AssertTrue boolean,Boolean 対象のフィールドがtrueであることを検証する(例:規約に同意したかどうか)
@AssertTrue
private boolean checked;
@AssertFalse boolean,Boolean 対象のフィールドがfalseであることを検証する
@AssertFalse
private boolean checked;
@Future Date, CalendarおよびJSR-310 Date and Time APIで提供されるクラス
未来であるか検証する。
Dateのように日時を持つ型では未来日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では未来日付であるか検証する。
@Future
private Date eventDate;
@FutureOrPresent Date, CalendarおよびJSR-310 Date and Time APIで提供されるクラス
現在または未来であるか検証する。(2.0から追加)
Dateのように日時を持つ型では未来日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では未来日付であるか検証する。
@FutureOrPresent
private Date eventDate;
@Past Date, CalendarおよびJSR-310 Date and Time APIで提供されるクラス
過去であるか検証する。
Dateのように日時を持つ型では過去日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では過去日付であるか検証する。
@Past
private Date eventDate;
@PastOrPresent Date, CalendarおよびJSR-310 Date and Time APIで提供されるクラス
現在または過去であるか検証する。(2.0から追加)
Dateのように日時を持つ型では過去日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では過去日付であるか検証する。
@PastOrPresent
private Date eventDate;
@Email 任意のCharSequenceインタフェースの実装クラスに適用可能
E-mailアドレスとして妥当であること検証する。(2.0から追加)
@Email
private String email;
@Valid 任意の非プリミティブ型 関連付けられているオブジェクトについて、再帰的に検証を行う。
@Valid
private List<Employer> employers;

@Valid
private Dept dept;

Warning

@Sizeアノテーションでは、サロゲートペアと呼ばれるchar型2つ(32ビット)で表される文字に対する考慮がされていない。

サロゲートペアを含む文字列をチェック対象とした場合、カウントした文字数が実際の入力文字数より多くカウントされる可能性があるため注意すること。

サロゲートペアを含む文字列の文字列長については、 文字列長の取得 を参照されたい。

Warning

E-mailの形式はRFC2822で定義されているが、@Emailは厳密にRFC2822に準拠していることをチェックするものではない。

例えばマルチバイト文字(全角文字)を含んでいても@Emailでのチェックをパスすることが確認されている。 また、実際に利用されているEmailアドレスも、必ずしもRFC2822に厳密に準拠しているわけではない。

これらの注意点を考慮した上で、利用・サポートするSMTPサーバなどによって適切なルールでの入力チェックを実装することを推奨する。 実装の際は、既存ルールを組み合わせたBean Validationアノテーションの作成を参照されたい。

Warning

@Past@Future@PastOrPresent@FutureOrPresentアノテーションでは検証をcompareToメソッドで行っており、検証対象の型により日付のみ検証するか日時を検証するかが異なる。

このため、日付項目にDate型を利用していると以下のような事象が発生する可能性がある。

  • 現在日付を入力しているにも関わらず@Pastでのチェックをパスしてしまう。
  • 現在日付を入力しているにも関わらず@FutureOrPresentでのチェックをパスできない。

日付時刻の検証を行う場合は適切な型を利用するよう留意されたい。

Note

Bean Validationが提供するClockProviderを実装することで、@Past@Future@PastOrPresent@FutureOrPresentの基準となる日付を変更することが出来る。

なお、実装したClockProviderを適用するには、LocalValidatorFactoryBeanの継承クラスを作成し、postProcessConfigurationメソッドをオーバーライドすれば良い。

ClockProviderを実装したクラスの例に関しては、Hibernate Validator Reference Guide(ClockProvider and temporal validation tolerance)を参照されたい。


4.1.4.1.2. Hibernate Validatorのチェックルール

Hibernate Validatorの代表的なアノテーション(org.hibernate.validator.constraints.*)を以下に示す。

詳細は、Hibernate Validator仕様を参照されたい。

アノテーション 対象の型 説明 使用例
@CreditCardNumber 任意のCharSequenceインタフェースの実装クラスに適用可能
Luhnアルゴリズムでクレジットカード番号が妥当かどうかを検証する。使用可能な番号かどうかをチェックするわけではない。
ignoreNonDigitCharacters = trueを指定する事で、数字以外の文字を無視して検証する事ができる。
@CreditCardNumber
private String cardNumber;
@ISBN 任意のCharSequenceインタフェースの実装クラスに適用可能
ISBNの形式として妥当であること(番号の長さとチェックディジット)を検証する。
typeを指定する事で、ISBNの形式(ISBN-10とISBN-13)の選択が出来る。デフォルトではISBN-13となる。
検証時には、ISBN以外のすべての文字(0-9までの数字とX以外の文字)は無視される。
このため、番号の一部を”-“を利用して区切ることが出来る。(例:978-161-729-045-9)
@ISBN
private String bookNumber;
@URL 任意のCharSequenceインタフェースの実装クラスに適用可能 URLとして妥当であること検証する。java.net.URLのコンストラクタを使用して文字列検証を行っており、 URLとして妥当とされるプロトコルはJVMがサポートするプロトコル(http,https,file,jarなど)に依存する。
@URL
private String url;

Warning

従来、Hibernate Validatorの独自アノテーションであった@Email@NotBlank@NotEmptyは、Bean Validation 2.0よりデフォルトで提供されるようになった。 これに伴い、Hibernate Validator 6.0より、Hibernate Validatorが提供する@Email@NotBlank@NotEmptyは非推奨となった。 引き続き使用することは出来るが、Bean Validationで提供されるアノテーションを使用することを推奨する。

Tip

@URLにて、JVMがサポートしていないプロトコルについても妥当として検証したい場合、Hibernateから提供されているorg.hibernate.validator.constraintvalidators.RegexpURLValidatorを使用する。 当該クラスは@URLアノテーションに対応するValidatorクラスで、URL形式であるかを正規表現で検証しており、JVMがサポートしていないプロトコルについても妥当として検証可能である。

  • アプリケーション全体の@URLのチェックルールを変更してもよい場合には、JavaDocに記載されているように、XMLにてValidatorクラスをRegexpURLValidatorに変更する。
  • 一部の項目だけに正規表現による検証を適用し、@URLはデフォルトのルールを使用したい場合には、新規アノテーション、およびRegexpURLValidatorと同様の検証を行うjakarta.validation.ConstraintValidator実装クラスを作成し、必要な項目に作成したアノテーションによる検証を適用する。

など、用途に応じた適用を行えばよい。

XMLによるチェックルール変更の詳細についてはHibernateのリファレンスを、新規アノテーションの作成方法については、新規ルールを実装したBean Validationアノテーションの作成をそれぞれ参照されたい。


4.1.4.1.3. Hibernate Validatorが用意するデフォルトメッセージ

hibernate-validator-<version>.jar内のorg/hibernate/validatorに、ValidationMessages.propertiesのデフォルト値が定義されている。

各言語に対応したValidationMessages.propertiesが定義されており、国際化の仕組みによりメッセージが決定される。
対応する言語については org/hibernate/validatorを参照されたい。
jakarta.validation.constraints.AssertFalse.message     = must be false
jakarta.validation.constraints.AssertTrue.message      = must be true
jakarta.validation.constraints.DecimalMax.message      = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
jakarta.validation.constraints.DecimalMin.message      = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
jakarta.validation.constraints.Digits.message          = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
jakarta.validation.constraints.Email.message           = must be a well-formed email address
jakarta.validation.constraints.Future.message          = must be a future date
jakarta.validation.constraints.FutureOrPresent.message = must be a date in the present or in the future
jakarta.validation.constraints.Max.message             = must be less than or equal to {value}
jakarta.validation.constraints.Min.message             = must be greater than or equal to {value}
jakarta.validation.constraints.Negative.message        = must be less than 0
jakarta.validation.constraints.NegativeOrZero.message  = must be less than or equal to 0
jakarta.validation.constraints.NotBlank.message        = must not be blank
jakarta.validation.constraints.NotEmpty.message        = must not be empty
jakarta.validation.constraints.NotNull.message         = must not be null
jakarta.validation.constraints.Null.message            = must be null
jakarta.validation.constraints.Past.message            = must be a past date
jakarta.validation.constraints.PastOrPresent.message   = must be a date in the past or in the present
jakarta.validation.constraints.Pattern.message         = must match "{regexp}"
jakarta.validation.constraints.Positive.message        = must be greater than 0
jakarta.validation.constraints.PositiveOrZero.message  = must be greater than or equal to 0
jakarta.validation.constraints.Size.message            = size must be between {min} and {max}


org.hibernate.validator.constraints.CreditCardNumber.message        = invalid credit card number
org.hibernate.validator.constraints.Currency.message                = invalid currency (must be one of {value})
org.hibernate.validator.constraints.EAN.message                     = invalid {type} barcode
org.hibernate.validator.constraints.Email.message                   = not a well-formed email address
org.hibernate.validator.constraints.ISBN.message                    = invalid ISBN
org.hibernate.validator.constraints.Length.message                  = length must be between {min} and {max}
org.hibernate.validator.constraints.CodePointLength.message         = length must be between {min} and {max}
org.hibernate.validator.constraints.LuhnCheck.message               = the check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod10Check.message              = the check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod11Check.message              = the check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
org.hibernate.validator.constraints.ModCheck.message                = the check digit for ${validatedValue} is invalid, {modType} checksum failed
org.hibernate.validator.constraints.NotBlank.message                = may not be empty
org.hibernate.validator.constraints.NotEmpty.message                = may not be empty
org.hibernate.validator.constraints.ParametersScriptAssert.message  = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.Range.message                   = must be between {min} and {max}
org.hibernate.validator.constraints.SafeHtml.message                = may have unsafe html content
org.hibernate.validator.constraints.ScriptAssert.message            = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.UniqueElements.message          = must only contain unique elements
org.hibernate.validator.constraints.URL.message                     = must be a valid URL

org.hibernate.validator.constraints.br.CNPJ.message                 = invalid Brazilian corporate taxpayer registry number (CNPJ)
org.hibernate.validator.constraints.br.CPF.message                  = invalid Brazilian individual taxpayer registry number (CPF)
org.hibernate.validator.constraints.br.TituloEleitoral.message      = invalid Brazilian Voter ID card number

org.hibernate.validator.constraints.pl.REGON.message                = invalid Polish Taxpayer Identification Number (REGON)
org.hibernate.validator.constraints.pl.NIP.message                  = invalid VAT Identification Number (NIP)
org.hibernate.validator.constraints.pl.PESEL.message                = invalid Polish National Identification Number (PESEL)

org.hibernate.validator.constraints.time.DurationMax.message        = must be shorter than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
org.hibernate.validator.constraints.time.DurationMin.message        = must be longer than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}

Note

Hibernate Validator 6.1.0より日本語メッセージに対応している。

4.1.4.2. 共通ライブラリが用意する入力チェックルール

共通ライブラリでは、独自の検証用アノテーションを提供している。 ここでは、共通ライブラリで提供しているアノテーションを使用した入力チェックルールの指定方法について説明する。

4.1.4.2.1. terasoluna-gfw-commonのチェックルール

terasoluna-gfw-commonが提供するアノテーション(org.terasoluna.gfw.common.codelist.*)を以下に示す。

アノテーション 対象の型 説明 使用例
@ExistInCodeList
Character
CharSequenceの実装クラス
(String, StringBuilderなど)
Numberの継承クラス
(Integer, Longなど)
値がコードリストに含まれているかどうかを検証する。 @ExistInCodeList参照

4.1.4.2.2. terasoluna-gfw-codepointsのチェックルール

terasoluna-gfw-codepointsが提供するアノテーション(org.terasoluna.gfw.common.codepoints.*)を以下に示す。なお、terasoluna-gfw-codepointsはバージョン5.1.0.RELEASE以上で利用することができる。

アノテーション 対象の型 説明 使用例
@ConsistOf
CharSequenceの実装クラス
(String, StringBuilderなど)
チェック対象の文字列が指定したコードポイント集合に全て含まれるかどうかを検証する。 @ConsistOf参照

4.1.4.2.3. terasoluna-gfw-validatorのチェックルール

terasoluna-gfw-validatorが提供するアノテーション(org.terasoluna.gfw.common.validator.constraints.*)を以下に示す。なお、terasoluna-gfw-validatorはバージョン5.1.0.RELEASE以上で利用することができる。

アノテーション 対象の型 説明 使用例
@ByteMin
CharSequenceの実装クラス
(String, StringBuilderなど)
値のバイト長が最小値以上であることを検証する。

[アノテーションの属性]
long value - バイト長の最小値を指定する。
String charset - 値をバイトシーケンスに符号化する際に使用する文字セットを指定する。デフォルト値はUTF-8

@ByteMin(value = 1, charset = “Shift_JIS”)
private String id;
@ByteMax
CharSequenceの実装クラス
(String, StringBuilderなど)
値のバイト長が最大値以下であることを検証する。

[アノテーションの属性]
long value - バイト長の最大値を指定する。
String charset - 値をバイトシーケンスに符号化する際に使用する文字セットを指定する。デフォルト値はUTF-8
@ByteMax(100)
private String id;
@ByteSize
CharSequenceの実装クラス
(String, StringBuilderなど)
値のバイト長が最小値と最大値の範囲内であることを検証する。(5.4.2から追加
@ByteMin@ByteMaxを組み合わせて使う場合は、こちらを使うことを推奨する。

[アノテーションの属性]
long min - バイト長の最小値を指定する。デフォルト値は0
long max - バイト長の最大値を指定する。デフォルト値はLong.MAX_VALUE
String charset - 値をバイトシーケンスに符号化する際に使用する文字セットを指定する。デフォルト値はUTF-8
@ByteSize(min = 1, max = 100)
private String id;
@Compare
Comparableインタフェースの実装クラスをプロパティにもつ任意のJavaBeanに適用可能
指定したプロパティの値の比較結果が正しいことを検証する。

[アノテーションの属性]
String left - オブジェクト内の比較元としたいプロパティ名を指定する。検証エラーとなった場合は、このプロパティにメッセージを表示される。
String right - オブジェクト内の比較先としたいプロパティ名を指定する。
org.terasoluna.gfw.common.validator.constraints.Compare.Operator operator - 比較方法を示す列挙型Operatorの値を指定する。指定可能な値は以下の通り。
  • EQUAL : left = rightである
  • NOT_EQUAL : left != rightである
  • GREATER_THAN : left > rightである
  • GREATER_THAN_OR_EQUAL : left >= rightである
  • LESS_THAN : left < rightである
  • LESS_THAN_OR_EQUAL : left <= rightである
NOT_EQUALは、terasoluna-gfw-validator 5.3.2.RELEASE以上で利用可能な値である。

boolean requireBoth - left属性とright属性で指定したフィールドの両方が入力されている(nullでない)必要があるかどうかを指定する。
  • true : どちらか一方だけ入力されている場合は検証エラーとする。ただし、両方とも未入力の場合は検証成功とする
  • false : どちらか一方でも入力されている場合は検証成功とする(デフォルト)
org.terasoluna.gfw.common.validator.constraints.Compare.Node node - エラーメッセージを出力するパスを示す列挙型Nodeの値を指定する。指定可能な値は以下の通り。
  • PROPERTY : left属性で指定したフィールドのエラーとして出力する(デフォルト)
  • ROOT_BEAN : チェックを実施したオブジェクトのエラーとして出力する

メールアドレスと確認用に入力したメールアドレスが一致することをチェックし、フォーム全体のエラーメッセージとして表示する場合、以下のように実装する。

@Compare(left = "email",
        right = "confirmEmail",
        operator = Compare.Operator.EQUAL,
        requireBoth = true,
        node = Compare.Node.ROOT_BEAN)
public class UserRegisterForm {
    private String email;
    private String confirmEmail;
}

期間の開始日と終了日が両方入力された場合は、開始日が終了日以前であることをチェックし、期間の開始日にエラーメッセージを表示する場合、以下のように実装する。

@Compare(left = "form",
        right = "to",
        operator = Compare.Operator.LESS_THAN_OR_EQUAL)
public class Period {
    private Date from;
    private Date to;
}

Note

相関項目チェックにおける入力必須について

単項目チェックにおいては、入力フィールドが入力されている( nullでない)かどうかは @NotNullを併用してチェックすればよい。しかし、相関項目チェックにおいては、「どちらか一方でも入力した場合は、もう一方の入力を強制する」といった、 @NotNullの併用だけでは実現できない場合がある。このため、@Compareでは、チェック対象の入力必須を制御する requireBoth属性を提供しており、これを併用して要件に応じたチェックを実装することができる。

なお、入力フィールドが未入力の場合に nullがバインドされる場合のみ、 requireBoth属性が利用できる。Spring MVCでは文字列の入力フィールドに未入力の状態でフォームを送信した場合、デフォルトでは、フォームオブジェクトにnullではなく、空文字がバインドされることに注意しなければならない。 文字列フィールドが未入力の場合に、空文字ではなく、nullをフォームオブジェクトにバインドするには、文字列フィールドが未入力の場合にnullをバインドするを参照されたい。

期間の開始日が終了日以前であることのチェックを例に、想定されるチェック要件と設定の例を以下に示す。

チェック要件 設定例
fromtoがともに必須で、fromtoの比較チェックを行う。

fromto@NotNullを付与し、 requireBoth属性はデフォルト値( false)を使用する。

@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL)
public class Period {
  @NotNull
  LocalDate from;
  @NotNull
  LocalDate to;
}
fromだけ必須だが、 toも入力された時は比較チェックする。

fromにだけ @NotNullを付与し、 requireBoth属性はデフォルト値( false)を使用する。

@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL)
public class Period {
  @NotNull
  LocalDate from;
  LocalDate to;
}
fromtoがともに必須ではなく、 fromtoが両方入力された時だけ比較チェックする。どちらか一方だけが入力された場合は比較チェックを行わない。

@NotNullは付与せず、 requireBoth属性はデフォルト値( false)を使用する。

@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL)
public class Period {
  LocalDate from;
  LocalDate to;
}
fromtoがともに必須ではないが、 fromtoのどちら一方でも入力した場合は、必ず両方入力して比較チェックを行う。

@NotNullは付与せず、 requireBoth属性に trueを設定する。

@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL, requireBoth = true)
public class Period {
  LocalDate from;
  LocalDate to;
}

4.1.4.2.4. 共通ライブラリが用意するデフォルトメッセージ

共通ライブラリの各Jar内のContributorValidationMessages.propertiesファイルに、ValidationMessages.propertiesのデフォルト値が定義されている。

各言語に対応したValidationMessages.propertiesが定義されており、国際化の仕組みによりメッセージが決定される。
対応する言語については各ライブラリ(terasoluna-gfw-commonのjarterasoluna-gfw-codepointsのjarterasoluna-gfw-validatorのjar)を参照されたい。
# terasoluna-gfw-common
org.terasoluna.gfw.common.codelist.ExistInCodeList.message = Does not exist in {codeListId}

# terasoluna-gfw-codepoints
org.terasoluna.gfw.common.codepoints.ConsistOf.message = not consist of specified code points

# terasoluna-gfw-validator
org.terasoluna.gfw.common.validator.constraints.ByteMin.message = must be greater than or equal to {value} bytes
org.terasoluna.gfw.common.validator.constraints.ByteMax.message = must be less than or equal to {value} bytes
org.terasoluna.gfw.common.validator.constraints.ByteSize.message = must be between {min} and {max} bytes
org.terasoluna.gfw.common.validator.constraints.Compare.message = invalid combination of {left} and {right}

Note

共通ライブラリでは、共通ライブラリの各Jar内のContributorValidationMessages.propertiesファイルで提供している。

ContributorValidationMessages.propertiesファイルはHibernate Validatorのメッセージ定義ファイルである。

他のBean Validation実装ライブラリを利用する場合はデフォルトメッセージが適用されないことに注意されたい。

4.1.4.2.5. 共通ライブラリのチェックルールの適用方法

以下の手順で、共通ライブラリのチェックルールを適用する。

使用したいチェックルールに応じて、依存ライブラリを追加する。terasoluna-gfw-validatorを追加する例を以下に示す。
なお、terasoluna-gfw-commonはブランクプロジェクトのデフォルト設定で使用可能であり、依存ライブラリを追加する必要はない。
<dependencies>
    <dependency>
        <groupId>org.terasoluna.gfw</groupId>
        <artifactId>terasoluna-gfw-validator</artifactId>
    </dependency>
</dependencies>

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

あとは基本的な単項目チェックで説明したように、JavaBeanのプロパティにアノテーションを付与すればよい。

Note

Bean Validationでは、アノテーションの属性値の不正により検証が実行できない場合、jakarta.validation.ValidationExceptionがスローされる。スタックトレースに出力される原因を参照し、属性値を適切な値に修正すること。

詳細は、Bean Validation specification(Exception model)を参照されたい。

4.1.4.2.6. 共通ライブラリのチェックルールの拡張方法

共通ライブラリで提供しているチェックルールを利用して、任意のルールを作成することができる。

以下では、相関項目チェックルールで独自に実装した@Confirmアノテーションを、共通ライブラリで提供しているチェックルールを利用して作成する例を紹介する。

既存ルールを組み合わせたBean Validationアノテーションの作成で説明したように、@Compareを利用して@Confirmアノテーションを作成する。

package com.example.sample.domain.validation;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.terasoluna.gfw.common.validator.constraints.Compare;

import com.example.sample.domain.validation.Confirm.List;

import jakarta.validation.Constraint;
import jakarta.validation.OverridesAttribute;
import jakarta.validation.Payload;

@Documented
@Constraint(validatedBy = {})
@Target({ TYPE, ANNOTATION_TYPE, TYPE_USE }) // (1)
@Retention(RUNTIME)
@Repeatable(List.class)
@Compare(left = "", right = "", operator = Compare.Operator.EQUAL, requireBoth = true) // (2)
public @interface Confirm {

    @OverridesAttribute(constraint = Compare.class, name = "message") // (3)
    String message() default "{com.example.sample.domain.validation.Confirm.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @OverridesAttribute(constraint = Compare.class, name = "right") // (4)
    String field();

    @OverridesAttribute(constraint = Compare.class, name = "left") // (5)
    String confirmField();

    @Documented
    @Target({ TYPE, ANNOTATION_TYPE, TYPE_USE })
    @Retention(RUNTIME)
    @interface List {
        Confirm[] value();
    }
}
項番 説明
(1)
このアノテーションを付与できる場所を、クラスまたはアノテーションに限定する。
(2)
@Compareアノテーションのoperator属性にCompare.Operator.EQUAL(同値であること)を指定する。どちらか一方が未入力の場合はエラーとするため、requireBoth属性にtrueを指定する。
(3)
@Compareアノテーションのmessage属性をオーバーライドし、エラー時にmessage属性に指定したメッセージが使用されるようにする。
(4)
@Compareアノテーションのright属性をオーバーライドし、属性名をfieldに変更する。
(5)
同様にleft属性をオーバーライドし、属性名をconfirmFieldに変更する。

Note

既存ルールを組み合わせたBean Validationアノテーションの作成」では@ReportAsSingleViolationを付与する方法を紹介しているが、@ReportAsSingleViolationを付与するとラップされた@Compareのエラーメッセージは使用されず、@Confirmのエラーメッセージのみが表示される。@Confirmはフォームオブジェクトに対する入力チェックであるため、エラーメッセージはフォームオブジェクトに割り当てられ、実際に表示したいfield属性に指定したフィールドには割り当てられない。

これを回避するためには、@ReportAsSingleViolationを付与せず、@Confirmmessage属性で@Comparemessage属性をオーバーライドする必要がある。これにより、@Compareのルールに従いleft属性(つまり@Confirmfield属性)に@Confirmのエラーメッセージを割り当てることができるようになる。

相関項目チェックルールで実装したアノテーションの代わりに、上記で作成したアノテーションを使用する。

package com.example.sample.app.validation;

import java.io.Serializable;

import com.example.sample.domain.validation.Confirm;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

@Confirm(field = "password", confirmField = "confirmPassword") // (1)
public class PasswordResetForm implements Serializable {
    private static final long serialVersionUID = 1L;

    @NotNull // (2)
    @Size(min = 8)
    private String password;

    @NotNull // (3)
    private String confirmPassword;

    // omitted geter/setter
}
項番 説明
(1)
クラスレベルに@Confirmアノテーションを付与する。
(2)
passwordフィールドがnullの場合は@Confirmの検証はパスするため、nullチェックは@NotNullアノテーションを付与して行う。
(3)
同様にconfirmPasswordフィールドにも、@NotNullアノテーションを付与する。

4.1.4.3. 型のミスマッチ

フォームオブジェクトのString以外のフィールドに対して、変換不可能な値を送信した場合はorg.springframework.beans.TypeMismatchExceptionがスローされる。

「新規ユーザー登録」処理の例では「Age」フィールドはIntegerで定義されているが、このフィールドに対して整数に変換できない値を入力すると、以下のようなエラーメッセージが表示される。

../../_images/validations-typemismatch1.png

例外の原因がそのまま表示されてしまい、エラーメッセージとしては不適切である。 型がミスマッチの場合のエラーメッセージは、org.springframework.context.MessageSourceが読み込むpropertiesファイル(application-messages.properties)に定義できる。

以下のルールで、エラーメッセージを定義すればよい。

メッセージキー メッセージ内容 用途
typeMismatch 型ミスマッチエラーのデフォルトメッセージ システム全体のデフォルト値
typeMismatch.対象のFQCN 特定の型ミスマッチエラーのデフォルトメッセージ システム全体のデフォルト値
typeMismatch.フォーム属性名.プロパティ名 特定のフォームのフィールドに対する型ミスマッチエラーのメッセージ 画面毎に変更したいメッセージ

application-messages.propertiesに以下の定義を行った場合、

# typemismatch
typeMismatch="{0}" is invalid.
typeMismatch.int="{0}" must be an integer.
typeMismatch.double="{0}" must be a double.
typeMismatch.float="{0}" must be a float.
typeMismatch.long="{0}" must be a long.
typeMismatch.short="{0}" must be a short.
typeMismatch.java.lang.Integer="{0}" must be an integer.
typeMismatch.java.lang.Double="{0}" must be a double.
typeMismatch.java.lang.Float="{0}" must be a float.
typeMismatch.java.lang.Long="{0}" must be a long.
typeMismatch.java.lang.Short="{0}" must be a short.
typeMismatch.java.util.Date="{0}" is not a date.

# filed names
name=Name
email=Email
age=Age

エラーメッセージは、次のように変更される。

../../_images/validations-typemismatch2.png
application-messages.propertiesに定義するメッセージで説明したように、{0}でフィールド名を埋めることができる。
基本的にデフォルトメッセージは定義しておくこと

Tip

メッセージキーのルールの詳細は、DefaultMessageCodesResolverのJavadocを参照されたい。

4.1.4.4. 文字列フィールドが未入力の場合にnullをバインドする

これまで説明してきたように、Spring MVCでは文字列の入力フィールドに未入力の状態でフォームを送信した場合、 デフォルトでは、フォームオブジェクトにnullではなく、空文字がバインドされる。

この場合、「未入力は許容するが、入力された場合は6文字以上であること」という要件を、既存のアノテーションで満たすことができない。

文字列フィールドが未入力の場合に、空文字ではなく、nullをフォームオブジェクトにバインドするには、
以下のようにorg.springframework.beans.propertyeditors.StringTrimmerEditorを使用すればよい。
@Controller
@RequestMapping("xxx")
public class XxxController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // bind empty strings as null
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }

    // omitted ...
}

この設定により、Controller毎に空文字をnullとみなすかどうかを設定できる。

プロジェクト全体で空文字をnullにしたい場合は、プロジェクト共通設定として@ControllerAdviceで設定すればよい。

@ControllerAdvice
public class XxxControllerAdvice {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // bind empty strings as null
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }

    // omitted ...
}
この設定を行った場合は、フォームオブジェクトの文字列フィールドに設定される空文字がすべてnullになる。
したがって、必須チェックに、かならず@NotNullが必要であることに注意しないといけない。

Tip

Spring Framework 4.0 より追加された@ControllerAdviceアノテーションの属性について

@ControllerAdviceアノテーションの属性を指定することで、@ControllerAdviceが付与されたクラスで実装したメソッドを適用するControllerを柔軟に指定できるように改善されている。

属性の詳細については、@ControllerAdviceの属性を参照されたい。


4.1.4.5. Native to Asciiを行わないメッセージの読み込み

Native to Asciiを行わずにBean Validationのメッセージ(ValidationMessage.properties)を読み込む方法紹介する。

日本語メッセージをNative to Asciiせずに直接扱いたい場合、Springの MessageSourceと連携すると簡単に実装することができる。

以下のように定義すると、MessageSourceの機能で読み込まれたメッセージがHibernate Validatorの中で 使用されるようになる。

  • Bean定義

    *-domain.xml

<!-- (1) -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <property name="validationMessageSource">
        <!-- (2) -->
        <bean class="org.springframework.context.support.ResourceBundleMessageSource">
            <property name="basenames">
                <list>
                    <value>ValidationMessages</value> <!-- (3) -->
                </list>
            </property>
            <property name="defaultEncoding" value="UTF-8" />
        </bean>
    </property>
</bean>

<!-- (4) -->
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
    <property name="validator" ref="validator" />
</bean>

spring-mvc.xml

<!-- (5) -->
<mvc:annotation-driven validator="validator">
    <!-- ommited -->
</mvc:annotation-driven>

<!-- (6) -->
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
    <property name="validator" ref="validator" />
</bean>
項番 説明
(1)
LocalValidatorFactoryBeanをBean定義する。
(2)
MessageSourceの定義。ここではResourceBundleMessageSourceを使用する。
(3)
ApplicationContextに読み込ませるリソースバンドルを指定する。
(4)
Method Validationを利用する際には、MethodValidationPostProcessorvalidatorプロパティに(1)で定義したBeanを指定する。
Method Validationを利用しない場合、このBean定義は不要である。
(5)
<mvc:annotation-driven>要素のvalidator属性に、(1)で定義したBeanを指定する。
(6)
(4)と同様である。

Note

MessageSourceの機能を利用することで、 プロパティファイルの配置先がクラスパス直下に制限されなくなる。また、複数のプロパティファイルを指定することもできるようになる。

4.1.4.6. OSコマンドインジェクション対策

ここでは、セキュリティ脆弱性の一種であるOSコマンドインジェクションとその対策について説明する。

4.1.4.6.1. OSコマンドインジェクションとは

OSコマンドインジェクションとは、アプリケーション内でユーザー入力文字列からコマンド実行文字列を組み立てている箇所がある場合に、 ユーザー入力文字列の中に悪意のあるコマンドが送り込まれると、コンピュータを不正に操られてしまう問題である。

Tip

詳細は、OWASPの解説ページなどを参照されたい。

JavaではProcessBuilderクラスや、Runtimeクラスのexecメソッドを用いてコマンドを実行する際に、実行するコマンドとして以下のものを利用する場合に、 OSコマンドインジェクションが発生する可能性がある。

  • /bin/sh(Unix系の場合)やcmd.exe(Windowsの場合)
  • ユーザーが入力した文字列

以下では、/bin/shを利用する場合にOSコマンドインジェクションが発生する例を示す。

ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", script); // (1)
Process p = pb.start();
項番 説明
(1)
例えば、scriptに”exec.sh ; cat /etc/passwd” が入ると、文字列中のセミコロンが/bin/shにより区切り文字として解釈され、”cat /etc/passwd”が実行される。
そのため、標準出力の扱い方によっては/etc/passwdが出力される可能性がある。

Warning

ScriptEngineやScriptTemplateViewResolverの利用について

Java SE 6より追加されたScriptEngineや、Spring Framework 4.2より追加されたScriptTemplateViewResolverでは、 JVM上で別言語(RubyPythonなど)を使用することができる。

これらの機能を利用して別言語のコードを実行する場合、コードの書き方によってはOSコマンドインジェクションが発生する可能性があるため、 利用には十分注意すること。

4.1.4.6.2. 対策方法

OSコマンドインジェクションを起こさないためには、可能な限り外部プロセスの実行を避ける。ただし諸般の事情により外部プロセスの実行がどうしても必要な場合、 以下の対策を行った上で外部プロセス実行を実装すること。

  • 極力、/bin/sh(Unix系の場合)やcmd.exe(Windowsの場合)を使用したコマンド実行を行わない
  • ユーザーにより入力された文字が、アプリケーションとして許可されたものであるか、ホワイトリスト方式を用いてチェックする

以下では、ユーザーが入力したコマンドと引数が指定された文字列で構成されているかをホワイトリスト方式でチェックするルールの例を示す。

@Pattern(regexp = "batch0\\d\\.sh") // (1)
private String cmdStr;

@Pattern(regexp = "[\\w=_]+")  // (2)
private String arg;
項番 説明
(1)
コマンドとして batch0X.sh(Xは0から9までの半角数字)のみ許可するルールを指定する。
(2)
引数として、無害な文字である半角英数字(\w)、”=” 、”_” から構成された文字列のみ許可するルールを指定する。

Note

この例では、コマンドや引数にパスが含まれないようなルールとすることで、ディレクトリトラバーサルを起こさないようにしている。

@Patternを利用する場合、@Patternに指定された正規表現がそのままエラーメッセージとして出力され、 以下の点でメッセージとしては不適切である。

  • エラーの意味が不明確となり、ユーザに優しくない
  • 脆弱性への対策のためのロジックが利用者に露呈してしまう
../../_images/validations-os-command-injection.png

エラーの意味を明確にし、かつ、ロジックを隠蔽するために、application-messages.propertiesに適切なメッセージを定義する。 メッセージの定義方法については、application-messages.propertiesに定義するメッセージを参照されたい。

Pattern.cmdForm.cmdStr = permit command name: batch00.sh - batch09.sh
Pattern.cmdForm.arg = permit parameter characters and symbols: alphanumeric, =, _
../../_images/validations-os-command-injection2.png