4.2. 入力チェック¶
4.2.1. Overview¶
長さや形式など、文脈によらず入力値だけを見て、それが妥当かどうかを判定できる検証
システムの状態によって入力値が妥当かどうかが変わる検証
がある。
1.の例としては必須チェックや、桁数チェックがあり、2.の例としては登録済みのE-mailかどうかのチェックや、注文数が在庫数以内であるかどうかのチェックが挙げられる。
本節では、基本的には前者のことを説明し、このチェックのことを「入力チェック」を呼ぶ。後者のチェックは「業務ロジックチェック」と呼ぶ。業務ロジックチェックについてはドメイン層の実装を参照されたい。
本ガイドラインでは、基本的に入力チェックをアプリケーション層で行い、業務ロジックチェックは、ドメイン層で行うことをポリシーとする。
Webアプリケーションの入力チェックには、サーバサイドで行うチェックと、クライアントサイド(JavaScript)で行うチェックがある。サーバーサイドのチェックは必須であるが、クライアントサイドでも同じチェックを実施すると、サーバー通信なしでチェック結果が分かるため、ユーザビリティが向上する。
Warning
JavaScriptによるクライアントサイドの処理は改ざん可能であるため、サーバーサイドのチェックは必ず行うこと。
クライアントサイドのみでチェックを行い、サーバーサイドでチェックを省略した場合は、システムが危険な状態に晒されていることになる。
4.2.1.1. 入力チェックの分類¶
入力チェックは、単項目チェック、相関項目チェックに分類される。
種類 |
説明 |
例 |
実現方法 |
|---|---|---|---|
単項目チェック |
単一のフィールドで完結するチェック
|
入力必須チェック
桁チェック
型チェック
|
Bean Validation (実装ライブラリとしてHibernate Validatorを使用)
|
相関項目チェック |
複数のフィールドを比較するチェック
|
パスワードと確認用パスワードの一致チェック
|
org.springframework.validation.Validatorインタフェースを実装したValidationクラス
または Bean Validation
|
org.springframework.validation.Validatorインタフェースを利用する。4.2.2. How to use¶
4.2.2.1. 依存ライブラリの追加¶
Bean Validation 3.0(Hibernate Validator 8.x)以上を使用する場合、Bean ValidationのAPI仕様クラス(jakarta.validationパッケージのクラス)が格納されているjarファイルとHibernate Validatorのjarファイルに加えて、
Expression Language 5.0以上のAPI仕様クラス (
jakarta.elパッケージのクラス)Expression Language 5.0以上のリファレンス実装クラス
が格納されているライブラリが必要となる。
アプリケーションサーバにデプロイして動かす場合は、これらのライブラリはアプリケーションサーバから提供されているため、依存ライブラリの追加は不要である。ただし、スタンドアロン環境(JUnitなど)で動かす場合は、これらのライブラリを依存ライブラリとして追加する必要がある。
スタンドアロン環境でBean Validation 3.0以上を動かす際に必要となるライブラリの追加例を以下に示す。
<!-- (1) -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<scope>test</scope> <!-- (2) -->
</dependency>
項番 |
説明 |
|---|---|
(1)
|
スタンドアロン環境で動かすプロジェクトの
pom.xmlファイルに、Expression Language用のクラスが格納されているライブラリを追加する。上記例では、組込み用のApache Tomcat向けに提供されているライブラリを指定している。
tomcat-embed-elのjarファイルには、Expression LanguageのAPI仕様クラスとリファレンス実装クラスの両方が格納されている。 |
(2)
|
JUnitを実行するために依存ライブラリが必要になる場合は、スコープは
testが適切である。 |
Note
上記設定例は、依存ライブラリのバージョンを親プロジェクトであるterasoluna-gfw-parentで管理する前提であるため、pom.xmlでのバージョンの指定は不要である。
4.2.2.2. 単項目チェック¶
単項目チェックを実装するには、
フォームクラスのフィールドに、Bean Validation用のアノテーションを付与する
Controllerに、検証するための
@Validatedアノテーションを付与するJSPまたはThymeleafのテンプレートHTMLに、検証エラーメッセージを表示するためのタグを追加する
が必要である。
Note
spring-mvc.xmlに<mvc:annotation-driven>の設定が行われていれば、Bean Validationは有効になる。
4.2.2.2.1. 入力必須チェックについて¶
@NotNullを付け、値がnullではないことを確認することでチェックを行う。しかし、文字列の入力フィールドに未入力の状態でフォームを送信した場合、Spring MVCのデフォルト挙動ではフォームオブジェクトにnullではなく、空文字がバインドされる。そのため、文字列の入力フィールドに対して入力必須チェックを行う場合は、@NotNullではなく、@NotEmptyを使用してnullおよび空文字を許可しないことを推奨する。nullにバインドする方法に関しては、文字列フィールドが未入力の場合にnullをバインドするを参照されたい。文字列のフィールドに対して入力必須チェックだけではなく、@Size(min = 1, max = 20)のように最小の文字数をチェックする場合は@NotNullを使用した場合と@NotEmptyを使用した場合では挙動が異なる点に注意されたい。
@NotNullを使用した場合は、未入力の場合@Sizeのエラーメッセージのみが表示される。@NotEmptyを使用した場合は、未入力の場合@NotEmptyのエラーメッセージと@Sizeのエラーメッセージが表示される。
@Sizeのminを1以上に設定する場合は空文字を許容しないため、@NotEmptyによるチェックは冗長となる。そのため、本ガイドラインで文字列フィールドに対して入力必須チェックと文字列長の最小値チェックを行う場合は、@NotNullを使用する。
以上の説明より、本ガイドラインでは次の表の内容に従い入力必須チェックを行う。
対象フィールド |
チェック内容 |
使用するアノテーション |
説明 |
|---|---|---|---|
文字列以外
|
入力必須チェック
|
@NotNull |
nullを許可しない |
文字列
|
入力必須チェック
|
@NotEmpty |
nullと空文字を許可しない |
入力必須チェック + 文字列長チェック
|
@NotNull+ @Size(min = 1, max = 20) |
未入力の場合は
@Sizeのエラーメッセージのみが表示される。 |
4.2.2.2.2. 基本的な単項目チェック¶
「新規ユーザー登録」処理を例に用いて、実装方法を説明する。ここでは「新規ユーザー登録」のフォームに、以下のチェックルールを設ける。
フィールド名 |
型 |
ルール |
|---|---|---|
name
|
java.lang.String |
入力必須
1文字以上
20文字以下
|
email
|
java.lang.String |
入力必須
1文字以上
50文字以下
E-mail形式
|
age
|
java.lang.Integer |
入力必須
1以上
200以下
|
フォームクラス
フォームクラスの各フィールドに、Bean Validationのアノテーションを付ける。使用するアノテーションの詳細はBean Validationのチェックルールを参照されたい。
package com.example.sample.app.validation; import java.io.Serializable; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class UserForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull // (1) @Size(min = 1, max = 20) // (2) private String name; @NotNull @Size(min = 1, max = 50) @Email // (3) private String email; @NotNull // (4) @Min(0) // (5) @Max(200) // (6) private Integer age; // omitted setter/getter }
項番
説明
(1)対象のフィールドがnullでないことを示すjakarta.validation.constraints.NotNullを付ける。Spring MVCでは、文字列の入力フィールドに未入力の状態でフォームを送信した場合、デフォルトではフォームオブジェクトにnullではなく、空文字がバインドされる。この@NotNullは、そもそもリクエストパラメータとしてnameが存在することをチェックする。(2)対象のフィールドの文字列長(またはコレクションのサイズ)が指定したサイズの範囲内にあることを示すjakarta.validation.constraints.Sizeを付ける。上記の通り、Spring MVCではデフォルトで、未入力の文字列フィールドには、空文字がバインドされるため、1文字以上というルールが入力必須を表す。(3)対象のフィールドがE-mail形式であることを示すjakarta.validation.constraints.Emailを付ける。E-mail形式の要件が@Emailのチェックと合致しない場合は、jakarta.validation.constraints.Patternを用いて、正規表現を指定する必要がある。@Emailについては、Bean Validationのチェックルールを参照されたい。(4)数値の入力フィールドに未入力の状態でフォームを送信した場合、フォームオブジェクトにnullがバインドされるため、@NotNullがageの入力必須条件を表す。(5)対象のフィールドが指定した数値の以上であることを示すjakarta.validation.constraints.Minを付ける。(6)対象のフィールドが指定した数値の以下であることを示すjakarta.validation.constraints.Maxを付ける。Tip
Bean Validation標準のアノテーション、Hibernate Validationが用意しているアノテーションについては、Bean Validationのチェックルール、Hibernate Validatorのチェックルールを参照されたい。
Controllerクラス
入力チェック対象のフォームクラスに、
@Validatedを付ける。package com.example.sample.app.validation; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("user") public class UserController { @ModelAttribute public UserForm setupForm() { return new UserForm(); } @GetMapping(value = "create", params = "form") public String createForm() { return "user/createForm"; // (1) } @PostMapping(value = "create", params = "confirm") public String createConfirm(@Validated /* (2) */ UserForm form, BindingResult /* (3) */ result) { if (result.hasErrors()) { // (4) return "user/createForm"; } return "user/createConfirm"; } @PostMapping(value = "create") public String create(@Validated UserForm form, BindingResult result) { // (5) if (result.hasErrors()) { return "user/createForm"; } // omitted business logic return "redirect:/user/create?complete"; } @GetMapping(value = "create", params = "complete") public String createComplete() { return "user/createComplete"; } }
項番
説明
(1)「新規ユーザー登録」フォーム画面を表示する。(2)フォームにつけたアノテーションで入力チェックをするために、フォームの引数にorg.springframework.validation.annotation.Validatedを付ける。(3)(2)のチェック結果を格納するorg.springframework.validation.BindingResultを、引数に加える。このBindingResultは、フォームの直後に記述する必要がある。直後に指定されていない場合は、検証後に結果をバインドできず、org.springframework.validation.BindExceptionがスローされる。(4)(2)のチェック結果は、BindingResult.hasErrors()メソッドで判定できる。hasErrors()の結果がtrueの場合は、入力値に問題があるため、フォーム表示画面に戻す。(5)入力内容確認画面から新規作成処理にリクエストを送る際にも、入力チェックを必ず再実行すること。途中でデータを改ざんすることは可能であるため、必ず業務処理の直前で入力チェックは必要である。@Validatedは、Bean Validation標準ではなくSpringの独自アノテーションである。Bean Validation標準の
jakarta.validation.Validアノテーションも使用できるが、@Validatedは@Validに比べてバリデーションのグループを指定できる点で優れているため、本ガイドラインではControllerの引数には@Validatedを使用することを推奨する。
View
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属性に、対象のフィールド名を指定する。この例では、フィールド毎に入力フィールドの横にエラーメッセージを表示する。
HTML
th:errors属性で、入力エラーがある場合にエラーメッセージを表示できる。<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <!--/* WEB-INF/views/user/createForm.html */--> <body> <form th:object="${userForm}" method="post" th:action="@{/user/create}"> <label for="name">Name:</label> <input type="text" th:field="*{name}"> <span id="name-errors" th:errors="*{name}"></span><!--/* (1) */--> <br> <label for="email">Email:</label> <input type="text" th:field="*{email}"> <span id="email-errors" th:errors="*{email}"></span> <br> <label for="age">Age:</label> <input type="text" th:field="*{age}"> <span id="age-errors" th:errors="*{age}"></span> <br> <button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button> </form> </body> </html>
項番
説明
(1)<span>タグのth:errors属性に、対象のフィールド名を指定する。この例では、フィールド毎に入力フィールドの横にエラーメッセージを表示する。
フォームは、以下のように表示される。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
NameとEmailが空文字であることに対するエラーメッセージと、Ageがnullであることに対するエラーメッセージが表示されている。
Note
Bean Validationでは、通常、入力値がnullの場合は正常な値とみなす。ただし、以下のアノテーションを除く。
jakarta.validation.constraints.NotNulljakarta.validation.constraints.NotEmptyjakarta.validation.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属性で指定する。 |
<form th:object="${userForm}" method="post"
class="form-horizontal" th:action="@{/user/create}">
<label for="name" name="name" th:errorclass="error-label">Name:</label><!--/* (1) */-->
<input type="text" th:field="*{name}" th:errorclass="error-input"><!--/* (2) */-->
<span id="name-errors" th:errors="*{name}" class="error-messages"></span><!--/* (3) */-->
<br>
<label for="email" name="email" th:errorclass="error-label">Email:</label>
<input type="text" th:field="*{email}" th:errorclass="error-input">
<span id="email-errors" th:errors="*{email}" class="error-messages"></span>
<br>
<label for="age" name="age" th:errorclass="error-label">Age:</label>
<input type="text" th:field="*{age}" th:errorclass="error-input">
<span id="age-errors" th:errors="*{age}" class="error-messages"></span>
<br>
<button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button>
</form>
項番 |
説明 |
|---|---|
(1)
|
エラー時に
<label>タグへ加えるクラス名を、th:errorclass属性で指定する。また、
th:field属性が指定されていないタグへth:errorclass属性を指定する場合は、対象のフィールド名をname属性で指定する。 |
(2)
|
エラー時に
<input>タグへ加えるクラス名を、th:errorclass属性で指定する。 |
(3)
|
エラーメッセージに加えるクラス名を、
class属性で指定する。 |
Note
エラー時にスタイルを変更する方法について
実装例のように、th:errorclass属性を使用することで、入力チェックエラーがある要素のスタイルを変更することができる。
しかし、th:errorclass属性を使用できるのは、同じタグに付与されたth:field属性またはname属性により、入力チェックエラーとなったフィールド名(フォームオブジェクトのプロパティ名)が特定できる場合のみとなる。
入力項目以外のスタイルを変更したい場合は、#fields.hasErrors('fieldName')を使用してフィールドに入力チェックエラーが存在するかを判定することでスタイルを変更することができる。
例えば、#fields.hasErrors('fieldName')を使用して上記実装例の(1)と同じ仕様を実現する場合には、以下のような構文となる。
th:classappend="${#fields.hasErrors('name')} ? 'error-label'"
Note
class属性が指定されているタグにth:errorclass属性をあわせて指定した場合、エラー時には、class属性で指定した値にth:errorclass属性で指定した値が追加される。
このJSPまたはHTMLに対して、例えば以下の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属性に指定する。 |
<form th:object="${userForm}" method="post" th:action="@{/user/create}">
<div id="userForm-errors" th:errors="*{*}" class="error-message-list"></div><!--/* (1) */-->
<label for="name" name="name" th:errorclass="error-label">Name:</label>
<input type="text" th:field="*{name}" th:errorclass="error-input">
<br>
<label for="email" name="email" th:errorclass="error-label">Email:</label>
<input type="text" th:field="*{email}" th:errorclass="error-input">
<br>
<label for="age" name="age" th:errorclass="error-label">Age:</label>
<input type="text" th:field="*{age}" th:errorclass="error-input">
<br>
<button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button>
</form>
項番 |
説明 |
|---|---|
(1)
|
<form>タグ内で、メッセージを包含するタグのth:errors属性に*{*}を指定することで、<form>タグのth:object属性に指定したModelに関する全エラーメッセージを出力できる。(なお、
*{*}の部分を*{all}と指定しても同等である。)ここではエラーメッセージ一覧をブロック要素として出力するために、
divを指定している。また、CSSのクラスを
class属性に指定する。 |
Tip
エラーメッセージを一覧で表示する際のHTML構造を独自に定義する方法
th:errors="*{*}"と指定した場合、各エラーは<br>区切りで出力される。
<br>区切りではなく独自のHTML構造で出力したい場合は、#fields.allErrors()メソッドを利用することで対応できる。
以下に、実装例を示す。
<ul th:if="${#fields.hasAnyErrors()}"> <!--/* (1) */--> <li th:each="err : ${#fields.allErrors()}" th:text="${err}"></li> <!--/* (2) */--> </ul>
項番
説明
(1)#fields.hasAnyErrors()メソッドを利用してエラー有無を取得し、th:if属性を用いてエラーがない場合はタグを生成しないようにしている。(なお、#fields.hasAnyErrors()の部分を#fields.hasErrors('*')と指定しても同等である。) (2)#fields.allErrors()メソッドを利用してすべてのエラーを取得し、th:each属性を用いて繰り返し処理を行いliタグを生成している。(なお、#fields.allErrors()の部分を#fields.errors('*')と指定しても同等である。)
例として、以下のCSSクラスを適用した場合の、エラーメッセージ出力例を示す。
.form-horizontal input {
display: block;
float: left;
}
.form-horizontal label {
display: block;
float: left;
text-align: right;
float: left;
}
.form-horizontal br {
clear: left;
}
.error-label {
color: #b94a48;
}
.error-input {
border-color: #b94a48;
margin-left: 5px;
}
.error-message-list {
color: #b94a48;
padding:5px 10px;
background-color: #fde9f3;
border:1px solid #c98186;
border-radius:5px;
margin-bottom: 10px;
}
Note
エラーメッセージを一覧で表示する際の注意点
エラーメッセージの出力順序は順不同であり、標準機能で出力順序を制御することはできない。そのため、出力順序を制御する(一定に保つ)必要がある場合は、エラー情報をソートするなどの拡張実装が必要となる。
「エラーメッセージを一覧で表示する」方式では、
フィールド単位のエラーメッセージ定義
エラーメッセージの出力順序を制御するための拡張実装
が必要となるため、「入力フィールドの横にエラーメッセージを表示する」方式に比べて対応コストが高くなる。 本ガイドラインでは、画面要件による制約がない場合は「入力フィールドの横にエラーメッセージを表示する」方式を推奨する。
なお、エラーメッセージの出力順序を制御するための拡張方法としては、Spring Frameworkから提供されているorg.springframework.validation.beanvalidation.LocalValidatorFactoryBeanの継承クラスを作成し、processConstraintViolationsメソッドをオーバーライドしてエラー情報をソートする方法などが考えられる。
Note
@GroupSequenceアノテーションについて
チェック順番を制御するための仕組みとして@GroupSequenceアノテーションが提供されているが、この仕組みは以下のような動作になるため、エラーメッセージの出力順序を制御するための仕組みではないという点を補足しておく。
エラーが発生した場合に後続のグループのチェックが実行されない。
同一グループ内のチェックで複数のエラー(複数の項目でエラー)が発生するとエラーメッセージの出力順序は順不同になる。
エラーメッセージをまとめて表示する際に、<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>
エラーメッセージをまとめて表示する際に、th:object属性を指定した要素(<form>タグなど)の外に表示したい場合は以下のようにth:errors属性にModelに格納されているフォームオブジェクトの属性名.*で指定する。
<div id="userForm-errors" th:errors="${userForm.*}" class="error-message-list"></div> <hr> <form th:object="${userForm}" method="post" th:action="@{/user/create}"> <label for="name" name="name" th:errorclass="error-label">Name:</label> <input type="text" th:field="*{name}" th:errorclass="error-input"> <br> <label for="email" name="email" th:errorclass="error-label">Email:</label> <input type="text" th:field="*{email}" th:errorclass="error-input"> <br> <label for="age" name="age" th:errorclass="error-label">Age:</label> <input type="text" th:field="*{age}" th:errorclass="error-input"> <br> <button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button> </form>
4.2.2.2.3. 日時フォーマットのチェック¶
@DateTimeFormatアノテーションの使用を推奨する。@DateTimeFormatアノテーションの使用方法については、フィールド単位の日時型変換を参照されたい。@Patternアノテーションを使用することでも日時フォーマットのチェックは可能である。@Patternアノテーションを使用すると、日時フォーマットを正規表現で記述する必要があり、存在しない日時をチェックする場合には、記述が煩雑化する。@Patternアノテーションよりも@DateTimeFormatアノテーションのほうが実装はシンプルになる。@DateTimeFormatアノテーションはSpringが提供する型変換の仕組みのひとつであるので、入力エラーの場合には、Bean Validationのエラーメッセージではなく、型のミスマッチが発生した時にスローされる例外(TypeMismatchException)の例外メッセージがそのまま画面へ表示される。4.2.2.2.4. ネストした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 jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; public class OrderForm implements Serializable { private static final long serialVersionUID = 1L; @Size(max = 5) @Pattern(regexp = "[a-zA-Z0-9]*") private String coupon; @NotNull // (1) @Valid // (2) private AddressForm receiverAddress; @NotNull @Valid private AddressForm senderAddress; // omitted setter/getter }
package com.example.sample.app.validation; import java.io.Serializable; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class AddressForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull @Size(min = 1, max = 50) private String name; @NotNull @Size(min = 1, max = 10) private String postcode; @NotNull @Size(min = 1, max = 100) private String address; // omitted setter/getter }
項番
説明
(1)子フォーム自体が必須であることを示す。この設定がない場合、receiverAddressにnullが設定されても、正常とみなされる。(2)ネストしたBeanのBean Validationを有効にするために、jakarta.validation.Validアノテーションを付与する。Controllerクラス
前述のControllerと違いはない。
package com.example.sample.app.validation; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("order") @Controller public class OrderController { @ModelAttribute public OrderForm setupForm() { return new OrderForm(); } @GetMapping(value = "order", params = "form") public String orderForm() { return "order/orderForm"; } @PostMapping(value = "order", params = "confirm") public String orderConfirm(@Validated OrderForm form, BindingResult result) { if (result.hasErrors()) { return "order/orderForm"; } return "order/orderConfirm"; } }
View
<!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)子フォームのフィールドは、親フィールド名.子フィールド名で指定する。<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <!--/* WEB-INF/views/order/orderForm.html */--> <head> <style type="text/css"> /* omitted (same as previous sample) */ </style> </head> <body> <form th:object="${orderForm}" method="post" class="form-horizontal" th:action="@{/order/order}"> <label for="coupon" name="coupon" th:errorclass="error-label">Coupon Code:</label> <input type="text" th:field="*{coupon}" th:errorclass="error-input"> <span id="coupon-errors" th:errors="*{coupon}" class="error-messages"></span> <br> <fieldset> <legend>Receiver</legend> <!--/* (1) */--> <span id="receiverAddress-errors" th:errors="*{receiverAddress}" class="error-messages"></span> <!--/* (2) */--> <label for="receiverAddress.name" name="receiverAddress.name" th:errorclass="error-label">Name:</label> <input type="text" th:field="*{receiverAddress.name}" th:errorclass="error-input" /> <span id="receiverAddress-name-errors" th:errors="*{receiverAddress.name}" class="error-messages"></span> <br> <label for="receiverAddress.postcode" name="receiverAddress.postcode" th:errorclass="error-label">Postcode:</label> <input type="text" th:field="*{receiverAddress.postcode}" th:errorclass="error-input" /> <span id="receiverAddress-postcode-errors" th:errors="*{receiverAddress.postcode}" class="error-messages"></span> <br> <label for="receiverAddress.address" name="receiverAddress.address" th:errorclass="error-label">Address:</label> <input type="text" th:field="*{receiverAddress.address}" th:errorclass="error-input" /> <span id="receiverAddress-address-errors" th:errors="*{receiverAddress.address}" class="error-messages"></span> </fieldset> <br> <fieldset> <legend>Sender</legend> <span id="senderAddress-errors" th:errors="*{senderAddress}" class="error-messages"></span> <label for="senderAddress.name" name="senderAddress.name" th:errorclass="error-label">Name:</label> <input type="text" th:field="*{senderAddress.name}" th:errorclass="error-input" /> <span id="senderAddress-name-errors" th:errors="*{senderAddress.name}" class="error-messages"></span> <br> <label for="senderAddress.postcode" name="senderAddress.postcode" th:errorclass="error-label">Postcode:</label> <input type="text" th:field="*{senderAddress.postcode}" th:errorclass="error-input" /> <span id="senderAddress-postcode-errors" th:errors="*{senderAddress.postcode}" class="error-messages"></span> <br> <label for="senderAddress.address" name="senderAddress.address" th:errorclass="error-label">Address:</label> <input type="text" th:field="*{senderAddress.address}" th:errorclass="error-input" /> <span id="senderAddress-address-errors" th:errors="*{senderAddress.address}" class="error-messages"></span> </fieldset> <button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button> </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 jakarta.validation.Valid; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class UserForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull @Size(min = 1, max = 20) private String name; @NotNull @Size(min = 1, max = 50) @Email private String email; @NotNull @Min(0) @Max(200) private Integer age; @NotNull @Size(min = 1, max = 3) // (1) @Valid private List<AddressForm> addresses; // omitted setter/getter }
項番
説明
(1)コレクションのサイズチェックにも、@Sizeアノテーションを使用できる。
View
<!DOCTYPE html> <html> <%-- WEB-INF/views/user/createForm.jsp --%> <head> <style type="text/css"> /* omitted (same as previous sample) */ </style> </head> <body> <form:form modelAttribute="userForm" method="post" class="form-horizontal" action="${pageContext.request.contextPath}/user/create"> <form:label path="name" cssErrorClass="error-label">Name:</form:label> <form:input path="name" cssErrorClass="error-input" /> <form:errors path="name" cssClass="error-messages" /> <br> <form:label path="email" cssErrorClass="error-label">Email:</form:label> <form:input path="email" cssErrorClass="error-input" /> <form:errors path="email" cssClass="error-messages" /> <br> <form:label path="age" cssErrorClass="error-label">Age:</form:label> <form:input path="age" cssErrorClass="error-input" /> <form:errors path="age" cssClass="error-messages" /> <br> <form:errors path="addresses" cssClass="error-messages" /><%-- (1) --%> <c:forEach items="${userForm.addresses}" varStatus="status"><%-- (2) --%> <fieldset class="address"> <legend>Address${f:h(status.index + 1)}</legend> <form:label path="addresses[${status.index}].name" cssErrorClass="error-label">Name:</form:label><%-- (3) --%> <form:input path="addresses[${status.index}].name" cssErrorClass="error-input" /> <form:errors path="addresses[${status.index}].name" cssClass="error-messages" /> <br> <form:label path="addresses[${status.index}].postcode" cssErrorClass="error-label">Postcode:</form:label> <form:input path="addresses[${status.index}].postcode" cssErrorClass="error-input" /> <form:errors path="addresses[${status.index}].postcode" cssClass="error-messages" /> <br> <form:label path="addresses[${status.index}].address" cssErrorClass="error-label">Address:</form:label> <form:input path="addresses[${status.index}].address" cssErrorClass="error-input" /> <form:errors path="addresses[${status.index}].address" cssClass="error-messages" /> <c:if test="${status.index > 0}"> <br> <button class="remove-address-button">Remove</button> </c:if> </fieldset> <br> </c:forEach> <button id="add-address-button">Add address</button> <br> <form:button name="confirm">Confirm</form:button> </form:form> <script type="text/javascript" src="${pageContext.request.contextPath}/resources/vendor/js/jquery-1.10.2.min.js"></script> <script type="text/javascript" src="${pageContext.request.contextPath}/resources/app/js/AddressesView.js"></script> </body> </html>
項番
説明
(1)addressesフィールドに対するエラーメッセージを表示する。(2)子フォームのコレクションを、<c:forEach>タグを使ってループで処理する。(3)コレクション中の子フォームのフィールドは、親フィールド名[インデックス].子フィールド名で指定する。<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <!--/* WEB-INF/views/user/createForm.html */--> <head> <style type="text/css"> /* omitted (same as previous sample) */ </style> </head> <body> <form th:object="${userForm}" method="post" class="form-horizontal" th:action="@{/user/create}"> <label for="name" name="name" th:errorclass="error-label">Name:</label> <input type="text" th:field="*{name}" th:errorclass="error-input"> <span id="name-errors" th:errors="*{name}" class="error-messages"></span> <br> <label for="email" name="email" th:errorclass="error-label">Email:</label> <input type="text" th:field="*{email}" th:errorclass="error-input"> <span id="email-errors" th:errors="*{email}" class="error-messages"></span> <br> <label for="age" name="age" th:errorclass="error-label">Age:</label> <input type="text" th:field="*{age}" th:errorclass="error-input"> <span id="age-errors" th:errors="*{age}" class="error-messages"></span> <br> <span id="addresses-errors" th:errors="*{addresses}" class="error-messages"></span><!--/* (1) */--> <fieldset class="address" th:each="address,status : *{addresses}"><!--/* (2) */--> <legend th:text="|Address${status.count}|">Address1</legend> <label th:for="|addresses${status.index}.name|" th:name="|addresses[${status.index}].name|" th:errorclass="error-label">Name:</label> <input type="text" th:field="*{addresses[__${status.index}__].name}" th:errorclass="error-input" /><!--/* (3) */--> <span th:id="|addresses${status.index}.name.errors|" th:errors="*{addresses[__${status.index}__].name}" class="error-messages"></span> <br> <label th:for="|addresses${status.index}.postcode|" th:name="|addresses[${status.index}].postcode|" th:errorclass="error-label">Postcode:</label> <input type="text" th:field="*{addresses[__${status.index}__].postcode}" th:errorclass="error-input" /> <span th:id="|addresses${status.index}.postcode.errors|" th:errors="*{addresses[__${status.index}__].postcode}" class="error-messages"></span> <br> <label th:for="|addresses${status.index}.address|" th:name="|addresses[${status.index}].address|" th:errorclass="error-label">Address:</label> <input type="text" th:field="*{addresses[__${status.index}__].address" th:errorclass="error-input" /> <span th:id="|addresses${status.index}.address.errors|" th:errors="*{addresses[__${status.index}__].address}" class="error-messages"></span> <span th:if="${status.index > 0}"> <br> <button class="remove-address-button">Remove</button> </span> </fieldset> <br> <button id="add-address-button">Add address</button> <br> <button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button> </form> <script type="text/javascript" th:src="@{/resources/vendor/js/jquery-1.10.2.min.js}"></script> <script type="text/javascript" th:src="@{/resources/app/js/AddressesView.js}"></script> </body> </html>
項番
説明
(1)addressesフィールドに対するエラーメッセージを表示する。(2)子フォームのコレクションを、th:each属性を使ってループで処理する。(3)コレクション中の子フォームのフィールドは、親フィールド名[インデックス].子フィールド名で指定する。なお、インデックスを先に評価させる必要があるため、プリプロセッシング式(__${...}__)を利用している。プリプロセッシング式の詳細については、プリプロセッシングを参照されたい。
Controllerクラス
package com.example.sample.app.validation; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("user") public class UserController { @ModelAttribute public UserForm setupForm() { UserForm form = new UserForm(); List<AddressForm> addresses = new ArrayList<AddressForm>(); addresses.add(new AddressForm()); form.setAddresses(addresses); // (1) return form; } @GetMapping(value = "create", params = "form") public String createForm() { return "user/createForm"; } @PostMapping(value = "create", params = "confirm") public String createConfirm(@Validated UserForm form, BindingResult result) { if (result.hasErrors()) { return "user/createForm"; } return "user/createConfirm"; } }
項番
説明
(1)「ユーザー登録」フォーム初期表示時に、一件の住所フォームを表示させるために、フォームオブジェクトを編集する。
JavaScript
動的にアドレス入力フィールドを追加するためのJavaScriptも記載するが、このコードの説明は、本質的ではないため割愛する。
// webapp/resources/app/js/AddressesView.js function AddressesView() { this.addressSize = $('fieldset.address').size(); }; AddressesView.prototype.addAddress = function() { var $address = $('fieldset.address'); var newHtml = addressTemplate(this.addressSize++); $address.last().next().after($(newHtml)); }; AddressesView.prototype.removeAddress = function($fieldset) { $fieldset.next().remove(); // remove <br> $fieldset.remove(); // remove <fieldset> }; function addressTemplate(number) { return '\ <fieldset class="address">\ <legend>Address' + (number + 1) + '</legend>\ <label for="addresses' + number + '.name">Name:</label>\ <input id="addresses' + number + '.name" name="addresses[' + number + '].name" type="text" value=""/><br>\ <label for="addresses' + number + '.postcode">Postcode:</label>\ <input id="addresses' + number + '.postcode" name="addresses[' + number + '].postcode" type="text" value=""/><br>\ <label for="addresses' + number + '.address">Address:</label>\ <input id="addresses' + number + '.address" name="addresses[' + number + '].address" type="text" value=""/><br>\ <button class="remove-address-button">Remove</button>\ </fieldset>\ <br>\ '; } $(function() { var addressesView = new AddressesView(); $('#add-address-button').on('click', function(e) { e.preventDefault(); addressesView.addAddress(); }); $(document).on('click', '.remove-address-button', function(e) { if (this === e.target) { e.preventDefault(); var $this = $(this); // this button var $fieldset = $this.parent(); // fieldset addressesView.removeAddress($fieldset); } }); });
フォームは、以下のように表示される。
「Add address」ボタンを2回押して、住所フォームを2件追加する。
このフォームに対して、すべての入力フィールドを未入力のまま送信すると、以下のようにエラーメッセージが表示される。
4.2.2.2.5. コレクション内の値のチェック¶
複数選択可能な画面項目(チェックボックスや複数選択ドロップダウンなど)を扱う際は、フォームクラスで画面項目を String等の基本型のコレクションとして扱うことが一般的である。
ここでは、Bean Validation 2.0の標準アノテーションである@NotEmpty及び共通ライブラリが提供する@ExistInCodeListを例に、コレクション内の値の入力チェックを行う例を示す。
フォームクラス
package com.example.sample.app.validation; import java.util.List; import jakarta.validation.constraints.NotEmpty; import org.terasoluna.gfw.common.codelist.ExistInCodeList; public class SampleForm { @NotEmpty private List<@NotEmpty @ExistInCodeList(codeListId = "CL_ROLE") String> roles; // (1) public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } }
項番
説明
(1)入力チェック対象となるコレクションの型引数に対して@NotEmptyアノテーション及び@ExistInCodeListアノテーションを設定する。@ExistInCodeListアノテーションのcodeListIdパラメータにチェック元となるコードリストを指定する。View
<form:form modelAttribute="sampleForm"> <!-- (1) --> <form:checkboxes path="roles" items="${CL_ROLE}"/> <form:errors path="roles*"/> <form:button>Submit</form:button> </form:form>
項番
説明
(1)<form:checkboxes>を実装する。<form th:object="${roleForCollectionForm}"> <!-- (1) --> <span th:each="var : ${CL_ROLE}"> <input type="checkbox" th:field="*{roles}" th:value="${var.key}" /> <label th:for="${#ids.prev('roles')}" th:text="${var.value}" /> </span> <span id="roles*.errors" th:errors="*{roles*}"></span> <button type="submit">Submit</button> </form>
項番
説明
(1)チェックボックスを実装する。Note
#ids.prevメソッドについて
#idsを利用すると、繰り返し処理の中でIDを生成するのが容易になる。上記の実装例では、labelタグのfor属性に#ids.prevメソッドを利用して対応するチェックボックス(<input type="checkbox">)のIDを取得している。通常、
#ids.prevメソッドは直前に#ids.seqメソッドを使用して生成されたIDを取得するために利用するが、チェックボックスにth:field属性を付与した場合は内部的に#ids.seqメソッドと同等の処理を実行してIDを生成するため、#ids.prevメソッドを利用してIDを取得することが可能である。#idsの詳細については、Tutorial: Using Thymeleaf -IDs-を参照されたい。
4.2.2.2.6. バリデーションのグループ化¶
バリデーショングループを作成し、一つのフィールドに対して、グループごとに入力チェックルールを指定することができる。
前述の「新規ユーザー登録」の例で、ageフィールドに「成年であること」というルールを追加する。「成年かどうか」は国によってルールが違うため、countryフィールドも追加する。
Bean Validationでグループを指定する場合、アノテーションのgroup属性に、グループを示す任意のjava.lang.Classオブジェクトを設定する。
ここでは、以下の3グループ(interface)を作成する。
グループ |
成人条件 |
|---|---|
|
18歳以上 |
|
20歳以上 |
|
21歳以上 |
このグループを使ってバリデーションを実行する例を示す。
フォームクラス
package com.example.sample.app.validation; import java.io.Serializable; import java.util.List; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class UserForm implements Serializable { private static final long serialVersionUID = 1L; // (1) public static interface Chinese { }; public static interface Japanese { }; public static interface Singaporean { }; @NotNull @Size(min = 1, max = 20) private String name; @NotNull @Size(min = 1, max = 50) @Email private String email; @NotNull @Min(value = 18, groups = Chinese.class) // (2) @Min(value = 20, groups = Japanese.class) @Min(value = 21, groups = Singaporean.class) @Max(200) private Integer age; @NotNull @Size(min = 2, max = 2) private String country; // (3) // omitted setter/getter }
項番
説明
(1)グループクラスを指定するために、各グループをインタフェースで定義する。(2)グループごとにルールを定義する。グループを指定するために、groups属性に対象のグループクラスを指定する。groups属性を省略した場合、jakarta.validation.groups.Defaultグループが使用される。(3)グループを振り分けるための、フィールドを追加する。
View
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>
テンプレートHTMLに大きな変更はない。
<form th:object="${userForm}" method="post" class="form-horizontal" th:action="@{/user/create}"> <label for="name" name="name" th:errorclass="error-label">Name:</label> <input type="text" th:field="*{name}" th:errorclass="error-input"> <span id="name-errors" th:errors="*{name}" class="error-messages"></span> <br> <label for="email" name="email" th:errorclass="error-label">Email:</label> <input type="text" th:field="*{email}" th:errorclass="error-input"> <span id="email-errors" th:errors="*{email}" class="error-messages"></span> <br> <label for="age" name="age" th:errorclass="error-label">Age:</label> <input type="text" th:field="*{age}" th:errorclass="error-input"> <span id="age-errors" th:errors="*{age}" class="error-messages"></span> <br> <label for="country" name="country" th:errorclass="error-label">Country:</label> <select th:field="*{country}" th:errorclass="error-input"> <option value="cn">China</option> <option value="jp">Japan</option> <option value="sg">Singapore</option> </select> <span id="country-errors" th:errors="*{country}" class="error-messages"></span> <br> <button id="confirm" name="confirm" type="submit" value="Submit">Confirm</button> </form>
Controllerクラス
@Validatedに、対象のグループを設定することで、バリデーションルールを変更できる。package com.example.sample.app.validation; import jakarta.validation.groups.Default; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import com.example.sample.app.validation.UserForm.Chinese; import com.example.sample.app.validation.UserForm.Japanese; import com.example.sample.app.validation.UserForm.Singaporean; @Controller @RequestMapping("user") public class UserController { @ModelAttribute public UserForm setupForm() { UserForm form = new UserForm(); return form; } @GetMapping(value = "create", params = "form") public String createForm() { return "user/createForm"; } String createConfirm(UserForm form, BindingResult result) { if (result.hasErrors()) { return "user/createForm"; } return "user/createConfirm"; } @PostMapping(value = "create", params = { "confirm", "country=cn" }) // (1) public String createConfirmForChinese(@Validated({Chinese.class, Default.class }) // (2) UserForm form, BindingResult result) { return createConfirm(form, result); } @PostMapping(value = "create", params = { "confirm", "country=jp" }) public String createConfirmForJapanese(@Validated({ Japanese.class, Default.class }) UserForm form, BindingResult result) { return createConfirm(form, result); } @PostMapping(value = "create", params = { "confirm", "country=sg" }) public String createConfirmForSingaporean(@Validated({ Singaporean.class, Default.class }) UserForm form, BindingResult result) { return createConfirm(form, result); } }
項番
説明
(1)グループを振り分けるためのパラメータの条件を、param属性に追加する。(2)@Min以外のアノテーションは、Defaultグループに属しているため、Defaultの指定も必要である。
この例では、各入力値の組み合わせに対するチェック結果は、以下の表の通りである。
|
|
入力チェック結果 |
エラーメッセージ |
|---|---|---|---|
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歳以上をデフォルトルールとしたい場合を考える。
ルールは、以下のようになる。
グループ |
成人条件 |
|---|---|
|
20歳以上 |
|
21歳以上 |
上記以外の国( |
18歳以上 |
フォームクラス
Defaultグループに意味を持たせるため、@Min以外のアノテーションにも、明示的に全グループを指定する必要がある。package com.example.sample.app.validation; import java.io.Serializable; import java.util.List; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import jakarta.validation.groups.Default; public class UserForm implements Serializable { private static final long serialVersionUID = 1L; public static interface Japanese { }; public static interface Singaporean { }; @NotNull(groups = { Default.class, Japanese.class, Singaporean.class }) // (1) @Size(min = 1, max = 20, groups = { Default.class, Japanese.class, Singaporean.class }) private String name; @NotNull(groups = { Default.class, Japanese.class, Singaporean.class }) @Size(min = 1, max = 50, groups = { Default.class, Japanese.class, Singaporean.class }) @Email(groups = { Default.class, Japanese.class, Singaporean.class }) private String email; @NotNull(groups = { Default.class, Japanese.class, Singaporean.class }) @Min(value = 18, groups = Default.class) // (2) @Min(value = 20, groups = Japanese.class) @Min(value = 21, groups = Singaporean.class) @Max(value = 200, groups = { Default.class, Japanese.class, Singaporean.class }) private Integer age; @NotNull(groups = { Default.class, Japanese.class, Singaporean.class }) @Size(min = 2, max = 2, groups = { Default.class, Japanese.class, Singaporean.class }) private String country; // omitted setter/getter }
項番
説明
(1)@Min以外のアノテーションにも、全グループを設定する。(2)Defaultグループに対するルールを設定する。View
JSP/テンプレートHTMLに変更はない
Controllerクラス
package com.example.sample.app.validation; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import com.example.sample.app.validation.UserForm.Japanese; import com.example.sample.app.validation.UserForm.Singaporean; @Controller @RequestMapping("user") public class UserController { @ModelAttribute public UserForm setupForm() { UserForm form = new UserForm(); return form; } @GetMapping(value = "create", params = "form") public String createForm() { return "user/createForm"; } String createConfirm(UserForm form, BindingResult result) { if (result.hasErrors()) { return "user/createForm"; } return "user/createConfirm"; } @PostMapping(value = "create", params = { "confirm" }) public String createConfirmForDefault(@Validated /* (1) */ UserForm form, BindingResult result) { return createConfirm(form, result); } @PostMapping(value = "create", params = { "confirm", "country=jp" }) public String createConfirmForJapanese( @Validated(Japanese.class) /* (2) */ UserForm form, BindingResult result) { return createConfirm(form, result); } @PostMapping(value = "create", params = { "confirm", "country=sg" }) public String createConfirmForSingaporean( @Validated(Singaporean.class) UserForm form, BindingResult result) { return createConfirm(form, result); } }
項番
説明
(1)countryフィールド指定がない場合に、Defaultグループが使用されるように設定する。(2)countryフィールド指定がある場合に、Defaultグループが含まれないように設定する。
バリデーショングループを使用する方法について、2パターン説明した。
前者はDefaultグループをControllerクラスで使用し、後者はDefaultグループをフォームクラスで使用した。
パターン |
メリット |
デメリット |
使用の判断ポイント |
|---|---|---|---|
|
グループ化する必要のないルールは、 |
グループの全パターンを定義する必要があるので、グループパターンが多いと、定義が困難になる。 |
グループパターンが、数種類の場合に使用すべき(新規作成グループ、更新グループ、削除グループ等) |
|
デフォルトに属さないグループのみ定義すればよいため、パターンが多くても対応できる。 |
グループ化する必要のないルールにも、 |
グループパターンにデフォルト値を設定できる(グループの大多数に共通項がある)場合に使用すべき |
Note
これまでの例ではバリデーショングループの切り替えは、リクエストパラメータ等、@GetMappingや@PostMappingで指定できるパラメータによって行った。この方法では認証オブジェクトが有する権限情報など、@GetMappingや@PostMappingでは扱えない情報でグループを切り替えることはできない。
この場合は、@Validatedアノテーションを使用せず、org.springframework.validation.SmartValidatorを使用し、Controllerのハンドラメソッド内でグループを指定したバリデーションを行えばよい。
@Controller @RequestMapping("user") public class UserController { @Inject SmartValidator smartValidator; // (1) // omitted @PostMapping(value = "create", params = "confirm") public String createConfirm(/* (2) */ UserForm form, BindingResult result) { // (3) Class<?> validationGroup = Default.class; // logic to determine validation group // if (xxx) { // validationGroup = Xxx.class; // } smartValidator.validate(form, result, validationGroup); // (4) if (result.hasErrors()) { return "user/createForm"; } return "user/createConfirm"; } }
項番
説明
(1)SmartValidatorをインジェクションする。SmartValidatorは<mvc:annotation-driven>の設定が行われていれば使用できるため、別途Bean定義不要である。 (2)@Validatedアノテーションは使わない。 (3) バリデーショングループを決定する。バリデーショングループを決定するロジックは、Helperクラスに委譲して、Controller内のロジックをシンプルな状態に保つことを推奨する。 (4)SmartValidatorのvalidateメソッドを使用して、グループを指定したバリデーションを実行する。グループの指定は可変長引数になっており、複数指定できる。
基本的にはControllerにロジックを書くべきではないため、@GetMappingや@PostMappingの属性でルールを切り替えられるのであればSmartValidatorは使わない方がよい。
4.2.2.3. 相関項目チェック¶
複数フィールドにまたがる相関項目チェックには、Spring Validator(org.springframework.validation.Validatorインタフェースを実装したValidator)、または、Bean Validationを用いる。
それぞれ説明するが、先にそれぞれの特徴と推奨する使い分けを述べる。
方式 |
特徴 |
用途 |
|---|---|---|
Spring Validator
|
特定のクラスに対する入力チェックの作成が容易である。
Controllerでの利用が不便。
|
特定のフォームに依存した業務要件固有の入力チェック実装
|
Bean Validation
|
入力チェックの作成はSpring Validatorほど容易でない。
Controllerでの利用が容易。
|
特定のフォームに依存しない、開発プロジェクト共通の入力チェック実装
|
4.2.2.3.1. Spring Validatorによる相関項目チェック実装¶
フィールド名 |
型 |
ルール |
説明 |
|---|---|---|---|
password
|
java.lang.String |
入力必須
8文字以上
|
パスワード
|
confirmPassword
|
java.lang.String |
passwordが入力されている場合、passwordと同じ値であること
|
確認用パスワード
|
「passwordが入力されている場合、passwordと同じ値であること」というルールはpasswordフィールドとconfirmPasswordフィールドの両方の情報が必要であるため、相関項目チェックルールである。
フォームクラス
相関項目チェックルール以外は、これまで通りBean Validationのアノテーションで実装する。
package com.example.sample.app.validation; import java.io.Serializable; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public class PasswordResetForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull @Size(min = 8) private String password; private String confirmPassword; // omitted setter/getter }
Note
パスワードは、通常ハッシュ化してデータベースに保存するため、最大値のチェックは行わなくても良い。
Validatorクラス
org.springframework.validation.Validatorインタフェースを実装して、相関項目チェックルールを実現する。package com.example.sample.app.validation; import java.util.Objects; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @Component // (1) public class PasswordEqualsValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return PasswordResetForm.class.isAssignableFrom(clazz); // (2) } @Override public void validate(Object target, Errors errors) { PasswordResetForm form = (PasswordResetForm) target; String password = form.getPassword(); String confirmPassword = form.getConfirmPassword(); if (!StringUtils.hasLength(password)) { // (3) return; } if (!Objects.equals(password, confirmPassword)) { // (4) errors.rejectValue(/* (5) */ "confirmPassword", /* (6) */ "PasswordEqualsValidator.passwordResetForm.password", /* (7) */ "password and confirm password must be same."); } } }
項番
説明
(1)@Componentを付与し、Validatorをコンポーネントスキャン対象にする。(2)このValidatorのチェック対象であるかどうかを判別する。ここでは、PasswordResetFormクラスをチェック対象とする。(3)passwordフィールドが未入力の場合は、このValidatorで相関チェックは行わない。相関チェックを必ず行う必要がある場合は、この判定ロジックは不要である。(4)チェックロジックを実装する。(5)エラー対象のフィールド名を指定する。(6)エラーメッセージのコード名を指定する。ここではコードを、“バリデータ名.フォーム属性名.プロパティ名”とする。メッセージ定義はapplication-messages.propertiesに定義するメッセージを参照されたい。(7)エラーメッセージをコードで解決できなかった場合に使用する、デフォルトメッセージを設定する。Note
Spring Validator実装クラスは、使用するControllerと同じパッケージに配置することを推奨する。
Controllerクラス
package com.example.sample.app.validation; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import jakarta.inject.Inject; @Controller @RequestMapping("password") public class PasswordResetController { @Inject PasswordEqualsValidator passwordEqualsValidator; // (1) @ModelAttribute public PasswordResetForm setupForm() { return new PasswordResetForm(); } @InitBinder public void initBinder(WebDataBinder binder) { binder.addValidators(passwordEqualsValidator); // (2) } @GetMapping(value = "reset", params = "form") public String resetForm() { return "password/resetForm"; } @PostMapping(value = "reset") public String reset(@Validated PasswordResetForm form, BindingResult result) { // (3) if (result.hasErrors()) { return "password/resetForm"; } return "redirect:/password/reset?complete"; } @GetMapping(value = "reset", params = "complete") public String resetComplete() { return "password/resetComplete"; } }
項番
説明
(1)使用するSpring Validatorを、インジェクションする。(2)@InitBinderアノテーションがついたメソッド内で、WebDataBinder.addValidatorsメソッドにより、Validatorを追加する。これにより、@Validatedアノテーションでバリデーションをする際に、追加したValidatorも呼び出される。(3)入力チェックの実装は、これまで通りである。View
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>タグを使用すると、再表示時に、データがクリアされる。テンプレートHTMLに特筆すべき点はない。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <!--/* WEB-INF/views/password/resetForm.html */--> <head> <style type="text/css"> /* omitted */ </style> </head> <body> <form th:object="${passwordResetForm}" method="post" class="form-horizontal" th:action="@{/password/reset}"> <label for="password" name="password" th:errorclass="error-label">Password:</label> <input type="password" th:field="*{password}" th:errorclass="error-input"> <span id="password-errors" th:errors="*{password}" class="error-messages"></span> <br> <label for="confirmPassword" name="confirmPassword" th:errorclass="error-label">Password (Confirm):</label> <input type="password" th:field="*{confirmPassword}" th:errorclass="error-input"> <span id="confirmPassword-errors" th:errors="*{confirmPassword}" class="error-messages"></span> <br> <button type="submit" value="Submit">Reset</button> </form> </body> </html>
passwordフィールドと、confirmPasswordフィールドに、別の値を入力してフォームを送信した場合は、以下のようにエラーメッセージが表示される。
Note
パスワード入力フィールド(
<input type="password">)にth:field属性を付与すると、再表示時に、データがクリアされる。
Note
相関チェック対象の複数フィールドに対してエラー情報を設定することも可能である。ただし、必ずエラーメッセージの表示とスタイル適用がセットで行われ、いずれか片方のみを行うことはできない。
相関チェックエラーとなった両方のフィールドにスタイル適用したいが、エラーメッセージは1つだけ表示したいような場合は、エラーメッセージに空文字を設定することで実現することが可能である。
以下に、passwordフィールドとconfirmPasswordフィールドにスタイルを適用し、confirmPasswordフィールドのみにエラーメッセージを表示する例を示す。
package com.example.sample.app.validation; import java.util.Objects; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @Component public class PasswordEqualsValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return PasswordResetForm.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { // omitted if (!Objects.equals(password, confirmPassword)) { // register a field error for confirmPassword errors.rejectValue("confirmPassword", "PasswordEqualsValidator.passwordResetForm.confirmPassword", "password and confirm password must be same."); // register a field error for password errors.rejectValue("password", // (1) "PasswordEqualsValidator.passwordResetForm.password", // (2) ""); // (3) } } }
項番
説明
(1)passwordフィールドのエラーを登録する。 (2) エラーメッセージのコード名を指定する。この際、対応するエラーメッセージに空文字を指定する。メッセージ定義はapplication-messages.propertiesに定義するメッセージを参照されたい。 (3) エラーメッセージをコードで解決できなかった場合に使用する、デフォルトメッセージを設定する。上記の例では空文字を設定している。
Warning
@InitBinderアノテーションを付与したメソッドでValidatorが登録されると、Validatorの supportsメソッドでValidatorのサポート対象の型かどうか判定される。このとき、サポート対象でない場合はjava.lang.IllegalStateExceptionが発生する点に注意されたい。
@InitBinderアノテーションを付与したメソッドは、Modelに独自の型のオブジェクトが追加された際に必ず実行されるが、 @InitBinder("xxx")でモデル名を指定することで、適用範囲を限定することが可能である。
例えば以下のようなケースが該当するため、注意されたい。
一つのControllerで複数のフォームを扱う場合(複数のフォームオブジェクトを
@ModelAttributeアノテーションを付与したメソッドで登録する場合や、ハンドラメソッドの引数として受け取る場合)フォームオブジェクトに限らず、ハンドラメソッドの引数として受け取った
Modelに、ResultMessagesオブジェクトやPageオブジェクトなどの独自の型のオブジェクトを登録する場合同様に
RedirectAttributesに独自の型のオブジェクトを登録する場合
以下に、一つのControllerで複数のフォームを扱う場合の実装例を示す。
@Controller @RequestMapping("xxx") public class XxxController { // omitted @ModelAttribute("aaa") public AaaForm() { return new AaaForm(); } @ModelAttribute("bbb") public BbbForm() { return new BbbForm(); } @InitBinder("aaa") public void initBinderForAaa(WebDataBinder binder) { // add validators for AaaForm binder.addValidators(aaaValidator); } @InitBinder("bbb") public void initBinderForBbb(WebDataBinder binder) { // add validators for BbbForm binder.addValidators(bbbValidator); } // omitted }
Note
相関項目チェックルールのチェック内容をバリデーショングループに応じて変更したい場合(例えば、特定のバリデーショングループが指定された場合だけ相関項目チェックを実施したい場合など)は、 org.springframework.validation.Validatorインターフェイスを実装する代わりに、 org.springframework.validation.SmartValidatorインターフェイスを実装し、validateメソッド内で処理を切り替えるとよい。
package com.example.sample.app.validation; import org.apache.commons.lang3.ArrayUtils; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; @Component public class PasswordEqualsValidator implements SmartValidator { // Implements SmartValidator instead of Validator interface @Override public boolean supports(Class<?> clazz) { return PasswordResetForm.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { validate(target, errors, new Object[] {}); } @Override public void validate(Object target, Errors errors, Object... validationHints) { // Check validationHints(groups) and apply validation logic only when 'Update.class' is specified if (ArrayUtils.contains(validationHints, Update.class)) { PasswordResetForm form = (PasswordResetForm) target; String password = form.getPassword(); String confirmPassword = form.getConfirmPassword(); // omitted... } } }
4.2.2.3.2. Bean Validationによる相関項目チェック実装¶
Bean Validationによって、相関項目チェックの実装するためには、独自バリデーションルールの追加を行う必要がある。
How to extendにて説明する。
4.2.2.4. エラーメッセージの定義¶
入力チェックエラーメッセージを変更する方法を説明する。
Spring MVCによるBean Validationのエラーメッセージは、以下の順で解決される。
org.springframework.context.MessageSourceに定義されているメッセージの中に、ルールに合致するものがあればそれをエラーメッセージとして使用する (Springのルール)。Springのデフォルトのルールについては、「DefaultMessageCodesResolverのJavaDoc」を参照されたい。1.でメッセージが見つからない場合、アノテーションの
message属性に、指定されたメッセージからエラーメッセージを取得する (Bean Validationのルール)
message属性に指定されたメッセージが、”{メッセージキー}”形式でない場合、そのテキストをエラーメッセージとして使用する。
message属性に指定されたメッセージが、”{メッセージキー}”形式の場合、クラスパス直下のValidationMessages.propertiesから、メッセージキーに対応するメッセージを探す。メッセージキーに対応するメッセージが定義されている場合は、そのメッセージを使用する
メッセージキーに対応するメッセージが定義されていない場合は、”{メッセージキー}”をそのままエラーメッセージとして使用する
基本的にエラーメッセージは、propertiesファイルに定義することを推奨する。
定義する箇所は、以下の2パターン存在する。
org.springframework.context.MessageSourceが読み込むpropertiesファイルクラスパス直下のValidationMessages.properties
以下の説明では、ApplicationContext設定ファイルに次の設定があることを前提とし、前者を”application-messages.properties”、後者を”ValidationMessages.properties”と呼ぶ。
@Bean("messageSource")
public MessageSource messageSource() {
ResourceBundleMessageSource bean = new ResourceBundleMessageSource();
bean.setBasenames("i18n/application-messages");
return bean;
}
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>i18n/application-messages</value>
</list>
</property>
</bean>
Warning
ValidationMessages.propertiesファイルは、クラスパスの直下に複数存在させてはいけない。
クラスパスの直下に複数のValidationMessages.propertiesファイルが存在する場合、いずれか1つのファイルが読み込まれ、他のファイルが読み込まれないため、適切なメッセージが表示されない可能性がある。
マルチプロジェクト構成を採用する場合は、
ValidationMessages.propertiesファイルを複数のプロジェクトに配置しないように注意すること。Bean Validation用の共通部品をjarファイルとして配布する際に、
ValidationMessages.propertiesファイルをjarファイルの中に含めないように注意すること。
ブランクプロジェクト からプロジェクトを生成した場合は、xxx-web/src/main/resourcesの直下にValidationMessages.propertiesが格納されている。
本ガイドラインでは、以下のように定義を分けることを推奨する。
プロパティファイル名 |
定義する内容 |
|---|---|
ValidationMessages.properties
|
システムで定めたBean Validationのデフォルトエラーメッセージ
|
application-messages.properties
|
個別で上書きしたいBean Validationのエラーメッセージ
Spring Validatorで実装した入力チェックのエラーメッセージ
|
ValidationMessages.propertiesを用意しない場合は、Hibernate Validatorが用意するデフォルトメッセージが使用される。
MessageSourceと連携することで、日本語メッセージをNative to Asciiせずに直接扱うことができる。詳細は、Native to Asciiを行わないメッセージの読み込みを参照されたい。
4.2.2.4.1. ValidationMessages.propertiesに定義するメッセージ¶
クラスパス直下(通常src/main/resources)のValidationMessages.properties内の、Bean Validationのアノテーションのmessage属性に指定されたメッセージキーに対して、メッセージを定義する。
基本的な単項目チェックの初めに使用した、以下のフォームを用いて説明する。
フォームクラス(再掲)
public class UserForm implements Serializable { @NotNull @Size(min = 1, max = 20) private String name; @NotNull @Size(min = 1, max = 50) @Email private String email; @NotNull @Min(0) @Max(200) private Integer age; // omitted getter/setter }
ValidationMessages.properties
@NotNull,@Size,@Min,@Max,@Emailのエラーメッセージを変更する。jakarta.validation.constraints.NotNull.message=is required. # (1) jakarta.validation.constraints.Size.message=size is not in the range {min} through {max}. jakarta.validation.constraints.Min.message=can not be less than {value}. jakarta.validation.constraints.Max.message=can not be greater than {value}. jakarta.validation.constraints.Email.message=is an invalid e-mail address.
項番
説明
(1)アノテーションに指定した属性値は、{属性名}で埋め込むことができる。
この設定を加えた状態で、すべての入力フィールドを未入力のままフォームを送信すると、以下のように変更したエラーメッセージが、表示される。
Warning
Bean Validation標準のアノテーションやHibernate Validator独自のアノテーションにはmessage属性に{アノテーションのFQCN.message}という値が設定されているため、
アノテーションのFQCN.message=メッセージ
という形式でプロパティファイルにメッセージを定義すればよいが、すべてのアノテーションが、この形式になっているわけではないので、 対象のアノテーションのJavadocまたはソースコードを確認すること。
エラーメッセージに、フィールド名を含める場合は、以下のように、メッセージに{0}を加える。
ValidationMessages.properties
@NotNull、@Size、@Min、@Max、@Emailのエラーメッセージを変更する。jakarta.validation.constraints.NotNull.message="{0}" is required. jakarta.validation.constraints.Size.message=The size of "{0}" is not in the range {min} through {max}. jakarta.validation.constraints.Min.message="{0}" can not be less than {value}. jakarta.validation.constraints.Max.message="{0}" can not be greater than {value}. jakarta.validation.constraints.Email.message="{0}" is an invalid e-mail address.
エラーメッセージは、以下のように変更される。
このままでは、フォームクラスのプロパティ名が表示されてしまい、ユーザーフレンドリではない。適切なフィールド名を表示したい場合は、application-messages.propertiesに
フォームのプロパティ名=表示するフィールド名
形式でフィールド名を定義すればよい。
これまでの例に、以下の設定を追加する。
application-messages.properties
name=Name email=Email age=Age
エラーメッセージは、以下のように変更される。
Note
{0}でフィールド名を埋め込めるのは、Bean Validationの機能ではなく、Springの機能である。したがって、フィールド名変更の設定は、Spring管理下のapplication-messages.properties(ResourceBundleMessageSource)に定義する必要がある。
Tip
Bean Validation 1.1より、ValidationMessages.propertiesに指定するメッセージの中にExpression Language(以降、「EL式」と呼ぶ)を使用する事ができるようになった。Hibernate Validator 8.xでは、Expression Language 5.0以上をサポートしている。
実行可能なEL式のバージョンは、アプリケーションサーバのバージョンによって異なる。そのため、EL式を使用する場合は、アプリケーションサーバがサポートしているEL式のバージョンを確認した上で使用すること。
以下に、Hibernate Validatorがデフォルトで用意しているValidationMessages.propertiesに定義されているメッセージを例に、EL式の使用例を示す。
# ... # (1) jakarta.validation.constraints.DecimalMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value} # ...
項番
説明
(1)メッセージの中の 「
${inclusive == true ? 'or equal to ' : ''}」の部分がEL式である。上記のメッセージ定義から実際に生成されるメッセージのパターンは、
must be less than or equal to {value}
must be less than {value}
の2パターンとなる。(
{value}の部分には、@DecimalMaxアノテーションのvalue属性に指定した値が埋め込まれる)前者は
@DecimalMaxアノテーションのinclusive属性にtrueを指定した場合(又は指定しなかった場合)、 後者は@DecimalMaxアノテーションのinclusive属性にfalseを指定した場合に生成される。Bean ValidationにおけるEL式の扱いについては、Hibernate Validator Reference Guide(Interpolation with message expressions)を参照されたい。
また、ValidationMessages.propertiesに指定するメッセージに ${validatedValue}を使用することで、エラーメッセージにチェック対象の値を含むことができる。
以下に、 ${validatedValue}の使用例を示す。
# ... # (1) jakarta.validation.constraints.Pattern.message = The value entered "${validatedValue}" is invalid. # ...
項番
説明
(1) 上記のメッセージ定義から実際に生成されるメッセージは、${validatedValue}の部分にフォームに入力した値が埋め込まれる。入力値に機密情報を含む場合、機密情報がメッセージに表示されないようにするため、${validatedValue}を使用しないように注意すること。
4.2.2.4.2. application-messages.propertiesに定義するメッセージ¶
ValidationMessages.propertiesでシステムが利用するデフォルトのメッセージを定義したが、画面によっては、デフォルトメッセージから変更したい場合が出てくる。
その場合、application-messages.propertiesに、以下の形式でメッセージを定義する。
アノテーション名.フォーム属性名.プロパティ名=対象のメッセージ
ValidationMessages.propertiesに定義するメッセージの設定がある前提で、以下の設定でemailとageフィールドのメッセージを上書きする。
application-messages.properties
# override messages # for email field Size.userForm.email=The size of "{0}" must be between {2} and {1}. # for age field NotNull.userForm.age="{0}" is compulsory. Min.userForm.age="{0}" must be greater than or equal to {1}. Max.userForm.age="{0}" must be less than or equal to {1}. # filed names name=Name email=Email age=Age
アノテーションの属性値は、{1}以降に埋め込まれる。なお、属性値のインデックス位置は、アノテーションの属性名のアルファベット順(昇順)となる。
例えば、@Sizeのインデックス位置は、
{0}: プロパティ名 (物理名又は論理名){1}:max属性の値{2}:min属性の値
エラーメッセージは以下のように変更される。
Note
application-messages.propertiesのメッセージキーの形式はこれ以外にも用意されているが、デフォルトメッセージを一部上書きする目的で使用するのであれば、基本的に、アノテーション名.フォーム属性名.プロパティ名形式でよい。
4.2.3. How to extend¶
Bean Validationは標準で用意されているチェックルール以外に、独自ルール用アノテーションを作成する仕組みをもつ。
作成方法は大きく分けて、以下の観点で分かれる。
既存ルールの組み合わせ
新規ルールの作成
基本的には、以下の雛形を使用して、ルール毎にアノテーションを作成する。
package com.example.common.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import com.example.common.validation.Xxx.List;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface Xxx {
String message() default "{com.example.common.validation.Xxx.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
Xxx[] value();
}
}
4.2.3.1. 既存ルールを組み合わせたBean Validationアノテーションの作成¶
システム共通で、
文字列は半角英数字の文字種に限定したい
数値は8桁までの正の数に限定したい
「ユーザーID」は、4文字以上20文字以下の半角英字に制限したい
「年齢」は、1歳以上150歳以下に制限したい
@Pattern、@Size、@Min、@Max等を組み合わせることでも実現できるが、以下に、実装例を示す。
半角英数字の文字種に限定する
@Alphanumericアノテーションの実装例package com.example.common.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.common.validation.AlphaNumeric.List; import jakarta.validation.Constraint; import jakarta.validation.Payload; import jakarta.validation.ReportAsSingleViolation; import jakarta.validation.constraints.Pattern; @Documented @Constraint(validatedBy = {}) // (1) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) @ReportAsSingleViolation // (2) @Pattern(regexp = "[a-zA-Z0-9]*") // (3) public @interface AlphaNumeric { String message() default "{com.example.common.validation.AlphaNumeric.message}"; // (4) Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { AlphaNumeric[] value(); } }
項番
説明
(1)既存のアノテーションを利用して実装を行う場合、validatedByは空にしておく必要がある。(2)エラーメッセージをまとめ、エラー時はこのアノテーションによるメッセージだけを変えるようにする。(3)このアノテーションにより使用されるルールを定義する。(4)エラーメッセージのデフォルト値を定義する。8桁までの正の数に限定する
@MaxDigitsアノテーションの実装例package com.example.common.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.common.validation.MaxDigits.List; import jakarta.validation.Constraint; import jakarta.validation.Payload; import jakarta.validation.ReportAsSingleViolation; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.PositiveOrZero; @Documented @Constraint(validatedBy = {}) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) @ReportAsSingleViolation @PositiveOrZero @Max(value = 99999999) public @interface MaxDigits { String message() default "{com.example.common.validation.MaxDigits.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { MaxDigits[] value(); } }
「ユーザーID」のフォーマットを規定する
@UserIdアノテーションの実装例package com.example.sample.domain.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.sample.domain.validation.UserId.List; import jakarta.validation.Constraint; import jakarta.validation.Payload; import jakarta.validation.ReportAsSingleViolation; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @Documented @Constraint(validatedBy = {}) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) @ReportAsSingleViolation @Size(min = 4, max = 20) @Pattern(regexp = "[a-z]*") public @interface UserId { String message() default "{com.example.sample.domain.validation.UserId.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { UserId[] value(); } }
「年齢」の制限を規定する
@Ageアノテーションの実装例package com.example.sample.domain.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.sample.domain.validation.Age.List; import jakarta.validation.Constraint; import jakarta.validation.Payload; import jakarta.validation.ReportAsSingleViolation; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @Documented @Constraint(validatedBy = {}) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) @ReportAsSingleViolation @Min(1) @Max(150) public @interface Age { String message() default "{com.example.sample.domain.validation.Age.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { Age[] value(); } }
Note
1つのアノテーションに複数のルールを設定した場合、それらのAND条件が複合ルールとなる。Hibernate Validatorでは、OR条件を実現するための
@ConstraintCompositionアノテーションが用意されている。詳細は、Hibernate Validatorのドキュメントを参照されたい。
4.2.3.2. 新規ルールを実装したBean Validationアノテーションの作成¶
jakarta.validation.ConstraintValidatorインタフェースを実装し、そのValidatorを使用するアノテーションを作成することで、任意のルールを作成することができる。
用途としては、以下の3通りが挙げられる。
既存のルールの組み合わせでは表現できないルール
相関項目チェックルール
業務ロジックチェック
4.2.3.2.1. 既存のルールの組み合わせでは表現できないルール¶
@Pattern、@Size、@Min、@Max等を組み合わせても表現できないルールは、jakarta.validation.ConstraintValidator実装クラスに記述する。
例として、IPv4形式のIPアドレス(Internet Protocol address)であることをチェックするルールを挙げる。
アノテーション
package com.example.common.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.common.validation.IPv4.List; import com.example.common.validation.IPv4Validator; import jakarta.validation.Constraint; import jakarta.validation.Payload; @Documented @Constraint(validatedBy = { IPv4Validator.class }) // (1) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) public @interface IPv4 { String message() default "{com.example.common.validation.IPv4.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { IPv4[] value(); } }
項番
説明
(1)このアノテーションを使用したときに実行されるConstraintValidatorを指定する。複数指定することができる。Validator
package com.example.common.validation; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public class IPv4Validator implements ConstraintValidator<IPv4, String> { // (1) @Override public void initialize(IPv4 constraintAnnotation) { // (2) } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // (3) if (value == null) { return true; // (4) } return isIPv4Valid(value); // (5) } // This logic check IPv4 address like 192.168.0.1 static boolean isIPv4Valid(String ipAddress) { String[] octets = ipAddress.split("\\."); if (octets.length != 4) { return false; } try { for (String octet: octets) { int intOctet = Integer.parseInt(octet); if (intOctet < 0 || 255 < intOctet) { return false; } } } catch (NumberFormatException e) { return false; } return true; } }
項番
説明
(1)ジェネリクスのパラメータに、対象のアノテーションとフィールドの型を指定する。(2)initializeメソッドに、初期化処理を実装する。(3)isValidメソッドで入力チェック処理を実装する。(4)入力値が、nullの場合は、正常とみなす。(5)IPv4形式のIPアドレスであることのチェックを行う。
Tip
ファイルアップロードのBean Validationの例も、ここに分類される。また共通ライブラリでは、この実装として@ExistInCodeListを用意している。
4.2.3.2.2. 相関項目チェックルール¶
以下では、「あるフィールドとその確認用フィールドの内容が一致すること」というルールを実現する例を挙げる。
Tip
共通ライブラリでは、2つのフィールドの内容を比較する相関項目チェックの実装として@Compareアノテーションを用意している。
@Compareアノテーションを利用することで、このルールをより簡単に実現することができる。 詳細は共通ライブラリのチェックルールの拡張方法を参照されたい。
ここでは、内容が一致しない場合には確認用フィールドにエラーメッセージを表示する。
アノテーション
相関項目チェック用のアノテーションはクラスレベルに付与できるようにする。
package com.example.common.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.common.validation.Confirm.List; import jakarta.validation.Constraint; import jakarta.validation.Payload; @Documented @Constraint(validatedBy = { ConfirmValidator.class }) @Target({ TYPE, ANNOTATION_TYPE, TYPE_USE }) // (1) @Retention(RUNTIME) @Repeatable(List.class) public @interface Confirm { String message() default "{com.example.common.validation.Confirm.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; /** * Field name */ String field(); // (2) /** * ConfirmField name */ String confirmField(); // (2) @Target({ TYPE, ANNOTATION_TYPE, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { Confirm[] value(); } }
項番
説明
(1)このアノテーションが、クラスまたはアノテーションにのみ付加できるように、対象を絞る。(2)アノテーションに渡すパラメータを定義する。Validator
package com.example.common.validation; import java.util.Objects; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public class ConfirmValidator implements ConstraintValidator<Confirm, Object> { private String field; private String confirmField; private String message; public void initialize(Confirm constraintAnnotation) { field = constraintAnnotation.field(); confirmField = constraintAnnotation.confirmField(); message = constraintAnnotation.message(); } public boolean isValid(Object value, ConstraintValidatorContext context) { BeanWrapper beanWrapper = new BeanWrapperImpl(value); // (1) Object fieldValue = beanWrapper.getPropertyValue(field); // (2) Object confirmFieldValue = beanWrapper.getPropertyValue(confirmField); boolean matched = Objects.equals(fieldValue, confirmFieldValue); if (matched) { return true; } else { context.disableDefaultConstraintViolation(); // (3) context.buildConstraintViolationWithTemplate(message) .addPropertyNode(confirmField).addConstraintViolation(); // (4) return false; } } }
項番
説明
(1)JavaBeanのプロパティにアクセスする際に便利なorg.springframework.beans.BeanWrapperを使用する。(2)BeanWrapper経由で、フォームオブジェクトからプロパティ値を取得する。(3)デフォルトのConstraintViolationオブジェクトの生成を無効にする。(4)独自ConstraintViolationオブジェクトを生成する。ConstraintValidatorContext.buildConstraintViolationWithTemplateで出力するメッセージを定義する。ConstraintViolationBuilder.addPropertyNodeでエラーメッセージを出力したいフィールド名を指定する。詳細は、ConstraintValidatorContextのJavaDocを参照されたい。
Note
Spring Validatorによる相関項目チェックにて紹介したように、Bean Validationにおいても相関チェック対象の複数フィールドに対してエラー情報を設定することが可能である。
以下に、Bean ValidationにてpasswordフィールドとconfirmPasswordフィールドにスタイルを適用し、confirmPasswordフィールドのみにエラーメッセージを表示する例を示す。
// omitted public class ConfirmValidator implements ConstraintValidator<Confirm, Object> { private String field; private String confirmField; private String message; public void initialize(Confirm constraintAnnotation) { // omitted } public boolean isValid(Object value, ConstraintValidatorContext context) { // omitted if (matched) { return true; } else { context.disableDefaultConstraintViolation(); //new ConstraintViolation to be generated for confirmField context.buildConstraintViolationWithTemplate(message) .addPropertyNode(confirmField).addConstraintViolation(); //new ConstraintViolation to be generated for field context.buildConstraintViolationWithTemplate("") // (1) .addPropertyNode(field).addConstraintViolation(); return false; } } }
項番
説明
(1)passwordフィールドのエラーを登録する。この際、エラーメッセージに空文字を設定している。
この@Confirmアノテーションを使用して、前述の「パスワードリセット」処理を再実装すると、以下のようになる。
フォームクラス
package com.example.sample.app.validation; import java.io.Serializable; import com.example.common.validation.Confirm; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @Confirm(field = "password", confirmField = "confirmPassword") // (1) public class PasswordResetForm implements Serializable { private static final long serialVersionUID = 1L; @NotNull @Size(min = 8) private String password; private String confirmPassword; // omitted geter/setter }
項番
説明
(1)クラスレベルに@Confirmアノテーションを付与する。これによりConstraintValidator.isValidの引数にはフォームオブジェクトが渡る。Controllerクラス
Validatorのインジェクションおよび
@InitBinderによるValidatorの追加は、不要になる。package com.example.sample.app.validation; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("password") public class PasswordResetController { @ModelAttribute public PasswordResetForm setupForm() { return new PasswordResetForm(); } @GetMapping(value = "reset", params = "form") public String resetForm() { return "password/resetForm"; } @PostMapping(value = "reset") public String reset(@Validated PasswordResetForm form, BindingResult result) { if (result.hasErrors()) { return "password/resetForm"; } return "redirect:/password/reset?complete"; } @GetMapping(value = "reset", params = "complete") public String resetComplete() { return "password/resetComplete"; } }
4.2.3.2.3. 業務ロジックチェック¶
ResultMessagesオブジェクトに格納することを推奨している。ConstraintValidator.isValidの結果に使用すればよい。「入力されたユーザー名が既に登録済みかどうか」をBean Validationで実現する例を示す。
Serviceクラス
実装クラス(UserServiceImpl)は割愛する。
package com.example.sample.domain.service.user; public interface UserService { boolean isUnusedUserId(String userId); // omitted other methods }
アノテーション
package com.example.sample.domain.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import com.example.sample.domain.validation.UnusedUserId.List; import jakarta.validation.Constraint; import jakarta.validation.Payload; @Documented @Constraint(validatedBy = { UnusedUserIdValidator.class }) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Repeatable(List.class) public @interface UnusedUserId { String message() default "{com.example.sample.domain.validation.UnusedUserId.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @interface List { UnusedUserId[] value(); } }
Validatorクラス
package com.example.sample.domain.validation; import com.example.sample.domain.service.user.UserService; import jakarta.inject.Inject; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @Component // (1) public class UnusedUserIdValidator implements ConstraintValidator<UnusedUserId, String> { @Inject // (2) UserService userService; @Override public void initialize(UnusedUserId constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } return userService.isUnusedUserId(value); // (3) } }
項番
説明
(1)Validatorクラスをコンポーネントスキャンの対象にする。パッケージがBean定義ファイルの<context:component-scan base-package="..." />の設定に含まれている必要がある。(2)呼び出すServiceクラスを、インジェクションする。(3)業務ロジックの結果を返却する。isValidメソッド名で業務ロジックを記述せず、かならずServiceに処理を委譲すること。
4.2.3.3. Method Validation¶
4.2.3.3.1. アプリケーションの設定¶
Spring Frameworkが提供するMethod Validationを使用する場合は、Spring Frameworkから提供されているorg.springframework.validation.beanvalidation.MethodValidationPostProcessorクラスをBean定義する必要がある。
MethodValidationPostProcessorを定義するBean定義ファイルは、Method Validationを使用する箇所によって異なる。
ここでは、本ガイドラインが推奨するマルチプロジェクト環境でMethod Validationを使用するための設定例を示す。
アプリケーション層用のプロジェクト(
projectName-web)ドメイン層用のプロジェクト(
projectName-domain)
の両プロジェクトの設定を変更する必要がある。
projectName-domain/src/main/xxx/yyy/zzz/config/app/ProjectNameDomainConfig.java// (1) @Bean("validator") public LocalValidatorFactoryBean validator() { return new LocalValidatorFactoryBean(); } @Bean public MethodValidationPostProcessor validationMethodValidationPostProcessor() { MethodValidationPostProcessor bean = new MethodValidationPostProcessor(); bean.setValidator(validator()); return bean; }
projectName-domain/src/main/xxx/yyy/zzz/config/web/SpringMvcConfig.java@EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Inject private LocalValidatorFactoryBean validator; // omitted // (3) @Override public Validator getValidator() { return validator; } // (4) @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor bean = new MethodValidationPostProcessor(); bean.setValidator(validator); return bean; }
項番 |
説明 |
|---|---|
(1)
|
|
(2)
|
|
(3)
|
この設定がない場合は(1)で作成したものとは異なる |
(4)
|
|
projectName-domain/src/main/resources/META-INF/spring/projectName-domain.xml<!-- (1) --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/> <!-- (2) --> <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator" /> </bean>
projectName-web/src/main/resources/META-INF/spring/spring-mvc.xml<!-- (3) --> <mvc:annotation-driven validator="validator"> <!-- ... --> </mvc:annotation-driven> <!-- (4) --> <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator" /> </bean>
項番 |
説明 |
|---|---|
(1)
|
|
(2)
|
|
(3)
|
この設定がない場合は(1)で作成したものとは異なる |
(4)
|
|
Tip
LocalValidatorFactoryBeanは、Bean Validation(Hibernate Validator)が提供するValidatorクラスとSpring Frameworkを連携するためのラッパーValidatorオブジェクトを生成するためのクラスである。
このクラスによって生成されたラッパーValidatorを使用することで、Spring Frameworkが提供するメッセージ管理機能(MessageSource)やDIコンテナなどとの連携が行えるようになる。
Tip
Spring Frameworkでは、DIコンテナで管理されているBeanのメソッド呼び出しに対するMethod Validationの実行を、AOPの仕組みを利用して行っている。
MethodValidationPostProcessorは、Method Validationを実行するためのAOPを適用するためのクラスである。
Note
上記例では、各Beanのvalidatorプロパティに対して、同じValidatorオブジェクト(インスタンス)を設定しているが、これは必ずしも必須ではない。ただし、特に理由がない場合は、同じオブジェクト(インスタンス)を設定しておくことを推奨する。
4.2.3.3.2. Method Validation対象のメソッドにするための定義方法¶
メソッドにMethod Validationを適用するには、対象のメソッドを含むことを示したアノテーションをクラスレベルに、Bean Validationの制約アノテーションをメソッドと仮引数にそれぞれ指定する必要がある。
「アプリケーションの設定」を行っただけでは、Method Validationを実行するAOPは適用されない。Method Validationを実行するAOPを適用するためには、インタフェース又はクラスに@ org.springframework.validation.annotation.Validatedアノテーションを付与する必要がある。
ここでは、インタフェースに対してアノテーションを指定する方法を紹介する。
package com.example.domain.service;
import org.springframework.validation.annotation.Validated;
@Validated // (1)
public interface HelloService {
// ...
}
項番 |
説明 |
|---|---|
(1)
|
Method Validationの対象となるインタフェースに、 上記例では、 |
Tip
@Validatedアノテーションのvalue属性にグループインタフェースを指定することで、 指定したグループに属するValidationのみ実行する事も可能である。
また、メソッドレベルにValidatedアノテーションを付与することで、メソッド毎にバリデーショングループを切り替える事も可能な仕組みとなっている。
バリデーショングループについては、「バリデーションのグループ化」を参照されたい。
メソッドの引数
メソッドの引数に指定されたJavaBeanのフィールド
に対してBean Validationの制約アノテーションを、
メソッドの返り値
メソッドの返り値として返却するJavaBeanのフィールド
に対してBean Validationの制約アノテーションを指定する。
package com.example.domain.service;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotEmpty;
@Validated
public interface HelloService {
// (2)
@NotEmpty
String hello(@NotEmpty /* (1) */ String message);
}
項番 |
説明 |
|---|---|
(1)
|
Bean Validationの制約アノテーションをメソッドの引数アノテーションとして指定する。 @NotEmptyはmessageという引数がNull値と空文字を許可しないことを意味する制約である。引数にNull値もしくは空文字が指定された場合、
jakarta.validation.ConstraintViolationExceptionが発生する。 |
(2)
|
Bean Validationの制約アノテーションをメソッドアノテーションとして指定する。 上記例では、返り値がNull値もしくは空文字にならないことを示しており、返り値としてNull値もしくは空文字が返却された場合、 |
次に、メソッドのシグネチャとしてJavaBeanを使用するメソッドに対して、Bean Validationの制約アノテーションを指定する方法について説明する。
ここでは、インタフェースに対してアノテーションを指定する方法を紹介する。
Serviceインタフェース
package com.example.domain.service;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotNull;
@Validated
public interface HelloService {
@NotNull // (3)
@Valid // (4)
HelloOutput hello(@NotNull /* (1) */ @Valid /* (2) */ HelloInput input);
}
項番 |
説明 |
|---|---|
(1)
|
Bean Validationの制約アノテーションをメソッドの引数アノテーションとして指定する。
|
(2)
|
@Validアノテーションを付与する事で、引数のJavaBeanのフィールドに指定したBean Validationの制約アノテーションが有効となる。JavaBeanに指定された制約を満たさない場合は
jakarta.validation.ConstraintViolationExceptionが発生する。 |
(3)
|
Bean Validationの制約アノテーションをメソッドアノテーションとして指定する。 返り値のJavaBeanがNull値にならないことを示しており、返り値としてNull値が返却された場合は例外が発生する。 |
(4)
|
@Validアノテーションを付与する事で、返り値のJavaBeanのフィールドに指定したBean Validationの制約アノテーションが有効となる。JavaBeanに指定された制約を満たさない場合は
jakarta.validation.ConstraintViolationExceptionが発生する。 |
Input用のJavaBean
package com.example.domain.service;
import java.util.Date;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
public class HelloInput {
@NotNull
@Past
private Date visitDate;
@NotEmpty
private String visitMessage;
private String userId;
// ...
}
Output用のJavaBean
package com.example.domain.service;
import java.util.Date;
import com.example.domain.model.User;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
public class HelloOutput {
@NotNull
@Past
private Date acceptDate;
@NotEmpty
private String acceptMessage;
@Valid // (5)
private User user;
// ...
}
Output用のJavaBean内でネストしているJavaBean
package com.example.domain.model;
import java.util.Date;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Past;
public class User {
@NotEmpty
private String userId;
@NotEmpty
private String userName;
@Past
private Date dateOfBirth;
// ...
}
項番 |
説明 |
|---|---|
(5)
|
ネストしたJavaBeanに指定しているBean Validationの制約アノテーションを有効にする場合は、
@Validアノテーションを付与する事で、ネストしたJavaBeanのフィールドに指定したBean Validationの制約アノテーションが有効となる。ネストしたJavaBeanに指定された制約を満たさない場合は
jakarta.validation.ConstraintViolationExceptionが発生する。 |
4.2.3.3.3. 制約違反時の例外ハンドリング¶
制約に違反した場合、jakarta.validation.ConstraintViolationExceptionが発生する。
ConstraintViolationExceptionが発生した場合、スタックトレースから発生したメソッドは特定できるが、具体的な違反内容が特定できない。
違反内容を特定するためには、ConstraintViolationExceptionをハンドリングしてログ出力を行う例外ハンドリングクラスを作成するとよい。
以下の例外ハンドリングクラスの作成例を示す。
package com.example.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import jakarta.validation.ConstraintViolationException;
@ControllerAdvice
public class ConstraintViolationExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(ConstraintViolationExceptionHandler.class);
// (1)
@ExceptionHandler
public String handleConstraintViolationException(ConstraintViolationException e){
// (2)
if (logger.isErrorEnabled()) {
logger.error("ConstraintViolations[\n{}\n]", e.getConstraintViolations());
}
return "common/error/systemError";
}
}
項番 |
説明 |
|---|---|
(1)
|
メソッドの引数として、 |
(2)
|
メソッドの引数で受け取った |
Note
@ControllerAdviceアノテーションの詳細については「@ControllerAdviceの実装」を参照されたい。
Warning
ConstraintViolation#getMessageメソッドを使用することでエラーメッセージを取得することができるが、Springの機能によるメッセージ補完は行われないため、エラーメッセージに {0}でフィールド名を埋め込むことはできない。
代わりに、フィールド名はConstraintViolation#getPropertyPathメソッドで取得することが可能である。
Springの機能によるメッセージ補完については、ValidationMessages.propertiesに定義するメッセージのNoteを参照されたい。
ConstraintViolationの詳細については、Hibernate Validatorのリファレンスを参照されたい。
4.2.4. Appendix¶
4.2.4.1. Hibernate Validatorが用意する入力チェックルール¶
4.2.4.1.1. Bean Validationのチェックルール¶
Bean Validationの標準アノテーション(jakarta.validation.*)を以下に示す。
詳細は、Bean Validation specification(Built-in Constraint definitions)を参照されたい。
アノテーション |
対象の型 |
説明 |
使用例 |
|---|---|---|---|
|
任意 |
対象のフィールドが、 |
@NotNull
private String id;
|
|
|
null、または空文字でないことを検証する。Stringの入力必須チェックについては
@NotNullではなくこちらを使用することを推奨する。(2.0から追加) |
@NotEmpty
private String password;
|
|
任意の |
null、空文字("")、空白のみでないことを検証する。(2.0から追加) |
@NotBlank
private String userId;
|
|
任意 |
対象のフィールドが、
nullであることを検証する。(例:グループ検証での使用)
|
@Null(groups={Update.class})
private String id;
|
|
|
対象のフィールドが正規表現にマッチするかどうか
(Hibernate Validator実装では、任意の
CharSequenceインタフェースの実装クラスにも適用可能) |
@Pattern(regexp = "[0-9]+")
private String tel;
|
|
BigDecimal, BigInteger, byte, short, int, longおよびラッパー(Hibernate Validator実装では、任意の
Numberの継承クラス,CharSequenceインタフェースの実装クラスにも適用可能。ただし、文字列が数値表現の場合に限る。) |
値が、最小値以上であるかどうかを検証する。 |
@Max参照 |
|
BigDecimal, BigInteger, byte, short, int, longおよびラッパー(Hibernate Validator実装では任意の
Numberの継承クラス,CharSequenceインタフェースの実装クラスにも適用可能。ただし、文字列が数値表現の場合に限る。) |
値が、最大値以下であるかどうかを検証する。 |
@Min(1)
@Max(100)
private int quantity;
|
|
|
Decimal型の値が、最小値以上であるかどうかを検証する。inclusive = falseを指定する事で、最小値より大きいかどうかを検証するように動作を変更する事ができる。 |
|
|
|
Decimal型の値が、最大値以下であるかどうかを検証する。inclusive = falseを指定する事で、最大値より小さいかどうかを検証するように動作を変更する事ができる。 |
@DecimalMin("0.0")
@DecimalMax("99999.99")
private BigDecimal price;
|
|
|
値が正の数値(0を含まない)であるかどうかを検証する。(2.0から追加)
|
@Positive
private int deposit;
|
|
|
値が正の数値(0を含む)であるかどうかを検証する。(2.0から追加)
|
@PositiveOrZero
private int deposit;
|
|
|
値が負の数値(0を含まない)であるかどうかを検証する。(2.0から追加)
|
@Negative
private int deposit;
|
|
|
値が正の数値(0を含む)であるかどうかを検証する。(2.0から追加)
|
@NegativeOrZero
private int deposit;
|
|
|
要素の長さ(要素のサイズ)が
minとmaxの間のサイズか検証する。minとmaxは省略可能であるが、デフォルトはmin=0,max= Integer.MAX_VALUEとなる。 |
@Size(min=4, max=64)
private String password;
|
|
|
値が指定された範囲内の数値であるかチェックする。
integerに最大整数の桁を指定し、fractionに最大小数桁を指定する。 |
@Digits(integer=6, fraction=2)
private BigDecimal price;
|
|
|
対象のフィールドが |
@AssertTrue
private boolean checked;
|
|
|
対象のフィールドが |
@AssertFalse
private boolean checked;
|
|
|
未来であるか検証する。
Dateのように日時を持つ型では未来日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では未来日付であるか検証する。 |
@Future
private Date eventDate;
|
|
|
現在または未来であるか検証する。(2.0から追加)
Dateのように日時を持つ型では未来日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では未来日付であるか検証する。 |
@FutureOrPresent
private Date eventDate;
|
|
|
過去であるか検証する。
Dateのように日時を持つ型では過去日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では過去日付であるか検証する。 |
@Past
private Date eventDate;
|
|
|
現在または過去であるか検証する。(2.0から追加)
Dateのように日時を持つ型では過去日時であるか検証し、java.time.LocalDateのように日付のみ持つ型では過去日付であるか検証する。 |
@PastOrPresent
private Date eventDate;
|
|
任意の |
E-mailアドレスとして妥当であること検証する。(2.0から追加)
|
@Email
private String email;
|
|
任意の非プリミティブ型 |
関連付けられているオブジェクトについて、再帰的に検証を行う。 |
@Valid
private List<Employer> employers;
@Valid
private Dept dept;
|
Warning
@Sizeアノテーションでは、サロゲートペアと呼ばれるchar型2つ(32ビット)で表される文字に対する考慮がされていない。
サロゲートペアを含む文字列をチェック対象とした場合、カウントした文字数が実際の入力文字数より多くカウントされる可能性があるため注意すること。
サロゲートペアを含む文字列の文字列長については、文字列長の取得を参照されたい。
Warning
E-mailの形式はRFC2822で定義されているが、@Emailは厳密にRFC2822に準拠していることをチェックするものではない。
例えばマルチバイト文字(全角文字)を含んでいても@Emailでのチェックをパスすることが確認されている。また、実際に利用されているEmailアドレスも、必ずしもRFC2822に厳密に準拠しているわけではない。
これらの注意点を考慮した上で、利用・サポートするSMTPサーバなどによって適切なルールでの入力チェックを実装することを推奨する。実装の際は、既存ルールを組み合わせたBean Validationアノテーションの作成を参照されたい。
Warning
@Past、@Future、@PastOrPresent、@FutureOrPresentアノテーションでは検証をcompareToメソッドで行っており、検証対象の型により日付のみ検証するか日時を検証するかが異なる。
このため、日付項目にDate型を利用していると以下のような事象が発生する可能性がある。
現在日付を入力しているにも関わらず
@Pastでのチェックをパスしてしまう。現在日付を入力しているにも関わらず
@FutureOrPresentでのチェックをパスできない。
日付時刻の検証を行う場合は適切な型を利用するよう留意されたい。
Note
Bean Validationが提供するClockProviderを実装することで、@Past、@Future、@PastOrPresent、@FutureOrPresentの基準となる日付を変更することが出来る。
なお、実装したClockProviderを適用するには、LocalValidatorFactoryBeanの継承クラスを作成し、postProcessConfigurationメソッドをオーバーライドすれば良い。
ClockProviderを実装したクラスの例に関しては、Hibernate Validator Reference Guide(ClockProvider and temporal validation tolerance)を参照されたい。
4.2.4.1.2. Hibernate Validatorのチェックルール¶
Hibernate Validatorの代表的なアノテーション(org.hibernate.validator.constraints.*)を以下に示す。
詳細は、Hibernate Validator仕様を参照されたい。
アノテーション |
対象の型 |
説明 |
使用例 |
|---|---|---|---|
|
任意の |
Luhnアルゴリズムでクレジットカード番号が妥当かどうかを検証する。使用可能な番号かどうかをチェックするわけではない。
ignoreNonDigitCharacters = trueを指定する事で、数字以外の文字を無視して検証する事ができる。 |
@CreditCardNumber
private String cardNumber;
|
|
任意の |
ISBNの形式として妥当であること(番号の長さとチェックディジット)を検証する。
typeを指定する事で、ISBNの形式(ISBN-10とISBN-13)の選択が出来る。デフォルトではISBN-13となる。検証時には、ISBN以外のすべての文字(0-9までの数字とX以外の文字)は無視される。
このため、番号の一部を”
-“を利用して区切ることが出来る。(例:978-161-729-045-9) |
@ISBN
private String bookNumber;
|
|
任意の |
URLとして妥当であること検証する。 |
@URL
private String url;
|
Warning
従来、Hibernate Validatorの独自アノテーションであった@Email、@NotBlank、@NotEmptyは、Bean Validation 2.0よりデフォルトで提供されるようになった。これに伴い、Hibernate Validator 6.0より、Hibernate Validatorが提供する@Email、@NotBlank、@NotEmptyは非推奨となった。引き続き使用することは出来るが、Bean Validationで提供されるアノテーションを使用することを推奨する。
Tip
@URLにて、JVMがサポートしていないプロトコルについても妥当として検証したい場合、Hibernateから提供されているorg.hibernate.validator.constraintvalidators.RegexpURLValidatorを使用する。当該クラスは@URLアノテーションに対応するValidatorクラスで、URL形式であるかを正規表現で検証しており、JVMがサポートしていないプロトコルについても妥当として検証可能である。
アプリケーション全体の
@URLのチェックルールを変更してもよい場合には、JavaDocに記載されているように、XMLにてValidatorクラスをRegexpURLValidatorに変更する。一部の項目だけに正規表現による検証を適用し、
@URLはデフォルトのルールを使用したい場合には、新規アノテーション、およびRegexpURLValidatorと同様の検証を行うjakarta.validation.ConstraintValidator実装クラスを作成し、必要な項目に作成したアノテーションによる検証を適用する。
など、用途に応じた適用を行えばよい。
XMLによるチェックルール変更の詳細についてはHibernateのリファレンスを、新規アノテーションの作成方法については、新規ルールを実装したBean Validationアノテーションの作成をそれぞれ参照されたい。
4.2.4.1.3. Hibernate Validatorが用意するデフォルトメッセージ¶
hibernate-validator-<version>.jar内のorg/hibernate/validatorに、ValidationMessages.propertiesのデフォルト値が定義されている。
jakarta.validation.constraints.AssertFalse.message = must be false
jakarta.validation.constraints.AssertTrue.message = must be true
jakarta.validation.constraints.DecimalMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
jakarta.validation.constraints.DecimalMin.message = must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}
jakarta.validation.constraints.Digits.message = numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
jakarta.validation.constraints.Email.message = must be a well-formed email address
jakarta.validation.constraints.Future.message = must be a future date
jakarta.validation.constraints.FutureOrPresent.message = must be a date in the present or in the future
jakarta.validation.constraints.Max.message = must be less than or equal to {value}
jakarta.validation.constraints.Min.message = must be greater than or equal to {value}
jakarta.validation.constraints.Negative.message = must be less than 0
jakarta.validation.constraints.NegativeOrZero.message = must be less than or equal to 0
jakarta.validation.constraints.NotBlank.message = must not be blank
jakarta.validation.constraints.NotEmpty.message = must not be empty
jakarta.validation.constraints.NotNull.message = must not be null
jakarta.validation.constraints.Null.message = must be null
jakarta.validation.constraints.Past.message = must be a past date
jakarta.validation.constraints.PastOrPresent.message = must be a date in the past or in the present
jakarta.validation.constraints.Pattern.message = must match "{regexp}"
jakarta.validation.constraints.Positive.message = must be greater than 0
jakarta.validation.constraints.PositiveOrZero.message = must be greater than or equal to 0
jakarta.validation.constraints.Size.message = size must be between {min} and {max}
org.hibernate.validator.constraints.CreditCardNumber.message = invalid credit card number
org.hibernate.validator.constraints.Currency.message = invalid currency (must be one of {value})
org.hibernate.validator.constraints.EAN.message = invalid {type} barcode
org.hibernate.validator.constraints.Email.message = not a well-formed email address
org.hibernate.validator.constraints.ISBN.message = invalid ISBN
org.hibernate.validator.constraints.Length.message = length must be between {min} and {max}
org.hibernate.validator.constraints.CodePointLength.message = length must be between {min} and {max}
org.hibernate.validator.constraints.LuhnCheck.message = the check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod10Check.message = the check digit for ${validatedValue} is invalid, Modulo 10 checksum failed
org.hibernate.validator.constraints.Mod11Check.message = the check digit for ${validatedValue} is invalid, Modulo 11 checksum failed
org.hibernate.validator.constraints.ModCheck.message = the check digit for ${validatedValue} is invalid, {modType} checksum failed
org.hibernate.validator.constraints.NotBlank.message = may not be empty
org.hibernate.validator.constraints.NotEmpty.message = may not be empty
org.hibernate.validator.constraints.ParametersScriptAssert.message = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.Range.message = must be between {min} and {max}
org.hibernate.validator.constraints.SafeHtml.message = may have unsafe html content
org.hibernate.validator.constraints.ScriptAssert.message = script expression "{script}" didn't evaluate to true
org.hibernate.validator.constraints.UniqueElements.message = must only contain unique elements
org.hibernate.validator.constraints.URL.message = must be a valid URL
org.hibernate.validator.constraints.br.CNPJ.message = invalid Brazilian corporate taxpayer registry number (CNPJ)
org.hibernate.validator.constraints.br.CPF.message = invalid Brazilian individual taxpayer registry number (CPF)
org.hibernate.validator.constraints.br.TituloEleitoral.message = invalid Brazilian Voter ID card number
org.hibernate.validator.constraints.pl.REGON.message = invalid Polish Taxpayer Identification Number (REGON)
org.hibernate.validator.constraints.pl.NIP.message = invalid VAT Identification Number (NIP)
org.hibernate.validator.constraints.pl.PESEL.message = invalid Polish National Identification Number (PESEL)
org.hibernate.validator.constraints.time.DurationMax.message = must be shorter than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
org.hibernate.validator.constraints.time.DurationMin.message = must be longer than${inclusive == true ? ' or equal to' : ''}${days == 0 ? '' : days == 1 ? ' 1 day' : ' ' += days += ' days'}${hours == 0 ? '' : hours == 1 ? ' 1 hour' : ' ' += hours += ' hours'}${minutes == 0 ? '' : minutes == 1 ? ' 1 minute' : ' ' += minutes += ' minutes'}${seconds == 0 ? '' : seconds == 1 ? ' 1 second' : ' ' += seconds += ' seconds'}${millis == 0 ? '' : millis == 1 ? ' 1 milli' : ' ' += millis += ' millis'}${nanos == 0 ? '' : nanos == 1 ? ' 1 nano' : ' ' += nanos += ' nanos'}
Note
Hibernate Validator 6.1.0より日本語メッセージに対応している。
4.2.4.2. 共通ライブラリが用意する入力チェックルール¶
共通ライブラリでは、独自の検証用アノテーションを提供している。ここでは、共通ライブラリで提供しているアノテーションを使用した入力チェックルールの指定方法について説明する。
4.2.4.2.1. terasoluna-gfw-commonのチェックルール¶
terasoluna-gfw-commonが提供するアノテーション(org.terasoluna.gfw.common.codelist.*)を以下に示す。
アノテーション |
対象の型 |
説明 |
使用例 |
|---|---|---|---|
|
CharacterCharSequenceの実装クラス(
String, StringBuilderなど)Numberの継承クラス(
Integer, Longなど) |
値がコードリストに含まれているかどうかを検証する。 |
4.2.4.2.2. terasoluna-gfw-codepointsのチェックルール¶
terasoluna-gfw-codepointsが提供するアノテーション(org.terasoluna.gfw.common.codepoints.*)を以下に示す。なお、terasoluna-gfw-codepointsはバージョン5.1.0.RELEASE以上で利用することができる。
アノテーション |
対象の型 |
説明 |
使用例 |
|---|---|---|---|
|
CharSequenceの実装クラス(
String, StringBuilderなど) |
チェック対象の文字列が指定したコードポイント集合に全て含まれるかどうかを検証する。 |
4.2.4.2.3. terasoluna-gfw-validatorのチェックルール¶
terasoluna-gfw-validatorが提供するアノテーション(org.terasoluna.gfw.common.validator.constraints.*)を以下に示す。なお、terasoluna-gfw-validatorはバージョン5.1.0.RELEASE以上で利用することができる。
アノテーション |
対象の型 |
説明 |
使用例 |
|---|---|---|---|
|
CharSequenceの実装クラス(
String, StringBuilderなど) |
値のバイト長が最小値以上であることを検証する。
[アノテーションの属性]
long value - バイト長の最小値を指定する。String charset - 値をバイトシーケンスに符号化する際に使用する文字セットを指定する。デフォルト値はUTF-8。 |
@ByteMin(value = 1, charset = "Shift_JIS")
private String id;
|
|
CharSequenceの実装クラス(
String, StringBuilderなど) |
値のバイト長が最大値以下であることを検証する。
[アノテーションの属性]
long value - バイト長の最大値を指定する。String charset - 値をバイトシーケンスに符号化する際に使用する文字セットを指定する。デフォルト値はUTF-8。 |
@ByteMax(100)
private String id;
|
|
CharSequenceの実装クラス(
String, StringBuilderなど) |
値のバイト長が最小値と最大値の範囲内であることを検証する。(5.4.2から追加)
@ByteMinと @ByteMaxを組み合わせて使う場合は、こちらを使うことを推奨する。[アノテーションの属性]
long min - バイト長の最小値を指定する。デフォルト値は0。long max - バイト長の最大値を指定する。デフォルト値はLong.MAX_VALUE。String charset - 値をバイトシーケンスに符号化する際に使用する文字セットを指定する。デフォルト値はUTF-8。 |
@ByteSize(min = 1, max = 100)
private String id;
|
|
Comparableインタフェースの実装クラスをプロパティにもつ任意のJavaBeanに適用可能 |
指定したプロパティの値の比較結果が正しいことを検証する。
[アノテーションの属性]
String left - オブジェクト内の比較元としたいプロパティ名を指定する。検証エラーとなった場合は、このプロパティにメッセージを表示される。String right - オブジェクト内の比較先としたいプロパティ名を指定する。org.terasoluna.gfw.common.validator.constraints.Compare.Operator operator - 比較方法を示す列挙型Operatorの値を指定する。指定可能な値は以下の通り。
NOT_EQUALは、terasoluna-gfw-validator 5.3.2.RELEASE以上で利用可能な値である。boolean requireBoth - left属性とright属性で指定したフィールドの両方が入力されている(nullでない)必要があるかどうかを指定する。
org.terasoluna.gfw.common.validator.constraints.Compare.Node node - エラーメッセージを出力するパスを示す列挙型Nodeの値を指定する。指定可能な値は以下の通り。
|
メールアドレスと確認用に入力したメールアドレスが一致することをチェックし、フォーム全体のエラーメッセージとして表示する場合、以下のように実装する。 @Compare(left = "email",
right = "confirmEmail",
operator = Compare.Operator.EQUAL,
requireBoth = true,
node = Compare.Node.ROOT_BEAN)
public class UserRegisterForm {
private String email;
private String confirmEmail;
}
期間の開始日と終了日が両方入力された場合は、開始日が終了日以前であることをチェックし、期間の開始日にエラーメッセージを表示する場合、以下のように実装する。 @Compare(left = "form",
right = "to",
operator = Compare.Operator.LESS_THAN_OR_EQUAL)
public class Period {
private Date from;
private Date to;
}
|
Note
相関項目チェックにおける入力必須について
単項目チェックにおいては、入力フィールドが入力されている( nullでない)かどうかは @NotNullを併用してチェックすればよい。しかし、相関項目チェックにおいては、「どちらか一方でも入力した場合は、もう一方の入力を強制する」といった、 @NotNullの併用だけでは実現できない場合がある。このため、@Compareでは、チェック対象の入力必須を制御する requireBoth属性を提供しており、これを併用して要件に応じたチェックを実装することができる。
なお、入力フィールドが未入力の場合に nullがバインドされる場合のみ、 requireBoth属性が利用できる。Spring MVCでは文字列の入力フィールドに未入力の状態でフォームを送信した場合、デフォルトでは、フォームオブジェクトにnullではなく、空文字がバインドされることに注意しなければならない。 文字列フィールドが未入力の場合に、空文字ではなく、nullをフォームオブジェクトにバインドするには、文字列フィールドが未入力の場合にnullをバインドするを参照されたい。
期間の開始日が終了日以前であることのチェックを例に、想定されるチェック要件と設定の例を以下に示す。
チェック要件
設定例
fromとtoがともに必須で、fromとtoの比較チェックを行う。
fromとtoに@NotNullを付与し、requireBoth属性はデフォルト値(false)を使用する。@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL) public class Period { @NotNull LocalDate from; @NotNull LocalDate to; }
fromだけ必須だが、toも入力された時は比較チェックする。
fromにだけ@NotNullを付与し、requireBoth属性はデフォルト値(false)を使用する。@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL) public class Period { @NotNull LocalDate from; LocalDate to; }
fromとtoがともに必須ではなく、fromとtoが両方入力された時だけ比較チェックする。どちらか一方だけが入力された場合は比較チェックを行わない。
@NotNullは付与せず、requireBoth属性はデフォルト値(false)を使用する。@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL) public class Period { LocalDate from; LocalDate to; }
fromとtoがともに必須ではないが、fromかtoのどちら一方でも入力した場合は、必ず両方入力して比較チェックを行う。
@NotNullは付与せず、requireBoth属性にtrueを設定する。@Compare(left = "from", right = "to", operator = Compare.Operator.LESS_THAN_OR_EQUAL, requireBoth = true) public class Period { LocalDate from; LocalDate to; }
4.2.4.2.4. 共通ライブラリが用意するデフォルトメッセージ¶
共通ライブラリの各Jar内のContributorValidationMessages.propertiesファイルに、ValidationMessages.propertiesのデフォルト値が定義されている。
# terasoluna-gfw-common
org.terasoluna.gfw.common.codelist.ExistInCodeList.message = Does not exist in {codeListId}
# terasoluna-gfw-codepoints
org.terasoluna.gfw.common.codepoints.ConsistOf.message = not consist of specified code points
# terasoluna-gfw-validator
org.terasoluna.gfw.common.validator.constraints.ByteMin.message = must be greater than or equal to {value} bytes
org.terasoluna.gfw.common.validator.constraints.ByteMax.message = must be less than or equal to {value} bytes
org.terasoluna.gfw.common.validator.constraints.ByteSize.message = must be between {min} and {max} bytes
org.terasoluna.gfw.common.validator.constraints.Compare.message = invalid combination of {left} and {right}
Note
共通ライブラリでは、共通ライブラリの各Jar内のContributorValidationMessages.propertiesファイルで提供している。
ContributorValidationMessages.propertiesファイルはHibernate Validatorのメッセージ定義ファイルである。
他のBean Validation実装ライブラリを利用する場合はデフォルトメッセージが適用されないことに注意されたい。
4.2.4.2.5. 共通ライブラリのチェックルールの適用方法¶
以下の手順で、共通ライブラリのチェックルールを適用する。
terasoluna-gfw-validatorを追加する例を以下に示す。terasoluna-gfw-commonはブランクプロジェクトのデフォルト設定で使用可能であり、依存ライブラリを追加する必要はない。<dependencies>
<dependency>
<groupId>org.terasoluna.gfw</groupId>
<artifactId>terasoluna-gfw-validator</artifactId>
</dependency>
</dependencies>
Note
上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。
あとは基本的な単項目チェックで説明したように、JavaBeanのプロパティにアノテーションを付与すればよい。
Note
Bean Validationでは、アノテーションの属性値の不正により検証が実行できない場合、jakarta.validation.ValidationExceptionがスローされる。スタックトレースに出力される原因を参照し、属性値を適切な値に修正すること。
詳細は、Bean Validation specification(Exception model)を参照されたい。
4.2.4.2.6. 共通ライブラリのチェックルールの拡張方法¶
共通ライブラリで提供しているチェックルールを利用して、任意のルールを作成することができる。
以下では、相関項目チェックルールで独自に実装した@Confirmアノテーションを、共通ライブラリで提供しているチェックルールを利用して作成する例を紹介する。
既存ルールを組み合わせたBean Validationアノテーションの作成で説明したように、@Compareを利用して@Confirmアノテーションを作成する。
package com.example.sample.domain.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.terasoluna.gfw.common.validator.constraints.Compare;
import com.example.sample.domain.validation.Confirm.List;
import jakarta.validation.Constraint;
import jakarta.validation.OverridesAttribute;
import jakarta.validation.Payload;
@Documented
@Constraint(validatedBy = {})
@Target({ TYPE, ANNOTATION_TYPE, TYPE_USE }) // (1)
@Retention(RUNTIME)
@Repeatable(List.class)
@Compare(left = "", right = "", operator = Compare.Operator.EQUAL, requireBoth = true) // (2)
public @interface Confirm {
@OverridesAttribute(constraint = Compare.class, name = "message") // (3)
String message() default "{com.example.sample.domain.validation.Confirm.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@OverridesAttribute(constraint = Compare.class, name = "right") // (4)
String field();
@OverridesAttribute(constraint = Compare.class, name = "left") // (5)
String confirmField();
@Documented
@Target({ TYPE, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@interface List {
Confirm[] value();
}
}
項番 |
説明 |
|---|---|
(1)
|
このアノテーションを付与できる場所を、クラスまたはアノテーションに限定する。
|
(2)
|
@Compareアノテーションのoperator属性にCompare.Operator.EQUAL(同値であること)を指定する。どちらか一方が未入力の場合はエラーとするため、requireBoth属性にtrueを指定する。 |
(3)
|
@Compareアノテーションのmessage属性をオーバーライドし、エラー時にmessage属性に指定したメッセージが使用されるようにする。 |
(4)
|
@Compareアノテーションのright属性をオーバーライドし、属性名をfieldに変更する。 |
(5)
|
同様に
left属性をオーバーライドし、属性名をconfirmFieldに変更する。 |
Note
「既存ルールを組み合わせたBean Validationアノテーションの作成」では@ReportAsSingleViolationを付与する方法を紹介しているが、@ReportAsSingleViolationを付与するとラップされた@Compareのエラーメッセージは使用されず、@Confirmのエラーメッセージのみが表示される。@Confirmはフォームオブジェクトに対する入力チェックであるため、エラーメッセージはフォームオブジェクトに割り当てられ、実際に表示したいfield属性に指定したフィールドには割り当てられない。
これを回避するためには、@ReportAsSingleViolationを付与せず、@Confirmのmessage属性で@Compareのmessage属性をオーバーライドする必要がある。これにより、@Compareのルールに従いleft属性(つまり@Confirmのfield属性)に@Confirmのエラーメッセージを割り当てることができるようになる。
相関項目チェックルールで実装したアノテーションの代わりに、上記で作成したアノテーションを使用する。
package com.example.sample.app.validation;
import java.io.Serializable;
import com.example.sample.domain.validation.Confirm;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Confirm(field = "password", confirmField = "confirmPassword") // (1)
public class PasswordResetForm implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull // (2)
@Size(min = 8)
private String password;
@NotEmpty // (3)
private String confirmPassword;
// omitted geter/setter
}
項番 |
説明 |
|---|---|
(1)
|
クラスレベルに
@Confirmアノテーションを付与する。 |
(2)
|
passwordフィールドがnullの場合は@Confirmの検証はパスするため、nullチェックは@NotNullアノテーションを付与して行う。 |
(3)
|
confirmPasswordフィールドには、@NotEmptyアノテーションを付与する。 |
4.2.4.3. 型のミスマッチ¶
フォームオブジェクトのString以外のフィールドに対して、変換不可能な値を送信した場合はorg.springframework.beans.TypeMismatchExceptionがスローされる。
「新規ユーザー登録」処理の例では「Age」フィールドはIntegerで定義されているが、このフィールドに対して整数に変換できない値を入力すると、以下のようなエラーメッセージが表示される。
例外の原因がそのまま表示されてしまい、エラーメッセージとしては不適切である。型がミスマッチの場合のエラーメッセージは、org.springframework.context.MessageSourceが読み込むpropertiesファイル(application-messages.properties)に定義できる。
以下のルールで、エラーメッセージを定義すればよい。
メッセージキー |
メッセージ内容 |
用途 |
|---|---|---|
|
型ミスマッチエラーのデフォルトメッセージ |
システム全体のデフォルト値 |
|
特定の型ミスマッチエラーのデフォルトメッセージ |
システム全体のデフォルト値 |
|
特定のフォームのフィールドに対する型ミスマッチエラーのメッセージ |
画面毎に変更したいメッセージ |
application-messages.propertiesに以下の定義を行った場合、
# typemismatch
typeMismatch="{0}" is invalid.
typeMismatch.int="{0}" must be an integer.
typeMismatch.double="{0}" must be a double.
typeMismatch.float="{0}" must be a float.
typeMismatch.long="{0}" must be a long.
typeMismatch.short="{0}" must be a short.
typeMismatch.java.lang.Integer="{0}" must be an integer.
typeMismatch.java.lang.Double="{0}" must be a double.
typeMismatch.java.lang.Float="{0}" must be a float.
typeMismatch.java.lang.Long="{0}" must be a long.
typeMismatch.java.lang.Short="{0}" must be a short.
typeMismatch.java.util.Date="{0}" is not a date.
# filed names
name=Name
email=Email
age=Age
エラーメッセージは、次のように変更される。
{0}でフィールド名を埋めることができる。Tip
メッセージキーのルールの詳細は、DefaultMessageCodesResolverのJavadocを参照されたい。
4.2.4.4. 文字列フィールドが未入力の場合にnullをバインドする¶
これまで説明してきたように、Spring MVCでは文字列の入力フィールドに未入力の状態でフォームを送信した場合、デフォルトでは、フォームオブジェクトにnullではなく、空文字がバインドされる。
この場合、「未入力は許容するが、入力された場合は6文字以上であること」という要件を、既存のアノテーションで満たすことができない。
nullをフォームオブジェクトにバインドするには、org.springframework.beans.propertyeditors.StringTrimmerEditorを使用すればよい。@Controller
@RequestMapping("xxx")
public class XxxController {
@InitBinder
public void initBinder(WebDataBinder binder) {
// bind empty strings as null
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
// omitted ...
}
この設定により、Controller毎に空文字をnullとみなすかどうかを設定できる。
プロジェクト全体で空文字をnullにしたい場合は、プロジェクト共通設定として@ControllerAdviceで設定すればよい。
@ControllerAdvice
public class XxxControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
// bind empty strings as null
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
// omitted ...
}
nullになる。@NotNullが必要であることに注意しないといけない。Tip
Spring Framework 4.0 より追加された@ControllerAdviceアノテーションの属性について
@ControllerAdviceアノテーションの属性を指定することで、@ControllerAdviceが付与されたクラスで実装したメソッドを適用するControllerを柔軟に指定できるように改善されている。
属性の詳細については、@ControllerAdviceの属性を参照されたい。
4.2.4.5. Native to Asciiを行わないメッセージの読み込み¶
Native to Asciiを行わずにBean Validationのメッセージ(ValidationMessage.properties)を読み込む方法紹介する。
日本語メッセージをNative to Asciiせずに直接扱いたい場合、Springの MessageSourceと連携すると簡単に実装することができる。
以下のように定義すると、MessageSourceの機能で読み込まれたメッセージがHibernate Validatorの中で使用されるようになる。
Bean定義
XxxDomainConfig.java// (1) @Bean("validator") public LocalValidatorFactoryBean validator( ResourceBundleMessageSource resourceBundleMessageSource) { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); bean.setValidationMessageSource(resourceBundleMessageSource()); return bean; } // (2) @Bean public ResourceBundleMessageSource resourceBundleMessageSource() { ResourceBundleMessageSource bean = new ResourceBundleMessageSource(); bean.setBasenames("ValidationMessages"); // (3) bean.setDefaultEncoding("UTF-8"); return bean; } // (4) @Bean public MethodValidationPostProcessor notAsciiValidationMethodValidationPostProcessor() { MethodValidationPostProcessor bean = new MethodValidationPostProcessor(); bean.setValidator(validator()); return bean; }
SpringMvcConfig.java@EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Inject private LocalValidatorFactoryBean validator; // omitted // (5) @Override public Validator getValidator() { return validator; } // (6) @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor bean = new MethodValidationPostProcessor(); bean.setValidator(validator); return bean; }
項番
説明
(1)LocalValidatorFactoryBeanをBean定義する。(2)MessageSourceの定義。ここではResourceBundleMessageSourceを使用する。(3)ApplicationContextに読み込ませるリソースバンドルを指定する。(4)Method Validationを利用しない場合、このBean定義は不要である。(5)WebMvcConfigurerのgetValidatorメソッドをOverrideし、(1)で定義したBeanを返却する。(6)(4)と同様である。
Bean定義
xxx-domain.xml<!-- (1) --> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="validationMessageSource"> <!-- (2) --> <bean class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>ValidationMessages</value> <!-- (3) --> </list> </property> <property name="defaultEncoding" value="UTF-8" /> </bean> </property> </bean> <!-- (4) --> <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator" /> </bean>
spring-mvc.xml<!-- (5) --> <mvc:annotation-driven validator="validator"> <!-- ommited --> </mvc:annotation-driven> <!-- (6) --> <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"> <property name="validator" ref="validator" /> </bean>
項番
説明
(1)LocalValidatorFactoryBeanをBean定義する。(2)MessageSourceの定義。ここではResourceBundleMessageSourceを使用する。(3)ApplicationContextに読み込ませるリソースバンドルを指定する。(4)Method Validationを利用しない場合、このBean定義は不要である。(5)<mvc:annotation-driven>要素のvalidator属性に、(1)で定義したBeanを指定する。(6)(4)と同様である。
Note
MessageSourceの機能を利用することで、プロパティファイルの配置先がクラスパス直下に制限されなくなる。また、複数のプロパティファイルを指定することもできるようになる。
4.2.4.6. OSコマンドインジェクション対策¶
ここでは、セキュリティ脆弱性の一種であるOSコマンドインジェクションとその対策について説明する。
4.2.4.6.1. OSコマンドインジェクションとは¶
OSコマンドインジェクションとは、アプリケーション内でユーザー入力文字列からコマンド実行文字列を組み立てている箇所がある場合に、ユーザー入力文字列の中に悪意のあるコマンドが送り込まれると、コンピュータを不正に操られてしまう問題である。
Tip
詳細は、OWASPの解説ページなどを参照されたい。
JavaではProcessBuilderクラスや、Runtimeクラスのexecメソッドを用いてコマンドを実行する際に、実行するコマンドとして以下のものを利用する場合に、OSコマンドインジェクションが発生する可能性がある。
/bin/sh(Unix系の場合)やcmd.exe(Windowsの場合)ユーザーが入力した文字列
以下では、/bin/shを利用する場合にOSコマンドインジェクションが発生する例を示す。
ProcessBuilder pb = new ProcessBuilder("/bin/sh", "-c", script); // (1)
Process p = pb.start();
項番 |
説明 |
|---|---|
(1)
|
例えば、
scriptに”exec.sh ; cat /etc/passwd” が入ると、文字列中のセミコロンが/bin/shにより区切り文字として解釈され、”cat /etc/passwd”が実行される。そのため、標準出力の扱い方によっては
/etc/passwdが出力される可能性がある。 |
Warning
ScriptEngineやScriptTemplateViewResolverの利用について
Java SE 6より追加されたScriptEngineや、Spring Framework 4.2より追加されたScriptTemplateViewResolverでは、JVM上で別言語(RubyやPythonなど)を使用することができる。
これらの機能を利用して別言語のコードを実行する場合、コードの書き方によってはOSコマンドインジェクションが発生する可能性があるため、利用には十分注意すること。
4.2.4.6.2. 対策方法¶
OSコマンドインジェクションを起こさないためには、可能な限り外部プロセスの実行を避ける。ただし諸般の事情により外部プロセスの実行がどうしても必要な場合、以下の対策を行った上で外部プロセス実行を実装すること。
極力、
/bin/sh(Unix系の場合)やcmd.exe(Windowsの場合)を使用したコマンド実行を行わないユーザーにより入力された文字が、アプリケーションとして許可されたものであるか、ホワイトリスト方式を用いてチェックする
以下では、ユーザーが入力したコマンドと引数が指定された文字列で構成されているかをホワイトリスト方式でチェックするルールの例を示す。
@Pattern(regexp = "batch0\\d\\.sh") // (1)
private String cmdStr;
@Pattern(regexp = "[\\w=_]+") // (2)
private String arg;
項番 |
説明 |
|---|---|
(1)
|
コマンドとして
batch0X.sh(Xは0から9までの半角数字)のみ許可するルールを指定する。 |
(2)
|
引数として、無害な文字である半角英数字(\w)、”
=“、”_“から構成された文字列のみ許可するルールを指定する。 |
Note
この例では、コマンドや引数にパスが含まれないようなルールとすることで、ディレクトリトラバーサルを起こさないようにしている。
@Patternを利用する場合、@Patternに指定された正規表現がそのままエラーメッセージとして出力され、以下の点でメッセージとしては不適切である。
エラーの意味が不明確となり、ユーザに優しくない
脆弱性への対策のためのロジックが利用者に露呈してしまう
エラーの意味を明確にし、かつ、ロジックを隠蔽するために、application-messages.propertiesに適切なメッセージを定義する。メッセージの定義方法については、application-messages.propertiesに定義するメッセージを参照されたい。
Pattern.cmdForm.cmdStr = permit command name: batch00.sh - batch09.sh
Pattern.cmdForm.arg = permit parameter characters and symbols: alphanumeric, =, _