4.1. 入力チェック

目次

4.1.1. Overview

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

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

がある。

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

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

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

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

Warning

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

Todo

クライアントサイドの入力チェックについては今後追記する。初版では、サーバーサイドの入力チェックのみ言及する。

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 1.1(Hibernate Validator 5.x)以上を使用する場合、 Bean ValidationのAPI仕様クラス(javax.validationパッケージのクラス)が格納されているjarファイルとHibernate Validatorのjarファイルに加えて、

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

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

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

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

<!-- (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

上記設定例では、依存ライブラリのバージョンは親プロジェクトで管理する前提である。 そのため、<version>要素は指定していない。

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文字以下
Email形式
age
java.lang.Integer
入力必須
1以上
200以下
  • フォームクラス

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

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    import org.hibernate.validator.constraints.Email;
    
    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でないことを示すjavax.validation.constraints.NotNullを付ける。

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

    上記の通り、Spring MVCではデフォルトで、未入力の文字列フィールドには、空文字がバインドされるため、
    1文字以上というルールが入力必須を表す。
    (3)
    対象のフィールドがRFC2822準拠のE-mail形式であることを示すorg.hibernate.validator.constraints.Emailを付ける。
    E-mail形式の要件がRFC2822準拠の制限よりも緩い場合は、@Emailを使用せず、javax.validation.constraints.Patternを用いて、正規表現を指定する必要がある。
    (4)
    数値の入力フィールドに未入力の状態でフォームを送信した場合、フォームオブジェクトにnull がバインドされるため、@NotNullageの入力必須条件を表す。
    (5)
    対象のフィールドが指定した数値の以上であることを示すjavax.validation.constraints.Minを付ける。
    (6)
    対象のフィールドが指定した数値の以下であることを示すjavax.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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @Controller
    @RequestMapping("user")
    public class UserController {
    
      @ModelAttribute
      public UserForm setupForm() {
        return new UserForm();
      }
    
      @RequestMapping(value = "create", method = RequestMethod.GET, params = "form")
      public String createForm() {
        return "user/createForm"; // (1)
      }
    
      @RequestMapping(value = "create", method = RequestMethod.POST, params = "confirm")
      public String createConfirm(@Validated /* (2) */ UserForm form, BindingResult /* (3) */ result) {
        if (result.hasErrors()) { // (4)
          return "user/createForm";
        }
        return "user/createConfirm";
      }
    
      @RequestMapping(value = "create", method = RequestMethod.POST)
      public String create(@Validated UserForm form, BindingResult result) { // (5)
        if (result.hasErrors()) {
          return "user/createForm";
        }
        // omitted business logic
        return "redirect:/user/create?complete";
      }
    
      @RequestMapping(value = "create", method = RequestMethod.GET, 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)
    入力内容確認画面から新規作成処理にリクエストを送る際にも、入力チェックを必ず再実行すること
    途中でデータを改ざんすることは可能であるため、必ず業務処理の直前で入力チェックは必要である。

    Note

    @Validatedは、Bean Validation標準ではなく、Springの独自アノテーションである。 Bean Validation標準のjavax.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の場合は正常な値とみなす。ただし、 以下のアノテーションを除く。

  • javax.validation.constraints.NotNull
  • org.hibernate.validator.constraints.NotEmpty
  • org.hibernate.validator.constraints.NotBlank

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

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

../../_images/validations-first-sample3.png
Nameの入力値は、チェック条件を満たすため、エラーメッセージが表示されない。
Emailの入力値は文字列長に関する条件は満たすが、Email形式ではないため、エラーメッセージが表示される。
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 javax.validation.Valid;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Pattern;
    import javax.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 javax.validation.constraints.NotNull;
    import javax.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を有効にするために、javax.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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @RequestMapping("order")
    @Controller
    public class OrderController {
    
        @ModelAttribute
        public OrderForm setupForm() {
            return new OrderForm();
        }
    
        @RequestMapping(value = "order", method = RequestMethod.GET, params = "form")
        public String orderForm() {
            return "order/orderForm";
        }
    
        @RequestMapping(value = "order", method = RequestMethod.POST, 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 javax.validation.Valid;
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    import org.hibernate.validator.constraints.Email;
    
    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)
    addressフィールドに対するエラーメッセージを表示する。
    (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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @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;
        }
    
        @RequestMapping(value = "create", method = RequestMethod.GET, params = "form")
        public String createForm() {
            return "user/createForm";
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, 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. バリデーションのグループ化

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

前述の「新規ユーザー登録」の例で、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 javax.validation.Valid;
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    import org.hibernate.validator.constraints.Email;
    
    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.List({ // (2)
                @Min(value = 18, groups = Chinese.class), // (3)
                @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; // (4)
    
        // omitted setter/getter
    }
    
    項番 説明
    (1)
    グループクラスを指定するために、各グループをインタフェースで定義する。
    (2)
    一つのフィールドに同じルールを複数指定するために、@Min.Listアノテーションを使用する。
    他のアノテーションを使用する場合も同様である。
    (3)
    各グループごとにルールを定義し、グループを指定するために、group属性に対象のグループクラスを指定する。
    group属性を省略した場合、javax.validation.groups.Defaultグループが使用される。
    (4)
    グループを振り分けるための、フィールドを追加する。
  • 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 javax.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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    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;
        }
    
        @RequestMapping(value = "create", method = RequestMethod.GET, params = "form")
        public String createForm() {
            return "user/createForm";
        }
    
        String createConfirm(UserForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "user/createForm";
            }
            return "user/createConfirm";
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, params = {
                "confirm",  /* (1) */ "country=cn" })
        public String createConfirmForChinese(@Validated({ /* (2) */ Chinese.class,
                Default.class }) UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, params = {
                "confirm", "country=jp" })
        public String createConfirmForJapanese(@Validated({ Japanese.class,
                Default.class }) UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, params = {
                "confirm", "country=sg" })
        public String createConfirmForSingaporean(@Validated({ Singaporean.class,
                Default.class }) UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    }
    
    項番 説明
    (1)
    グループを振り分けるためのパラメータの条件を、param属性に追加する。
    (2)
    ageフィールドの@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 javax.validation.Valid;
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    import javax.validation.groups.Default;
    
    import org.hibernate.validator.constraints.Email;
    
    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.List({
                @Min(value = 18, groups = Default.class), // (2)
                @Min(value = 20, groups = Japanese.class),
                @Min(value = 21, groups = Singaporean.class) })
        @Max(200)
        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)
    ageフィールドの@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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    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;
        }
    
        @RequestMapping(value = "create", method = RequestMethod.GET, params = "form")
        public String createForm() {
            return "user/createForm";
        }
    
        String createConfirm(UserForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "user/createForm";
            }
            return "user/createConfirm";
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, params = { "confirm" })
        public String createConfirmForDefault(@Validated /* (1) */ UserForm form,
                BindingResult result) {
            return createConfirm(form, result);
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, params = {
                "confirm", "country=jp" })
        public String createConfirmForJapanese(
                @Validated(Japanese.class)  /* (2) */ UserForm form, BindingResult result) {
            return createConfirm(form, result);
        }
    
        @RequestMapping(value = "create", method = RequestMethod.POST, 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

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

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

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

    @Inject
    SmartValidator smartValidator; // (1)

    // omitted

    @RequestMapping(value = "create", method = RequestMethod.POST, 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にロジックを書くべきではないため、@RequestMappingの属性でルールを切り替えられるのであれば、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と同じ値であること
パスワード
confirmPassword
java.lang.String
特になし
確認用パスワード

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

  • フォームクラス

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

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

    Note

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

  • Controllerクラス

    package com.example.sample.app.validation;
    
    import javax.inject.Inject;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.WebDataBinder;
    import org.springframework.web.bind.annotation.InitBinder;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @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)
        }
    
        @RequestMapping(value = "reset", method = RequestMethod.GET, params = "form")
        public String resetForm() {
            return "password/resetForm";
        }
    
        @RequestMapping(value = "reset", method = RequestMethod.POST)
        public String reset(@Validated PasswordResetForm form, BindingResult result) { // (3)
            if (result.hasErrors()) {
                return "password/resetForm";
            }
            return "redirect:/password/reset?complete";
        }
    
        @RequestMapping(value = "reset", method = RequestMethod.GET, 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フィールドにスタイルを適用し、passwordフィールドのみにエラーメッセージを表示する例を示す。

package com.example.sample.app.validation;

import org.springframework.stereotype.Component;
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 (!password.equals(confirmPassword)) {
            // register a field error for password
            errors.rejectValue("password",
                   "PasswordEqualsValidator.passwordResetForm.password",
                   "password and confirm password must be same.");

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

Note

一つのControllerで複数のフォームを扱う場合は、Validatorの対象を限定するために、@InitBinder("xxx")でモデル名を指定する必要がある。

@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から、メッセージキーに対応するメッセージを探す。
  1. メッセージキーに対応するメッセージが定義されている場合は、そのメッセージを使用する
  2. メッセージキーに対応するメッセージが定義されていない場合は、”{メッセージキー}”をそのままエラーメッセージとして使用する

基本的にエラーメッセージは、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ファイルの中に含めないように注意すること。

なお、version 1.0.2.RELEASE以降の ブランクプロジェクト からプロジェクトを生成した場合は、 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のエラーメッセージを変更する。

    javax.validation.constraints.NotNull.message=is required.
    # (1)
    javax.validation.constraints.Size.message=size is not in the range {min} through {max}.
    javax.validation.constraints.Min.message=can not be less than {value}.
    javax.validation.constraints.Max.message=can not be greater than {value}.
    org.hibernate.validator.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のエラーメッセージを変更する。

    javax.validation.constraints.NotNull.message="{0}" is required.
    javax.validation.constraints.Size.message=The size of "{0}" is not in the range {min} through {max}.
    javax.validation.constraints.Min.message="{0}" can not be less than {value}.
    javax.validation.constraints.Max.message="{0}" can not be greater than {value}.
    org.hibernate.validator.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 5.xでは、Expression Language 2.2以上をサポートしている。

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

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

# ...
# (1)
javax.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)を参照されたい。

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 java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
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.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
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 })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        Xxx[] value();
    }
}

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

システム共通で、

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

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

以下に、実装例を示す。

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

    package com.example.common.validation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import javax.validation.ReportAsSingleViolation;
    import javax.validation.constraints.Pattern;
    
    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.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @ReportAsSingleViolation // (1)
    @Pattern(regexp = "[a-zA-Z0-9]*") // (2)
    public @interface AlphaNumeric {
        String message() default "{com.example.common.validation.AlphaNumeric.message}"; // (3)
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            AlphaNumeric[] value();
        }
    }
    
    項番 説明
    (1)
    エラーメッセージをまとめ、エラー時はこのアノテーションによるメッセージだけを変えるようにする。
    (2)
    このアノテーションにより使用されるルールを定義する。
    (3)
    エラーメッセージのデフォルト値を定義する。
  • 正の数に限定する@NotNegativeアノテーションの実装例

    package com.example.common.validation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import javax.validation.ReportAsSingleViolation;
    import javax.validation.constraints.Min;
    
    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.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @ReportAsSingleViolation
    @Min(value = 0)
    public @interface NotNegative {
        String message() default "{com.example.common.validation.NotNegative.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            NotNegative[] value();
        }
    }
    
  • 「ユーザーID」のフォーマットを規定する@UserIdアノテーションの実装例

    package com.example.sample.domain.validation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import javax.validation.ReportAsSingleViolation;
    import javax.validation.constraints.Pattern;
    import javax.validation.constraints.Size;
    
    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.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @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 })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            UserId[] value();
        }
    }
    
  • 「年齢」の制限を規定する@Ageアノテーションの実装例

    package com.example.sample.domain.validation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import javax.validation.ReportAsSingleViolation;
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    
    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.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @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 })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            Age[] value();
        }
    }
    

    Note

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

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

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

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

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

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

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

例として、ISBN(International Standard Book Number)-13の形式をチェックするルールを挙げる。

  • アノテーション

    package com.example.common.validation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    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.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = { ISBN13Validator.class }) // (1)
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    public @interface ISBN13 {
        String message() default "{com.example.common.validation.ISBN13.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            ISBN13[] value();
        }
    }
    
    項番 説明
    (1)
    このアノテーションを使用したときに実行されるConstraintValidatorを指定する。複数指定することができる。
  • Validator

    package com.example.common.validation;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    public class ISBN13Validator implements ConstraintValidator<ISBN13, String> { // (1)
    
        @Override
        public void initialize(ISBN13 constraintAnnotation) { // (2)
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) { // (3)
            if (value == null) {
                return true; // (4)
            }
            return isISBN13Valid(value); // (5)
        }
    
        // This logic is written in http://en.wikipedia.org/wiki/International_Standard_Book_Number
        static boolean isISBN13Valid(String isbn) {
            if (isbn.length() != 13) {
                return false;
            }
            int check = 0;
            try {
                for (int i = 0; i < 12; i += 2) {
                    check += Integer.parseInt(isbn.substring(i, i + 1));
                }
                for (int i = 1; i < 12; i += 2) {
                    check += Integer.parseInt(isbn.substring(i, i + 1)) * 3;
                }
                check += Integer.parseInt(isbn.substring(12));
            } catch (NumberFormatException e) {
                return false;
            }
            return check % 10 == 0;
        }
    }
    
    項番 説明
    (1)
    ジェネリクスのパラメータに、対象のアノテーションとフィールドの型を指定する。
    (2)
    initializeメソッドに、初期化処理を実装する。
    (3)
    isValidメソッドで入力チェック処理を実装する。
    (4)
    入力値が、nullの場合は、正常とみなす。
    (5)
    ISBN-13の形式のチェックを行う。

Tip

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

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

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

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

ここでは、確認用フィールドの先頭に、「confirm」を付与する規約を設ける。

  • アノテーション

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

    package com.example.common.validation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.TYPE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = { ConfirmValidator.class })
    @Target({ TYPE, ANNOTATION_TYPE }) // (1)
    @Retention(RUNTIME)
    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)
    
        @Target({ TYPE, ANNOTATION_TYPE })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            Confirm[] value();
        }
    }
    
    項番 説明
    (1)
    このアノテーションが、クラスまたはアノテーションにのみ付加できるように、対象を絞る。
    (2)
    アノテーションに渡すパラメータを定義する。
  • Validator

    package com.example.common.validation;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    import org.springframework.beans.BeanWrapper;
    import org.springframework.beans.BeanWrapperImpl;
    import org.springframework.util.ObjectUtils;
    import org.springframework.util.StringUtils;
    
    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 = "confirm" + StringUtils.capitalize(field);
            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 = ObjectUtils.nullSafeEquals(fieldValue,
                    confirmFieldValue);
            if (matched) {
                return true;
            } else {
                context.disableDefaultConstraintViolation(); // (3)
                context.buildConstraintViolationWithTemplate(message)
                        .addPropertyNode(field).addConstraintViolation(); // (4)
                return false;
            }
        }
    
    }
    
    項番 説明
    (1)
    JavaBeanのプロパティにアクセスする際に便利なorg.springframework.beans.BeanWrapperを使用する。
    (2)
    BeanWrapper経由で、フォームオブジェクトからプロパティ値を取得する。
    (3)
    デフォルトのConstraintViolationオブジェクトの生成を無効にする。
    (4)
    独自ConstraintViolationオブジェクトを生成する。
    ConstraintValidatorContext.buildConstraintViolationWithTemplateで出力するメッセージを定義する。
    ConstraintViolationBuilder.addPropertyNodeでエラーメッセージを出力したいフィールド名を指定する。
    詳細は、ConstraintValidatorContextのJavaDocを参照されたい。

Tip

ConstraintViolationBuilder.addPropertyNodeメソッドは、Bean Validation 1.1 から追加されたメソッドである。

Bean Validation 1.0では ConstraintViolationBuilder.addNodeというメソッドを使用していたが、Bean Validation 1.1から非推奨のAPIとなっている。

Bean Validationの非推奨APIについては、Bean Validation API Document(Deprecated API)を参照されたい。

Note

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

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

// 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 field
            context.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(field).addConstraintViolation();

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

            return false;
        }
    }

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

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

  • フォームクラス

    package com.example.sample.app.validation;
    
    import java.io.Serializable;
    
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    
    import com.example.common.validation.Confirm;
    
    @Confirm(field = "password") // (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.ModelAttribute;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    @Controller
    @RequestMapping("password")
    public class PasswordResetController {
    
        @ModelAttribute
        public PasswordResetForm setupForm() {
            return new PasswordResetForm();
        }
    
        @RequestMapping(value = "reset", method = RequestMethod.GET, params = "form")
        public String resetForm() {
            return "password/resetForm";
        }
    
        @RequestMapping(value = "reset", method = RequestMethod.POST)
        public String reset(@Validated PasswordResetForm form, BindingResult result) {
            if (result.hasErrors()) {
                return "password/resetForm";
            }
            return "redirect:/password/reset?complete";
        }
    
        @RequestMapping(value = "reset", method = RequestMethod.GET, 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 java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    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.RetentionPolicy.RUNTIME;
    
    @Documented
    @Constraint(validatedBy = { UnusedUserIdValidator.class })
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    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 })
        @Retention(RUNTIME)
        @Documented
        public @interface List {
            UnusedUserId[] value();
        }
    }
    
  • Validatorクラス

    package com.example.sample.domain.validation;
    
    import javax.inject.Inject;
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    import org.springframework.stereotype.Component;
    
    import com.example.sample.domain.service.user.UserService;
    
    @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 javax.validation.constraints.NotNull;

@Validated
public interface HelloService {

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

}
項番 説明
(1)

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

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

(2)

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

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


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

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

Note

ポイントは、@javax.validation.Validアノテーションを指定するという点である。 以下に、サンプルコード使って指定方法を詳しく説明する。

Serviceインタフェース

package com.example.domain.service;

import org.springframework.validation.annotation.Validated;

import javax.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値が指定された場合は、javax.validation.ConstraintViolationExceptionが発生する。

(2)

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

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

(3)

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

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

(4)

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

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


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

Input用のJavaBean

package com.example.domain.service;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import java.util.Date;

public class HelloInput {

    @NotNull
    @Past
    private Date visitDate;

    @NotNull
    private String visitMessage;

    private String userId;

    // ...

}

Output用のJavaBean

package com.example.domain.service;

import com.example.domain.model.User;

import java.util.Date;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.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 javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import java.util.Date;

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に指定された制約を満たさない場合はjavax.validation.ConstraintViolationExceptionが発生する。


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

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

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

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

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

package com.example.app;

import javax.validation.ConstraintViolationException;

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

@ControllerAdvice
public class ConstraintViolationExceptionHandler {

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

    // (1)
    @ExceptionHandler
    public String handleConstraintViolationException(ConstraintViolationException e){
        // (2)
        if (log.isErrorEnabled()) {
            log.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の標準アノテーション(javax.validation.*)を以下に示す。

詳細は、Bean Validation specificationの7章を参照されたい。

アノテーション 対象の型 説明 使用例
@NotNull 任意 対象のフィールドが、nullでないことを検証する。
@NotNull
private String id;
@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;
@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(Hibernate Validator実装ではJoda-Timeのクラスにも適用可能) 未来日付であるか検証する。
@Future
private Date eventDate;
@Past Date, Calendar(Hibernate Validator実装ではJoda-Timeのクラスにも適用可能) 過去日付であるか検証する。
@Past
private Date eventDate;
@Valid 任意の非プリミティブ型 関連付けられているオブジェクトについて、再帰的に検証を行う。
@Valid
private List<Employer> employers;

@Valid
private Dept dept;

Tip

@DecimalMin@DecimalMaxアノテーションの inclusive属性は、 Bean Validation 1.1 から追加された属性である。

inclusive属性のデフォルト値には true(指定した閾値と同じ値を許容する)が指定されており、 Bean Validation 1.0 との互換性が保たれている。

Warning

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

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

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

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

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

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

アノテーション 対象の型 説明 使用例
@CreditCardNumber 任意のCharSequenceインタフェースの実装クラスに適用可能
Luhnアルゴリズムでクレジットカード番号が妥当かどうかを検証する。使用可能な番号かどうかをチェックするわけではない。
ignoreNonDigitCharacters = trueを指定する事で、数字以外の文字を無視して検証する事ができる。
@CreditCardNumber
private String cardNumber;
@Email 任意のCharSequenceインタフェースの実装クラスに適用可能 RFC2822に準拠したEmailアドレスかどうか検証する。
@Email
private String email;
@URL 任意のCharSequenceインタフェースの実装クラスに適用可能 RFC2396に準拠しているかどうか検証する。
@URL
private String url;
@NotBlank 任意のCharSequenceインタフェースの実装クラスに適用可能 null、空文字("")、空白のみでないことを検証する。
@NotBlank
private String userId;
@NotEmpty CollectionMap、Array、任意のCharSequenceインタフェースの実装クラスに適用可能
null、または空でないことを検証する。
@NotNull + @Min(1)の組み合わせでチェックする場合は、@NotEmptyを使用すること。
@NotEmpty
private String password;

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

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

javax.validation.constraints.AssertFalse.message = must be false
javax.validation.constraints.AssertTrue.message  = must be true
javax.validation.constraints.DecimalMax.message  = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.DecimalMin.message  = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.Digits.message      = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
javax.validation.constraints.Future.message      = must be in the future
javax.validation.constraints.Max.message         = must be less than or equal to {value}
javax.validation.constraints.Min.message         = must be greater than or equal to {value}
javax.validation.constraints.NotNull.message     = may not be null
javax.validation.constraints.Null.message        = must be null
javax.validation.constraints.Past.message        = must be in the past
javax.validation.constraints.Pattern.message     = must match "{regexp}"
javax.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.EAN.message                     = invalid {type} barcode
org.hibernate.validator.constraints.Email.message                   = not a well-formed email address
org.hibernate.validator.constraints.Length.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.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

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

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

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

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

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

4.1.4.3.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.3.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
@ByteMax参照
@ByteMax
CharSequenceの実装クラス
(String, StringBuilderなど)
値のバイト長が最大値以下であることを検証する。

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

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

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

使用したいチェックルールに応じて、依存ライブラリを追加する。terasoluna-gfw-validatorを追加する例を以下に示す。

<dependencies>
    <dependency>
        <groupId>org.terasoluna.gfw</groupId>
        <artifactId>terasoluna-gfw-validator</artifactId>
    </dependency>
</dependencies>

次に、ValidationMessages.propertiesに定義するメッセージで説明したように ValidationMessages.properties に、アノテーションに対応する任意のメッセージ定義を追加する。

# (1)
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.Compare.message = invalid combination of {left} and {right}
項番 説明
(1)
アノテーションごとにメッセージ定義を追加する。アノテーションの属性値は、プレースフォルダー({属性名}の形式)を使用してメッセージの中に埋め込むことができる。

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

Note

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

詳細は、Bean Validation specificationの9章を参照されたい。

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

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

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

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

package com.example.sample.domain.validation;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;

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

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

    String message() default "{com.example.sample.domain.validation.Confirm.message}"; // (4)

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

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

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

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

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

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

package com.example.sample.app.validation;

import java.io.Serializable;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import com.example.common.validation.Confirm;

@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.4. 型のミスマッチ

フォームオブジェクトの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.5. 文字列フィールドが未入力の場合に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で設定すればよい。

Tip

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

@ControllerAdviceアノテーションの属性を指定することで、 @ControllerAdviceが付与されたクラスで実装したメソッドを適用するControllerを柔軟に指定できるように改善されている。 属性の詳細については、@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が必要であることに注意しないといけない。

4.1.4.6. 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.7. OSコマンドインジェクション対策

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

4.1.4.7.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.7.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