Caution
本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。
ユーザーが入力した値が不正かどうかを検証することは必須である。 入力値の検証は大きく分けて、
がある。
1.の例としては必須チェックや、桁数チェックがあり、2.の例としては 登録済みのEMailかどうかのチェックや、注文数が在庫数以内であるかどうかのチェックが挙げられる。
本節では、基本的には前者のことを説明し、このチェックのことを「入力チェック」を呼ぶ。 後者のチェックは「業務ロジックチェック」と呼ぶ。業務ロジックチェックについては ドメイン層の実装を参照されたい。
本ガイドラインでは、基本的に入力チェックをアプリケーション層で行い、 業務ロジックチェックは、ドメイン層で行うことをポリシーとする。
Webアプリケーションの入力チェックには、サーバサイドで行うチェックと、クライアントサイド(JavaScript)で行うチェックがある。 サーバーサイドのチェックは必須であるが、クライアントサイドでも同じチェックを実施すると、 サーバー通信なしでチェック結果が分かるため、ユーザビリティが向上する。
Warning
JavaScriptによるクライアントサイドの処理は、改ざん可能であるため、サーバーサイドのチェックは、必ず行うこと。 クライアントサイドのみでチェックを行い、サーバーサイドでチェックを省略した場合は、システムが危険な状態に晒されていることになる。
Todo
クライアントサイドの入力チェックについては今後追記する。初版では、サーバーサイドの入力チェックのみ言及する。
入力チェックは、単項目チェック、相関項目チェックに分類される。
種類 | 説明 | 例 | 実現方法 |
---|---|---|---|
単項目チェック | 単一のフィールドで完結するチェック
|
入力必須チェック
桁チェック
型チェック
|
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インタフェースを利用する。
単項目チェックを実装するには、
が必要である。
Note
spring-mvc.xmlに<mvc:annotation-driven>の設定が行われていれば、Bean Validationは有効になる。
「新規ユーザー登録」処理を例に用いて、実装方法を説明する。ここでは「新規ユーザー登録」のフォームに、以下のチェックルールを設ける。
フィールド名 | 型 | ルール |
---|---|---|
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の場合は正常な値とみなす。ただし、 以下のアノテーションを除く。
上記の例では、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>
ネストした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件追加する。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
バリデーショングループを作成し、一つのフィールドに対して、グループごとに入力チェックルールを指定することができる。
前述の「新規ユーザー登録」の例で、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は使わない方がよい。
複数フィールドにまたがる相関項目チェックには、 Spring Validator(org.springframework.validation.Validatorインタフェースを実装したValidator)、 または、Bean Validationを用いる。
それぞれ説明するが、先にそれぞれの特徴と推奨する使い分けを述べる。
方式 | 特徴 | 用途 |
---|---|---|
Spring Validator
|
特定のクラスに対する入力チェックの作成が容易である。
Controllerでの利用が不便。
|
特定のフォームに依存した業務要件固有の入力チェック実装
|
Bean Validation
|
入力チェックの作成はSpring Validatorほど容易でない。
Controllerでの利用が容易。
|
特定のフォームに依存しない、開発プロジェクト共通の入力チェック実装
|
フィールド名 | 型 | ルール | 説明 |
---|---|---|---|
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に定義するメッセージを参照されたい。
|
(6)
|
エラーメッセージをコードで解決できなかった場合に使用する、デフォルトメッセージを設定する。
|
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 }
Bean Validationによって、相関項目チェックの実装するためには、独自バリデーションルールの追加を行う必要がある。
How to extendにて説明する。
入力チェックエラーメッセージを変更する方法を説明する。
Spring MVCによるBean Validationのエラーメッセージは、以下の順で解決される。
- message属性に指定されたメッセージが、”{メッセージキー}”形式でない場合、そのテキストをエラーメッセージとして使用する。
- message属性に指定されたメッセージが、”{メッセージキー}”形式の場合、クラスパス直下のValidationMessages.propertiesから、メッセージキーに対応するメッセージを探す。
- メッセージキーに対応するメッセージが定義されている場合は、そのメッセージを使用する
- メッセージキーに対応するメッセージが定義されていない場合は、”{メッセージキー}”をそのままエラーメッセージとして使用する
基本的にエラーメッセージは、propertiesファイルに定義することを推奨する。
定義する箇所は、以下の2パターン存在する。
以下の説明では、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が用意するデフォルトメッセージが使用される。
クラスパス直下(通常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)に定義する必要がある。
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のメッセージキーの形式は、これ以外にも用意されているが、 デフォルトメッセージを一部上書きする目的で使用するのであれば、基本的に、アノテーション名.フォーム属性名.プロパティ名形式でよい。
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();
}
}
システム共通で、
複数のルールを組み合わせて一つのルールを作成することができる。 独自アノテーションを作成すると、正規表現パターンや、最大値・最小値などの値を共通化できるだけでなく、エラーメッセージも共通化できるというメリットがある。 これにより、再利用性や保守性が高まる。複数のルールの組み合わせではなくても、一つのルールの属性を特定するだけでも効果的である。
以下に、実装例を示す。
半角英数字の文字種に限定する@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のドキュメントを参照されたい。
javax.validation.ConstraintValidatorインタフェースを実装し、そのValidatorを使用するアノテーションを作成することで、任意のルールを作成することができる。
用途としては、以下の3通りが挙げられる。
@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を用意している。
以下では、「あるフィールドとその確認用フィールドの内容が一致すること」というルールを実現する例を挙げる。
ここでは、確認用フィールドの先頭に、「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で出力するメッセージを定義する。
本例では、ConstraintValidatorContext.getDefaultConstraintMessageTemplateメソッドで、デフォルトのメッセージ定義を引き継いで設定する。
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";
}
}
一方で、「入力されたユーザー名が既に登録済みかどうか」など、対象の入力フィールドに対する業務ロジックエラーメッセージを、フィールドの横に表示したい場合もある。 このような場合は、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に処理を委譲すること。
|
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;
|
Hibernate Validatorの代表的なアノテーションを、以下に示す。
詳細は、Hibernate Validator仕様を参照されたい。
アノテーション(org.hibernate.validator.constraints.*) | 対象の型 | 用途 | 使用例 |
---|---|---|---|
@CreditCardNumber | 任意のCharSequence継承クラスに適用可能 | Luhnアルゴリズムでクレジットカード番号が妥当かどうかを検証する。使用可能な番号かどうかをチェックするわけではない。 | @CreditCardNumber
private String cardNumber;
|
任意の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;
|
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
フォームオブジェクトの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
エラーメッセージは、次のように変更される。
Tip
メッセージキーのルールの詳細は、Javadocを参照されたい。
これまで説明してきたように、Spring MVCでは文字列の入力フィールドに未入力の状態でフォームを送信した場合、 デフォルトでは、フォームオブジェクトにnullではなく、空文字がバインドされる。
この場合、「未入力は許容するが、入力された場合は6文字以上であること」という要件を、既存のアノテーションで満たすことができない。
@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 ...
}