新規登録の入力画面に入力チェックを追加します。入力内容に誤りがあれば、確認画面には進まずに入力画面へ戻り、項目ごとにエラーメッセージを表示します。エラーメッセージは properties ファイルにまとめて書きます。
この章で学ぶこと
- サーバー側で入力チェックを行う理由
- Bean Validation のアノテーション(
@NotBlank、@NotNull、@Sizeなど)の使い方 - エラーメッセージを
messages.propertiesに書く方法 -
@ValidatedとBindingResultでチェック結果を受け取る方法 - Thymeleaf でエラーメッセージを表示する方法
13.1 なぜ入力チェックが必要か
第11章の最後で、何も入力せずに「確認」→「登録」と進むと、ORA-01400(NULL は挿入できません)の Whitelabel エラー画面になることを確認しました。データベースの列に NOT NULL 制約があるためです。
利用者に Whitelabel エラー画面を見せるのではなく、入力画面の段階で誤りを知らせて、入力し直してもらう のが入力チェックの役割です。
ブラウザ側のチェックとサーバー側のチェック
入力チェックには、2つの場所で行う方法があります。
| 場所 | 方法の例 | 特徴 |
|---|---|---|
| ブラウザ側 | HTML の required 属性、JavaScript |
サーバーに送る前にすぐ知らせられる。ただし、ブラウザの機能で無効にしたり、ブラウザを使わずにリクエストを送ったりして、簡単に回避できる |
| サーバー側 | Java のプログラム(この教材では Bean Validation) | 送られてきた値を必ずチェックできる。回避できない |
【ご注意】サーバー側のチェックは必須
ブラウザ側のチェックは「使いやすくするためのもの」で、正しくないデータを防ぐ役には立ちません。業務システムでは、サーバー側のチェックを必ず行います。この教材では、サーバー側のチェックだけを作ります。
この章で作る入力チェックの内容は、次のとおりです。
| 項目 | チェック内容 |
|---|---|
| 氏名 | 必須、20文字以内 |
| 性別 | 必須 |
| 年齢 | 必須、半角数字、15〜99 |
| 生年月日 | 必須、正しい日付、過去の日付 |
| 部署 | 必須 |
| 住所(都道府県) | 必須、10文字以内 |
13.2 入力チェックのライブラリ(Validation)
入力チェックの機能(Bean Validation)を使うには、Validation というライブラリが必要です。第4章でプロジェクトを作ったときに、依存関係として Validation を選んでいるので、追加の作業は不要 です。
もし第4章で Validation を選び忘れていた場合は、パッケージ・エクスプローラーで employee-app プロジェクトを右クリックし、Spring → スターターの追加 を選んで、Validation にチェックを付けて追加できます。
13.3 エラーメッセージのファイルを作る
画面に表示するエラーメッセージは、Java のコードの中には書かず、messages.properties というファイルにまとめて書きます。
-
src/main/resourcesを右クリックし、新規 → ファイル を選びます。 - 「ファイル名」に
messages.propertiesと入力し、完了 を押します。 - 次の内容を入力して保存します。
# 入力チェックのエラーメッセージ
name.notBlank=氏名を入力してください。
name.size=氏名は20文字以内で入力してください。
gender.notNull=性別を選択してください。
age.notNull=年齢を入力してください。
age.range=年齢は15〜99の範囲で入力してください。
birthDate.notNull=生年月日を入力してください。
birthDate.past=生年月日には過去の日付を入力してください。
deptId.notNull=部署を選択してください。
address.notBlank=住所(都道府県)を入力してください。
address.size=住所(都道府県)は10文字以内で入力してください。
# 入力された値の形式が正しくないとき(数値の欄に文字を入力したときなど)のエラーメッセージ
typeMismatch.age=年齢は半角数字で入力してください。
typeMismatch.birthDate=生年月日を正しく入力してください。
1行に1つずつ、キー=メッセージ の形で書きます。# で始まる行はコメントです。キーは、次の節でアノテーションから指定するための名前です。
13.4 フォームにチェックのアノテーションを付ける
第10章で作った RegisterForm のフィールドに、チェックの内容を表すアノテーションを付けます。追加する行は色が付いている部分です。
package com.example.employeeapp.form;
import java.io.Serializable;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size;
public class RegisterForm implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "{name.notBlank}")
@Size(max = 20, message = "{name.size}")
private String name;
@NotNull(message = "{gender.notNull}")
private Integer gender;
@NotNull(message = "{age.notNull}")
@Min(value = 15, message = "{age.range}")
@Max(value = 99, message = "{age.range}")
private Integer age;
@NotNull(message = "{birthDate.notNull}")
@Past(message = "{birthDate.past}")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthDate;
@NotNull(message = "{deptId.notNull}")
private Integer deptId;
@NotBlank(message = "{address.notBlank}")
@Size(max = 10, message = "{address.size}")
private String address;
// ゲッター・セッターは第10章のまま(変更なし)
...
}
アノテーションの意味
| アノテーション | チェック内容 | 付けた項目 |
|---|---|---|
@NotBlank |
null、空文字、空白だけの文字列を許さない | 氏名、住所 |
@NotNull |
null を許さない | 性別、年齢、生年月日、部署 |
@Size(max = 20) |
文字数が指定の範囲に収まっているか | 氏名(20文字)、住所(10文字) |
@Min(value = 15) |
指定の値以上か | 年齢 |
@Max(value = 99) |
指定の値以下か | 年齢 |
@Past |
過去の日付か | 生年月日 |
String には @NotBlank、Integer には @NotNull
氏名の入力欄を空のまま送信すると、name には null ではなく 空文字("") が入ります。@NotNull では空文字を見逃してしまうので、文字列には @NotBlank を使います。
一方、年齢や部署のような Integer の項目は、空のまま送信すると null になります。数値には「空文字」がないので、@NotNull を使います。
message = "{name.notBlank}" の意味
message には、エラーのときに表示するメッセージを指定します。{ } で囲むと、messages.properties のキー を指定したことになり、Spring がファイルから対応するメッセージを探して表示します。
@NotBlank(message = "{name.notBlank}")
↓ キー name.notBlank を探す
name.notBlank=氏名を入力してください。 ← messages.properties
年齢の @Min と @Max は、どちらも同じキー age.range を指定しています。このように、同じメッセージを何か所からでも使えます。
13.5 Controller でチェック結果を受け取る
アノテーションを付けただけでは、チェックは行われません。Controller で「このフォームをチェックする」と指定します。EmployeeRegisterController の confirm メソッドを、次のように変更します。
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
// (以下の import は第11章のまま)
...
/** 社員登録の確認画面を表示
* @param registerForm 入力された社員情報
* @param bindingResult 入力チェックの結果
* @param model 表示する部署情報
* @return 正常:社員登録の確認画面
* エラー:社員登録の入力画面 */
@PostMapping("/employees/register/confirm")
public String confirm(@Validated @ModelAttribute RegisterForm registerForm,
BindingResult bindingResult, Model model) {
// 入力チェックでエラーがあれば、エラーメッセージを付けて入力画面に戻す
if (bindingResult.hasErrors()) {
List<Department> departments = departmentRepository.findAllByOrderByDeptIdAsc();
model.addAttribute("departments", departments);
return "employee/register/input";
}
Department department = departmentRepository.findByDeptId(registerForm.getDeptId());
model.addAttribute("department", department);
return "employee/register/confirm";
}
変更前は confirm(@ModelAttribute RegisterForm registerForm, Model model) でした。引数の Model model の前に BindingResult bindingResult が入る点に注意してください。
| 記述 | 意味 |
|---|---|
@Validated |
この引数のフォームを、アノテーションの内容に従ってチェックする |
BindingResult bindingResult |
チェックの結果(どの項目にどんなエラーがあったか)が入る |
bindingResult.hasErrors() |
エラーが1つでもあれば true
|
【ご注意】BindingResult は、チェックするフォームのすぐ後ろに書く
BindingResult は、@Validated を付けた引数の 直後 に書く決まりです。間に別の引数を挟んだり書き忘れたりすると、エラーがあったときに入力画面に戻らず、Whitelabel エラー画面(400 Bad Request)になってしまいます。
エラーがあったときは、部署のプルダウン用の一覧を入れ直してから、入力画面のテンプレート名を返します。セッションのフォームには、送信された値(誤りのある値も含む)がそのまま入っているので、入力画面には利用者が入力した内容が残った状態で表示されます。誤りのある項目だけを直せばよいので、入力し直しが楽になります。
13.6 入力画面にエラーメッセージを表示する
第10章で作った register/input.html に、項目ごとのエラーメッセージを表示する行を追加します。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>社員新規登録</title>
<link rel="stylesheet" th:href="@{/css/app.css}">
</head>
<body>
<h1>社員新規登録</h1>
<form th:action="@{/employees/register/confirm}" th:object="${registerForm}" method="post">
<table>
<tr>
<th>氏名</th>
<td>
<input type="text" th:field="*{name}">
<div class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</td>
</tr>
<tr>
<th>性別</th>
<td>
<label><input type="radio" th:field="*{gender}" value="1"> 男</label>
<label><input type="radio" th:field="*{gender}" value="2"> 女</label>
<div class="error" th:if="${#fields.hasErrors('gender')}" th:errors="*{gender}"></div>
</td>
</tr>
<tr>
<th>年齢</th>
<td>
<input type="text" th:field="*{age}">
<div class="error" th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></div>
</td>
</tr>
<tr>
<th>生年月日</th>
<td>
<input type="date" th:field="*{birthDate}">
<div class="error" th:if="${#fields.hasErrors('birthDate')}" th:errors="*{birthDate}"></div>
</td>
</tr>
<tr>
<th>部署</th>
<td>
<select th:field="*{deptId}">
<option value="">選択してください</option>
<option th:each="department : ${departments}"
th:value="${department.deptId}"
th:text="${department.deptName}"></option>
</select>
<div class="error" th:if="${#fields.hasErrors('deptId')}" th:errors="*{deptId}"></div>
</td>
</tr>
<tr>
<th>住所(都道府県)</th>
<td>
<input type="text" th:field="*{address}">
<div class="error" th:if="${#fields.hasErrors('address')}" th:errors="*{address}"></div>
</td>
</tr>
</table>
<button type="submit">確認</button>
</form>
<p><a th:href="@{/employees}">一覧へ戻る</a></p>
</body>
</html>
| 記述 | 意味 |
|---|---|
th:if="${#fields.hasErrors('name')}" |
name の項目にエラーがあるときだけ、この div を表示する(エラーがなければ div ごと出力されない) |
th:errors="*{name}" |
name の項目のエラーメッセージを、タグの中身として表示する |
class="error" |
第8章で作った app.css に用意しておいたスタイル。メッセージを赤い文字で表示する |
13.7 動作確認
- すべてのファイルを保存します。アプリを起動している場合は、いったん停止してから起動し直してください。
- Chrome で
http://localhost:8080/employeesを開き、「新規登録」ボタンを押します。 - 何も入力せずに「確認」ボタンを押します。確認画面には進まず、入力画面にすべての項目のエラーメッセージが表示されます。
【知識メモ】アドレス欄の URL
エラーで入力画面に戻ったとき、アドレス欄は送信先の /employees/register/confirm のままになります。Controller が確認画面ではなく入力画面のテンプレートを返したためで、問題ありません。
- 次のように入力して、もう一度「確認」ボタンを押します。
- 氏名:
田中 美咲、性別:女、部署:開発部、住所:愛知県 - 年齢:
二十四(漢字で入力) - 生年月日:
2099/01/01(未来の日付)
- 氏名:
年齢と生年月日だけにエラーメッセージが表示され、ほかの項目は入力した内容が残っています。
- 年齢を
24、生年月日を2001/06/15に直して「確認」ボタンを押します。今度は確認画面に進めます。
【便利な使い方】ほかのチェックも試してみよう
- 氏名に21文字以上入力する →「氏名は20文字以内で入力してください。」
- 年齢に
10を入力する →「年齢は15〜99の範囲で入力してください。」 - 住所に11文字以上入力する →「住所(都道府県)は10文字以内で入力してください。」
13.8 解説:型の変換エラーとメッセージ
typeMismatch:入力チェックの前の「型の変換」
ブラウザから送られてくる値は、すべて文字列です。Spring は、フォームに値を詰めるときに、Integer の age には数値へ、Date の birthDate には日付へと、型の変換 を行います。
年齢に 二十四 と入力すると、数値に変換できないため、アノテーションのチェックより前の段階で失敗します。このとき Spring は、typeMismatch.項目名 というキーのメッセージを messages.properties から自動で探して表示します。
| 入力 | 何が起きるか | 使われるキー |
|---|---|---|
年齢に 二十四
|
数値に変換できない(型の変換エラー) | typeMismatch.age |
年齢に 10
|
変換はできるが、@Min(15) のチェックに引っかかる |
age.range |
| 年齢が空 | null になり、@NotNull のチェックに引っかかる |
age.notNull |
typeMismatch.age はアノテーションで指定していないキーですが、Spring が決まった名前で探してくれるので、messages.properties に書いておくだけで使われます。
年齢の入力欄を type="text" にした理由
第10章で、年齢の入力欄をあえて type="number" ではなく type="text" にしました。type="number" にすると Chrome が数字以外を入力させないため、サーバー側の型の変換エラーを体験できないからです。実際のシステムでは、使いやすさのために type="number" にすることもよくあります。その場合でも、サーバー側のチェックは必要です(13.1 を参照)。
生年月日は type="date" なので、Chrome ではカレンダーから選ぶ形になり、通常は形式の誤りは起きません。ただし、ブラウザ側の仕組みは回避できるため、念のため typeMismatch.birthDate のメッセージも用意しています。
メッセージを properties ファイルに分ける利点
- 文言の変更が簡単:メッセージの言い回しを変えたいときは、messages.properties だけを直せばよく、Java のコードを修正する必要がない
-
同じメッセージを使い回せる:
age.rangeのように、1つのキーを複数のアノテーションから使える。第14章では、変更画面でもまったく同じメッセージを使う - メッセージの一覧が見やすい:画面に出るメッセージが1つのファイルにまとまっているので、表記のゆれ(「〜してください」「〜して下さい」など)に気づきやすい
【ご注意】現場では:チェック内容をデータベースの列定義と合わせる
氏名を「20文字以内」、住所を「10文字以内」にしたのは、第3章で作ったテーブルの列が NAME VARCHAR2(20 CHAR)、ADDRESS VARCHAR2(10 CHAR) だからです。入力チェックの上限が列の長さより大きいと、チェックは通ったのにデータベースへの登録でエラーになってしまいます。現場では、入力チェックの内容がテーブルの設計と食い違っていないか、必ず確認してください。
13.9 よくあるエラー
| 症状 | 主な原因と対処 |
|---|---|
| 何も入力しなくてもエラーにならず、確認画面に進んでしまう |
confirm メソッドの引数に @Validated を付け忘れていないか確認してください。また、第4章で Validation を選んでいるか確認してください(選んでいないと、jakarta.validation の import でエラーになります。追加の方法は 13.2) |
| 入力画面に戻らず、Whitelabel エラー画面(400 Bad Request)になる |
BindingResult を書き忘れているか、@Validated を付けた引数の直後に書かれていません |
エラーメッセージが {name.notBlank} のように、そのまま表示される |
messages.properties のキーの綴りと、アノテーションの message のキーが一致しているか確認してください。また、ファイル名が messages.properties で、src/main/resources の直下にあるか確認してください |
| エラーメッセージが文字化けする | この教材で利用している Pleiades 提供の Eclipse は UTF-8 なので、起きません。ほかの環境で起きたときは、messages.properties を UTF-8 で保存し直してください |
| エラーで入力画面に戻ると、部署のプルダウンが空になる |
hasErrors() のブロックで、部署の一覧を Model に入れる2行を書き忘れています |
| 年齢に文字を入れると、英語の長いメッセージが表示される | messages.properties に typeMismatch.age がないか、綴りが違っています |
13.10 まとめと確認問題
まとめ
- ブラウザ側のチェックは回避できるので、サーバー側の入力チェックは必ず行う
- Bean Validation を使うには、Validation のライブラリが必要(第4章で選択済み)
- フォームのフィールドに
@NotBlank、@NotNull、@Size、@Min、@Max、@Pastなどを付けてチェック内容を表す - エラーメッセージは messages.properties に
キー=メッセージの形で書き、アノテーションからはmessage = "{キー}"で指定する - Controller では、引数に
@Validatedを付け、直後のBindingResultでエラーの有無を調べる - 画面では
#fields.hasErrorsとth:errorsで項目ごとのエラーメッセージを表示する - 数値や日付に変換できないときは、
typeMismatch.項目名のメッセージが使われる
確認問題
問1 HTML の required 属性でブラウザ側のチェックをしていれば、サーバー側の入力チェックは不要でしょうか。
解答を見る
不要ではありません。ブラウザ側のチェックは、ブラウザの機能で無効にしたり、ブラウザを使わずにリクエストを送ったりして簡単に回避できます。正しくないデータを防ぐには、サーバー側のチェックが必須です。
問2 氏名(String)の必須チェックに @NotNull ではなく @NotBlank を使うのはなぜですか。
解答を見る
入力欄を空のまま送信すると、name には null ではなく空文字("")が入るからです。@NotNull は空文字を見逃してしまいますが、@NotBlank は null・空文字・空白だけの文字列をすべてエラーにします。
問3 @Size(max = 20, message = "{name.size}") の {name.size} は何を表していますか。
解答を見る
messages.properties のキーです。エラーのときは、messages.properties の name.size= の行に書いたメッセージ「氏名は20文字以内で入力してください。」が表示されます。
問4 confirm メソッドの引数で、BindingResult を Model の後ろに書いてしまいました。何が起きますか。
解答を見る
入力にエラーがあったとき、入力画面に戻らずにWhitelabel エラー画面(400 Bad Request)になります。BindingResult は、@Validated を付けた引数の直後に書く決まりです。
問5 年齢に abc と入力したときと、10 と入力したときでは、それぞれどのキーのメッセージが表示されますか。
解答を見る
abc のときは数値に変換できないため、型の変換エラーとして typeMismatch.age(「年齢は半角数字で入力してください。」)が表示されます。10 のときは変換はできますが @Min(15) のチェックに引っかかるため、age.range(「年齢は15〜99の範囲で入力してください。」)が表示されます。

