4.1. 入力チェック¶
目次
4.1.1. Overview¶
ユーザーが入力した値が不正かどうかを検証することは必須である。 入力値の検証は大きく分けて、
- 長さや形式など、文脈によらず入力値だけを見て、それが妥当かどうかを判定できる検証
- システムの状態によって入力値が妥当かどうかが変わる検証
がある。
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) | スタンドアロン環境で動かすプロジェクトの  上記例では、組込み用のApache Tomcat向けに提供されているライブラリを指定している。
 | 
| (2) | JUnitを実行するために依存ライブラリが必要になる場合は、スコープは testが適切である。 | 
Note
上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。 上記の依存ライブラリはterasoluna-gfw-parentが利用しているSpring IO Platformで定義済みである。
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がバインドされるため、- @NotNullが- ageの入力必須条件を表す。(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属性に、対象のフィールド名を指定する。この例では、フィールド毎に入力フィールドの横にエラーメッセージを表示する。
フォームは、以下のように表示される。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
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によるチェックは正常とみなされ、
エラーメッセージは出力されていない。
次に、フィールドに何らかの値を入力してフォームを送信する。
エラー時にスタイルを変更したい場合は、前述のフォームを、以下のように変更する。
<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;
}
エラー画面は、以下のように表示される。
画面の要件に応じて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;
}
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. 日時フォーマットのチェック¶
@DateTimeFormatアノテーションの使用を推奨する。@DateTimeFormatアノテーションの使用方法については、フィールド単位の日時型変換を参照されたい。@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文字以下 | 請求先住所 | 
receiverAddressとsenderAddressは、同じ項目であるため、同じフォームクラスを使用する。
- フォームクラス - 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)子フォーム自体が必須であることを示す。この設定がない場合、- receiverAddressに- nullが設定されても、正常とみなされる。(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.name、- receiverAddress.postcode、- receiverAddress.addressのすべてがリクエストパラメータとして送信されない場合、- receiverAddressが- nullとみなされ、この位置にエラーメッセージが表示される。(2)子フォームのフィールドは、- 親フィールド名.子フィールド名で指定する。
フォームは、以下のように表示される。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
ネストした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); } }); }); 
フォームは、以下のように表示される。
「Add address」ボタンを2回押して、住所フォームを2件追加する。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
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"; } }
項番 説明 SmartValidatorをインジェクションする。SmartValidatorは<mvc:annotation-driven>の設定が行われていれば使用できるため、別途Bean定義不要である。@Validatedアノテーションは使わない。SmartValidatorのvalidateメソッドを使用して、グループを指定したバリデーションを実行する。グループの指定は可変長引数になっており、複数指定できる。
基本的には、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フィールドとconfirmPasswordフィールドの両方の情報が必要であるため、相関項目チェックルールである。
- フォームクラス - 相関項目チェックルール以外は、これまで通り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フィールドに、別の値を入力してフォームを送信した場合は、以下のようにエラーメッセージが表示される。
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) } } }
項番 説明 confirmPasswordフィールドのエラーを登録する。
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のエラーメッセージは、以下の順で解決される。
- org.springframework.context.MessageSourceに定義されているメッセージの中に、ルールに合致するものがあればそれをエラーメッセージとして使用する (Springのルール)。Springのデフォルトのルールについては、「DefaultMessageCodesResolverのJavaDoc」を参照されたい。
- 1.でメッセージが見つからない場合、アノテーションの - message属性に、指定されたメッセージからエラーメッセージを取得する (Bean Validationのルール)
message属性に指定されたメッセージが、”{メッセージキー}”形式でない場合、そのテキストをエラーメッセージとして使用する。
message属性に指定されたメッセージが、”{メッセージキー}”形式の場合、クラスパス直下のValidationMessages.propertiesから、メッセージキーに対応するメッセージを探す。
- メッセージキーに対応するメッセージが定義されている場合は、そのメッセージを使用する
- メッセージキーに対応するメッセージが定義されていない場合は、”{メッセージキー}”をそのままエラーメッセージとして使用する
基本的にエラーメッセージは、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>
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)アノテーションに指定した属性値は、- {属性名}で埋め込むことができる。
この設定を加えた状態で、すべての入力フィールドを未入力のままフォームを送信すると、以下のように変更したエラーメッセージが、表示される。
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. 
エラーメッセージは、以下のように変更される。
このままでは、フォームクラスのプロパティ名が表示されてしまい、ユーザーフレンドリではない。 適切なフィールド名を表示したい場合は、application-messages.propertiesに
フォームのプロパティ名=表示するフィールド名
形式でフィールド名を定義すればよい。
これまでの例に、以下の設定を追加する。
- application-messages.properties - name=Name email=Email age=Age 
エラーメッセージは、以下のように変更される。
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} # ...
項番 説明 メッセージの中の 「
${inclusive == true ? 'or equal to ' : ''}」の部分がEL式である。上記のメッセージ定義から実際に生成されるメッセージのパターンは、
- must be less than or equal to {value}
- must be less than {value}
の2パターンとなる。(
{value}の部分には、@DecimalMaxアノテーションのvalue属性に指定した値が埋め込まれる)前者は
@DecimalMaxアノテーションのinclusive属性にtrueを指定した場合(又は指定しなかった場合)、 後者は@DecimalMaxアノテーションのinclusive属性にfalseを指定した場合に生成される。Bean ValidationにおけるEL式の扱いについては、 Hibernate Validator Reference Guide(Interpolation with message expressions)を参照されたい。
また、ValidationMessages.properties に指定するメッセージに ${validatedValue}を使用することで、エラーメッセージにチェック対象の値を含むことができる。
以下に、 ${validatedValue}の使用例を示す。
# ... # (1) javax.validation.constraints.Pattern.message = The value entered "${validatedValue}" is invalid. # ...
項番 説明 上記のメッセージ定義から実際に生成されるメッセージは、
${validatedValue}の部分にフォームに入力した値が埋め込まれる。 入力値に機密情報を含む場合、機密情報がメッセージに表示されないようにするため、${validatedValue}を使用しないように注意すること。詳細については、Hibernate Validator Reference Guide(Interpolation with message expressions)を参照されたい。
4.1.2.4.2. application-messages.propertiesに定義するメッセージ¶
ValidationMessages.propertiesでシステムで利用するデフォルトのメッセージを定義したが、 画面によっては、デフォルトメッセージから変更したい場合が出てくる。
その場合、application-messages.propertiesに、以下の形式でメッセージを定義する。
アノテーション名.フォーム属性名.プロパティ名=対象のメッセージ
ValidationMessages.propertiesに定義するメッセージの設定がある前提で、以下の設定でemailとageフィールドのメッセージを上書きする。
- 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を参照されたい。
エラーメッセージは以下のように変更される。
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. 相関項目チェックルール¶
以下では、「あるフィールドとその確認用フィールドの内容が一致すること」というルールを実現する例を挙げる。
ここでは、確認用フィールドの先頭に、「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; } } }
項番 説明 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. 業務ロジックチェック¶
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) | 
 
 | 
| (3) | 
 この設定がない場合は(1)で作成したものとは異なる | 
| (4) | 
 
 | 
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の対象となるインタフェースに、 上記例では、 | 
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の制約アノテーションをメソッドの引数アノテーションとして指定する。 
 | 
| (2) | Bean Validationの制約アノテーションをメソッドアノテーションとして指定する。 上記例では、返り値がNull値にならないことを示しており、
返り値としてNull値が返却された場合、 | 
次に、メソッドのシグネチャとして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の制約アノテーションをメソッドの引数アノテーションとして指定する。 
 | 
| (2) | 
 
 | 
| (3) | Bean Validationの制約アノテーションをメソッドアノテーションとして指定する。 返り値のJavaBeanがNull値にならないことを示しており、 返り値としてNull値が返却された場合は例外が発生する。 | 
| (4) | 
 
 | 
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の制約アノテーションを有効にする場合は、
 
 | 
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) | 
 メソッドの引数として、 | 
| (2) | メソッドの引数で受け取った ConstraintViolationExceptionが保持している違反内容(ConstraintViolationのSet)をログに出力する。 | 
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が用意する入力チェックルール¶
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インタフェースの実装クラスにも適用可能) | 要素の長さ(要素のサイズ)が minとmaxの間のサイズか検証する。minとmaxは省略可能であるが、デフォルトは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インタフェースの実装クラスに適用可能 | URLとして妥当であること検証する。 java.net.URLのコンストラクタを使用して文字列検証を行っており、
URLとして妥当とされるプロトコルはJVMがサポートするプロトコル(http,https,file,jarなど)に依存する。 | @URL
private String url;
 | 
| @NotBlank | 任意の CharSequenceインタフェースの実装クラスに適用可能 | null、空文字("")、空白のみでないことを検証する。 | @NotBlank
private String userId;
 | 
| @NotEmpty | Collection、Map、Array、任意のCharSequenceインタフェースの実装クラスに適用可能 | null、または空でないことを検証する。@NotNull+@Min(1)の組み合わせでチェックする場合は、@NotEmptyを使用すること。 | @NotEmpty
private String password;
 | 
Tip
@URLにて、JVMがサポートしていないプロトコルについても妥当として検証したい場合、Hibernateから提供されているorg.hibernate.validator.constraintvalidators.RegexpURLValidatorを使用する。
当該クラスは@URLアノテーションに対応するValidatorクラスで、URL形式であるかを正規表現で検証しており、JVMがサポートしていないプロトコルについても妥当として検証可能である。
- アプリケーション全体の@URLのチェックルールを変更してもよい場合には、JavaDocに記載されているように、 XMLにてValidatorクラスをRegexpURLValidatorに変更する。
- 一部の項目だけに正規表現による検証を適用し、@URLはデフォルトのルールを使用したい場合には、新規アノテーション、およびRegexpURLValidatorと同様の検証を行うjavax.validation.ConstraintValidator実装クラスを作成し、 必要な項目に作成したアノテーションによる検証を適用する。
など、用途に応じた適用を行えばよい。
XMLによるチェックルール変更の詳細についてはHibernateのリファレンスを、 新規アノテーションの作成方法については、新規ルールを実装したBean Validationアノテーションの作成をそれぞれ参照されたい。
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 | CharacterCharSequenceの実装クラス( 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の値を指定する。指定可能な値は以下の通り。
 boolean requireBoth-left属性とright属性で指定したフィールドの両方が入力されている(nullでない)必要があるかどうかを指定する。
 org.terasoluna.gfw.common.validator.constraints.Compare.Node node- エラーメッセージを出力するパスを示す列挙型Nodeの値を指定する。指定可能な値は以下の通り。
 | メールアドレスと確認用に入力したメールアドレスが一致することをチェックし、フォーム全体のエラーメッセージとして表示する場合、以下のように実装する。 @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をバインドするを参照されたい。
期間の開始日が終了日以前であることのチェックを例に、想定されるチェック要件と設定の例を以下に示す。
チェック要件 設定例 fromとtoがともに必須で、fromとtoの比較チェックを行う。
fromとtoに@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; }fromとtoがともに必須ではなく、fromとtoが両方入力された時だけ比較チェックする。どちらか一方だけが入力された場合は比較チェックを行わない。
@NotNullは付与せず、requireBoth属性はデフォルト値(false)を使用する。@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL) public class Period { LocalDate from; LocalDate to; }fromとtoがともに必須ではないが、fromかtoのどちら一方でも入力した場合は、必ず両方入力して比較チェックを行う。
@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>
Note
上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。
次に、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で定義されているが、このフィールドに対して整数に変換できない値を入力すると、以下のようなエラーメッセージが表示される。
例外の原因がそのまま表示されてしまい、エラーメッセージとしては不適切である。
型がミスマッチの場合のエラーメッセージは、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
エラーメッセージは、次のように変更される。
{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>
項番 説明 LocalValidatorFactoryBeanをBean定義する。MessageSourceの定義。ここではResourceBundleMessageSourceを使用する。ApplicationContextに読み込ませるリソースバンドルを指定する。<mvc:annotation-driven>要素のvalidator属性に、(1)で定義したBeanを指定する。(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上で別言語(RubyやPythonなど)を使用することができる。
これらの機能を利用して別言語のコードを実行する場合、コードの書き方によっては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に指定された正規表現がそのままエラーメッセージとして出力され、
以下の点でメッセージとしては不適切である。
- エラーの意味が不明確となり、ユーザに優しくない
- 脆弱性への対策のためのロジックが利用者に露呈してしまう
エラーの意味を明確にし、かつ、ロジックを隠蔽するために、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, =, _



















