5.5. 入力チェック¶
Caution
本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。
5.5.1. Overview¶
ユーザーが入力した値が不正かどうかを検証することは必須である。 入力値の検証は大きく分けて、
- 長さや形式など、文脈によらず入力値だけを見て、それが妥当かどうかを判定できる検証
- システムの状態によって入力値が妥当かどうかが変わる検証
がある。
1.の例としては必須チェックや、桁数チェックがあり、2.の例としては 登録済みのEMailかどうかのチェックや、注文数が在庫数以内であるかどうかのチェックが挙げられる。
本節では、基本的には前者のことを説明し、このチェックのことを「入力チェック」を呼ぶ。 後者のチェックは「業務ロジックチェック」と呼ぶ。業務ロジックチェックについては ドメイン層の実装を参照されたい。
本ガイドラインでは、基本的に入力チェックをアプリケーション層で行い、 業務ロジックチェックは、ドメイン層で行うことをポリシーとする。
Webアプリケーションの入力チェックには、サーバサイドで行うチェックと、クライアントサイド(JavaScript)で行うチェックがある。 サーバーサイドのチェックは必須であるが、クライアントサイドでも同じチェックを実施すると、 サーバー通信なしでチェック結果が分かるため、ユーザビリティが向上する。
Warning
JavaScriptによるクライアントサイドの処理は、改ざん可能であるため、サーバーサイドのチェックは、必ず行うこと。 クライアントサイドのみでチェックを行い、サーバーサイドでチェックを省略した場合は、システムが危険な状態に晒されていることになる。
Todo
クライアントサイドの入力チェックについては今後追記する。初版では、サーバーサイドの入力チェックのみ言及する。
5.5.1.1. 入力チェックの分類¶
入力チェックは、単項目チェック、相関項目チェックに分類される。
種類 | 説明 | 例 | 実現方法 |
---|---|---|---|
単項目チェック | 単一のフィールドで完結するチェック
|
入力必須チェック
桁チェック
型チェック
|
Bean Validation (実装ライブラリとしてHibernate Validatorを使用)
|
相関項目チェック | 複数のフィールドを比較するチェック
|
パスワードと確認用パスワードの一致チェック
|
org.springframework.validation.Validatorインタフェースを実装したValidationクラス
または Bean Validation
|
Spring は、Java標準であるJSR-303のBean Validationをサポートしている。
単項目チェックには、このBean Validationを利用する。
相関項目チェックの場合は、Bean ValidationまたはSpringが提供しているorg.springframework.validation.Validator
インタフェースを利用する。
5.5.2. How to use¶
5.5.2.1. 単項目チェック¶
単項目チェックを実装するには、
- フォームクラスのフィールドに、Bean Validation用のアノテーションを付与する
- Controllerに、検証するための
@Validated
アノテーションを付与する - JSPに、検証エラーメッセージを表示するためのタグを追加する
が必要である。
Note
spring-mvc.xmlに<mvc:annotation-driven>
の設定が行われていれば、Bean Validationは有効になる。
5.5.2.1.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が用意しているアノテーションについては、JSR-303のチェックルール、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が空文字であることに対するエラーメッセージと、Agが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
エラーメッセージの出力順序はデフォルトでは順不同である。 @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>
5.5.2.1.2. ネストした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件追加する。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
5.5.2.1.3. バリデーションのグループ化¶
バリデーショングループを作成し、一つのフィールドに対して、グループごとに入力チェックルールを指定することができる。
前述の「新規ユーザー登録」の例で、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) { Class<?> validationGroup = Default.class; // logic to determine validation group // if (xxx) { // validationGroup = Xxx.class; // } smartValidator.validate(form, result, validationGroup); // (3) if (result.hasErrors()) { return "user/createForm"; } return "user/createConfirm"; } }
項番 説明 (1)SmartValidator
をインジェクションする。SmartValidator
は<mvc:annotation-driven>
の設定が行われていれば使用できるため、別途Bean定義不要である。 (2)@Validated
アノテーションは使わない。 (3)SmartValidator
のvalidate
メソッドを使用して、グループを指定したバリデーションを実行する。グループの指定は可変長引数になっており、複数指定できる。
基本的には、Controllerにロジックを書くべきではないため、@RequestMapping
の属性でルールを切り替えられるのであれば、SmartValidator
は使わない方がよい。
5.5.2.2. 相関項目チェック¶
複数フィールドにまたがる相関項目チェックには、
Spring Validator(org.springframework.validation.Validator
インタフェースを実装したValidator
)、
または、Bean Validationを用いる。
それぞれ説明するが、先にそれぞれの特徴と推奨する使い分けを述べる。
方式 | 特徴 | 用途 |
---|---|---|
Spring Validator
|
特定のクラスに対する入力チェックの作成が容易である。
Controllerでの利用が不便。
|
特定のフォームに依存した業務要件固有の入力チェック実装
|
Bean Validation
|
入力チェックの作成はSpring Validatorほど容易でない。
Controllerでの利用が容易。
|
特定のフォームに依存しない、開発プロジェクト共通の入力チェック実装
|
5.5.2.2.1. Spring Validatorによる相関項目チェック実装¶
フィールド名 | 型 | ルール | 説明 |
---|---|---|---|
password
|
java.lang.String |
入力必須
8文字以上
confirmPasswordと同じ値であること
|
パスワード
|
confirmPassword
|
java.lang.String |
特になし
|
確認用パスワード
|
「confirmPasswordと同じ値であること」というルールはpassword
フィールドとpasswordConfirm
フィールドの両方の情報が必要であるため、相関項目チェックルールである。
フォームクラス
相関項目チェックルール以外は、これまで通りBean Validationのアノテーションで実装する。
package com.example.sample.app.validation; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class PasswordResetForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull @Size(min = 8) private String password; private String confirmPassword; // omitted setter/getter }
Note
パスワードは、通常ハッシュ化してデータベースに保存するため、最大値のチェックは行わなくても良い。
Validatorクラス
org.springframework.validation.Validator
インタフェースを実装して、相関項目チェックルールを実現する。package com.example.sample.app.validation; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @Component // (1) public class PasswordEqualsValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return PasswordResetForm.class.isAssignableFrom(clazz); // (2) } @Override public void validate(Object target, Errors errors) { PasswordResetForm form = (PasswordResetForm) target; String password = form.getPassword(); String confirmPassword = form.getConfirmPassword(); if (password == null || confirmPassword == null) { // (3) // must be checked by @NotNull return; } 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)Bean Validationのルールと同様に、フィールドがnull
かどうかのチェックは、@NotNull
アノテーションに任せ、このValidatorでは、フィールドがnull
の場合は正常値とみなす。(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
一つの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 }
5.5.2.2.2. Bean Validationによる相関項目チェック実装¶
Bean Validationによって、相関項目チェックの実装するためには、独自バリデーションルールの追加を行う必要がある。
How to extendにて説明する。
5.5.2.3. エラーメッセージの定義¶
入力チェックエラーメッセージを変更する方法を説明する。
Spring MVCによるBean Validationのエラーメッセージは、以下の順で解決される。
org.springframework.context.MessageSource
に定義されているメッセージの中に、ルールに合致するものがあればそれをエラーメッセージとして使用する (Springのルール)- 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>
本ガイドラインでは、以下のように定義を分けることを推奨する。
プロパティファイル名 | 定義する内容 |
---|---|
ValidationMessages.properties
|
システムで定めたBean Validationのデフォルトエラーメッセージ
|
application-messages.properties
|
個別で上書きしたいBean Validationのエラーメッセージ
Spring Validatorで実装した入力チェックのエラーメッセージ
|
ValidationMessages.propertiesを用意しない場合は、Hibernate Validatorが用意するデフォルトメッセージが使用される。
5.5.2.3.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}. # (2) 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)アノテーションに指定した属性値は、{属性名}
で埋め込むことができる。(2)不正となった入力値は、{value}
で埋め込むことができる。
この設定を加えた状態で、すべての入力フィールドを未入力のままフォームを送信すると、以下のように変更したエラーメッセージが、表示される。
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
)に定義する必要がある。
5.5.2.3.2. application-messages.propertiesに定義するメッセージ¶
ValidationMessages.propertiesでシステムで利用するデフォルトのメッセージを定義したが、 画面によっては、デフォルトメッセージから変更したい場合が出てくる。
その場合、application-messages.propertiesに、以下の形式でメッセージを定義する。
アノテーション名.フォーム属性名.プロパティ名=対象のメッセージ
ValidationMessages.propertiesに定義するメッセージの設定がある前提で、以下の設定でage
フィールドのメッセージを上書きする。
application-messages.properties
# override messages NotNull.userForm.age="{0}" is compulsory. Max.userForm.age="{0}" must be less than or equal to {1}. Max.userForm.age="{0}" must be less than or equal to {1}. NotNull.userForm.email="{0}" is compulsory. Size.userForm.age=The size of "{0}" must be between {2} and {1}. # filed names name=Name email=Email age=Age
アノテーションの属性値は、{1}
以降に埋め込まれる。
エラーメッセージは以下のように変更される。
Note
application-messages.propertiesのメッセージキーの形式は、これ以外にも用意されているが、
デフォルトメッセージを一部上書きする目的で使用するのであれば、基本的に、アノテーション名.フォーム属性名.プロパティ名
形式でよい。
5.5.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();
}
}
5.5.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)エラーメッセージのデフォルト値を定義する。正の数に限定する
@NotNegaitive
アノテーションの実装例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 NotNegaitive { String message() default "{com.example.common.validation.NotNegaitive.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented public @interface List { NotNegaitive[] 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のドキュメントを参照されたい。
5.5.3.2. 新規ルールを実装したBean Validationアノテーションの作成¶
javax.validation.ConstraintValidator
インタフェースを実装し、そのValidatorを使用するアノテーションを作成することで、任意のルールを作成することができる。
用途としては、以下の3通りが挙げられる。
- 既存のルールの組み合わせでは表現できないルール
- 相関項目チェックルール
- 業務ロジックチェック
5.5.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を用意している。
5.5.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) .addNode(field).addConstraintViolation(); // (4) return false; } } }
項番 説明 (1)JavaBeanのプロパティにアクセスする際に便利なorg.springframework.beans.BeanWrapper
を使用する。(2)BeanWrapper
経由で、フォームオブジェクトからプロパティ値を取得する。(3)デフォルトのConstraintViolation
オブジェクトの生成を無効にする。(4)独自ConstraintViolation
オブジェクトを生成する。ConstraintValidatorContext.buildConstraintViolationWithTemplate
で出力するメッセージを定義する。ConstraintViolationBuilder.addNode
でエラーメッセージを出力したいフィールド名を指定する。詳細は、以下のJavaDocを参照されたい。
この@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"; } }
5.5.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に処理を委譲すること。
5.5.4. Appendix¶
5.5.4.1. Hibernate Validatorが用意する入力チェックルール¶
5.5.4.1.1. JSR-303のチェックルール¶
JSR-303アノテーションを、以下に示す。
詳細は、JSR-303 Specificationの6章を参照されたい。
アノテーション(javax.validation.*) | 対象の型 | 用途 | 使用例 |
---|---|---|---|
@NotNull |
任意 | 対象のフィールドが、nullでないことを検証する。 | @NotNull
private String id;
|
@Null |
任意 | 対象のフィールドが、nullであることを検証する。
(例:グループ検証での使用)
|
@Nulll(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型の値が最小値以上であるかどうかを検証する。 | @DecimalMax参照 |
@DecimalMax |
BigDecimal, BigInteger, String, byte, short, int, longおよびラッパー (Hibernate Validator実装では任意のNumber,CharSequence継承クラスにも適用可能) | Decimal型の値が、最大値以下であるかどうかを検証する。 | @DecimalMin(“0.0”)
@DecimalMax(“99999.99”)
private BigDecimal price;
|
@Size |
String(length), Collection(size), Map(size), Array(length) (Hibernate Validator実装では、任意のCharSequence継承クラスにも適用可能) | lengthが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, Calender (Hibernate Validator実装ではJoda-Timeのクラスにも適用可能) | 未来日付であるか検証する。 | @Future
private Date eventDate;
|
@Past |
Date, Calender (Hibernate Validator実装ではJoda-Timeのクラスにも適用可能) | 過去日付であるか検証する。 | @Past
private Date eventDate;
|
@Valid |
任意の非プリミティブ型 | 関連付けられているオブジェクトについて、再帰的に検証を行う。 | @Valid
private List<Employer> employers;
@Valid
private Dept dept;
|
5.5.4.1.2. Hibernate Validatorのチェックルール¶
Hibernate Validatorの代表的なアノテーションを、以下に示す。
詳細は、Hibernate Validator仕様を参照されたい。
アノテーション(org.hibernate.validator.constraints.*) | 対象の型 | 用途 | 使用例 |
---|---|---|---|
@CreditCardNumber |
任意のCharSequence継承クラスに適用可能 | Luhnアルゴリズムでクレジットカード番号が妥当かどうかを検証する。使用可能な番号かどうかをチェックするわけではない。 | @CreditCardNumber
private String cardNumber;
|
@Email |
任意のCharSequence継承クラスに適用可能 | RFC2822に準拠したEmailアドレスかどうか検証する。 | @Email
private String email;
|
@URL |
任意のCharSequence継承クラスに適用可能 | RFC2396に準拠しているかどうか検証する。 | @URL
private String url;
|
@NotBlank |
任意のCharSequence継承クラスに適用可能 | Null、空文字(“”)、空白のみでないことを検証する。 | @NotBlank
private String userId;
|
@NotEmpty |
Collection、Map、arrays、任意のCharSequence継承クラスに適用可能 | Null、または空でないことを検証する。
@NotNull + @Min(1)の組み合わせでチェックする場合は、@NotEmptyを使用すること。
|
@NotEmpty
private String password;
|
5.5.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 or equal to {value}
javax.validation.constraints.DecimalMin.message = must be greater than 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.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.NotBlank.message = may not be empty
org.hibernate.validator.constraints.NotEmpty.message = may not be empty
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.TituloEleitor.message = invalid Brazilian Voter ID card number
5.5.4.3. 型のミスマッチ¶
フォームオブジェクトの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
メッセージキーのルールの詳細は、Javadocを参照されたい。
5.5.4.4. 文字列フィールドが未入力の場合にnullをバインドする¶
これまで説明してきたように、Spring MVCでは文字列の入力フィールドに未入力の状態でフォームを送信した場合、
デフォルトでは、フォームオブジェクトにnull
ではなく、空文字がバインドされる。
この場合、「未入力は許容するが、入力された場合は6文字以上であること」という要件を、既存のアノテーションで満たすことができない。
null
をフォームオブジェクトにバインドするには、org.springframework.beans.propertyeditors.StringTrimmerEditor
を使用すればよい。@Controller
@RequestMapping("xxx")
public class XxxController {
@InitBinder
public void initBinder(WebDataBinder binder) {
// bind empty strings as null
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
// omitted ...
}
この設定により、Controller毎に空文字をnull
とみなすかどうかを設定できる。
プロジェクト全体で空文字をnull
にしたい場合は、プロジェクト共通設定として@ControllerAdviceで設定すればよい。
@ControllerAdvice
public class XxxControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
// bind empty strings as null
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
// omitted ...
}
null
になる。@NotNull
が必要であることに注意しないといけない。