1.はじめに
この記事で書くこと
- Spring Boot で Bean Validation を使って入力チェックをする方法
- Validationを自作する方法
- 出力メッセージを自作する方法
- バリデーショングループを作成して、submit毎にバリデーションを分ける方法
この記事で書かないこと
- プロジェクトの作成方法
- Spring MVCに関して(Model、View、Controllerに関する説明)
- 細かい実装方法(コピペで利用できるサンプルソースは掲載します。)
説明に利用する画面
本記事ではこの画面にエラーメッセージを表示するさまざまな方法を紹介します。
パッケージ構成
説明の中でサンプルソースを紹介しますが、下記の構成で作成いただければ
└─main
├─java
│ ├─base
│ │ │ MainApplication.java
│ │ │
│ │ ├─config
│ │ │ MvcConfig.java
│ │ │ SecurityConfig.java
│ │ │
│ │ ├─constants
│ │ │ ValidationGroup.java
│ │ │
│ │ ├─controller
│ │ │ T001ValidateController.java
│ │ │
│ │ ├─dto
│ │ │ T001ValidateForm.java
│ │ │
│ │ └─validation
│ │ Word.java
│ │ WordValidator.java
│ │
│ └─org
└─resources
│ application.yml(今回の記事では触れません)
│ messages.properties
│
└─templates
T001Validate.html
2.Bean Validation を使って入力チェックをする方法
依存関係の追加
Gradleの場合
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Mavenの場合
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
その他使用するライブラリ
org.apache.commons:commons-lang3 ・・・空白チェックに使用するStringUtilsに利用
org.projectlombok:lombok ・・・DtoのGetter,Setterに利用
DTO(Data Transfer Object)の作成
今回はT001ValidateDto.javaは作成せず、formクラスにフィールド(今回はサンプルでtext1、text2)を定義します。
フィールドに対して、入力チェックしたいアノテーションを追加することでソースを実装することなく入力チェックを行うことができます。
アノテーションの引数として「message」を追加することで、独自のメッセージ内容を指定することができます。
まずは、text1に対して「1~4桁で入力すること」という入力制限を追加してみます。
メッセージに{0}と指定すると、デフォルトでフィールド名(text1)が設定されます。
package base.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Size;
@Getter
@Setter
public class T001ValidateForm {
@Size(min= 1, max = 4, message = "{0}は1~4桁を入力してください")
private String text1;
private String text2;
}
よく利用するアノテーション
アノテーション | チェック内容 |
---|---|
@NotNull | 必須チェック(NULLの場合NG) |
@NotEmpty | 必須チェック(NULL、空文字の場合NG) |
@Size | 桁数チェック |
@Min | 数値の最小値チェック |
@Max | 数値の最大値チェック |
@Pattern | 正規表現チェック |
@AssertTrue | 相関チェック(returnがTrueの場合OK) |
@AssertFalse | 相関チェック(returnがFalseの場合OK) |
@Valid | 子クラスでもBean Validationを行いたい場合指定する |
相関チェックを実装したい場合
2つ以上の項目の相関的な入力チェックを行いたい場合は下記のように実装します。
package base.dto;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Size;
@Getter
@Setter
public class T001ValidateForm {
@Size(min= 1, max = 4, message = "{0}は1~4桁を入力してください")
private String text1;
private String text2;
@AssertTrue(message = "text1、text2どちらかを入力してください")
// 必ずpublic booleanにする
// lombokの場合boolean型のgetterはisXXXXなのでメソッド名はisから始める
public boolean isTextEmpty(){
// ここに任意の相関チェックを実装
if (StringUtils.isEmpty(text1) && StringUtils.isEmpty(text2)) {
return false;
}
return true;
}
}
Controllerの作成
package base.controller;
import base.dto.T001ValidateForm;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.validation.Validator;
@Controller
public class T001ValidateController {
@Autowired
private Validator validator;
/**
* 初期表示(http://localhost:8080/t001)
* @param model
* @param form
* @return
*/
@GetMapping("/t001")
public String init(
Model model,
T001ValidateForm form) {
model.addAttribute("infoMessage", "初期処理完了");
model.addAttribute(form);
return "T001Validate";
}
/**
* 入力チェック1ボタン押下処理
* @param model
* @param form
* @param bindingResult
* @return
*/
@PostMapping(value="/t001", params="validated1")
public String validated1(
Model model,
// @ModelAttribute・・・入力チェックとは直接関係ありませんが、画面の情報をFormが受け取る
// ために指定します。
// @Validated・・・こちらを指定することでPOST時に自動的に入力チェックが実行されます。
@ModelAttribute @Validated T001ValidateForm form,
// BindingResult・・・入力チェック結果はこの変数に格納されます。必ずForm⇒BindingResult
// の順で引数を指定してください。
BindingResult bindingResult) {
// 入力チェック判定
if (!bindingResult.hasErrors()){
model.addAttribute("infoMessage", "エラーなし");
}
model.addAttribute(form);
return "T001Validate";
}
}
HTMLの作成(パターン1:画面上部にまとめて表示する)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>T001Validate</title>
</head>
<body>
<form th:action="@{/t001}" th:method="post" th:object="${t001ValidateForm}" >
<div class="container container-m">
<div th:if="${infoMessage}" class="row">
<div class="col-md-12 m-3">
<div class="p-3 mb-2 bg-success text-white">
<label th:text="${infoMessage}"/>
</div>
</div>
</div>
<div th:if="${#arrays.length(#fields.detailedErrors())} > 0" class="row">
<div class="col-md-12 m-3">
<div class="p-3 mb-2 bg-danger text-white">
<div th:each="error : ${#fields.detailedErrors()}">
<span th:text="${error.message}"/>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 m-3">
<label>text1</label>
<input th:field="*{text1}">
</div>
</div>
<div class="row">
<div class="col-md-12 m-3">
<label>text2</label>
<input th:field="*{text2}">
</div>
</div>
<div class="row">
<div class="col-md-12 m-3">
<button name="validated1" type="submit">入力チェック1</button>
</div>
</div>
</div>
</form>
</body>
</html>
HTMLの作成(パターン2:項目ごとに個別に表示する)
<!DOCTYPE html>
<!-- layoutを使わないバージョン -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>T001Validate</title>
<meta charset="utf-8" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</head>
<body>
<form th:action="@{/t001}" th:method="post" th:object="${t001ValidateForm}" >
<div class="container container-m">
<div th:if="${infoMessage}" class="row">
<div class="col-md-12 m-3">
<div class="p-3 mb-2 bg-success text-white">
<label th:text="${infoMessage}"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 m-3">
<label>text1</label>
<!-- 1.th:errorclass="border-danger" -->
<!-- 2.th:classappend="${#fields.hasErrors('textEmpty')} ? 'border-danger'" -->
<!-- 1と2はどちらもエラーの場合テキストボックスを赤枠に返る処理。1の方がシンプルだが、
"text1"というフィールドに対するエラーしか拾えない。今回は相関チェックで"textEmpty"という
フィールドも拾いたいので2も使用した。 -->
<input th:field="*{text1}" th:errorclass="border-danger" th:classappend="${#fields.hasErrors('textEmpty')} ? 'border-danger'">
<div class="text-danger" th:if="${#fields.hasErrors('text1')}" th:errors="*{text1}"></div>
<div class="text-danger" th:if="${#fields.hasErrors('textEmpty')}" th:errors="*{textEmpty}"></div>
</div>
</div>
<div class="row">
<div class="col-md-12 m-3">
<label>text2</label>
<input th:field="*{text2}" th:classappend="${#fields.hasErrors('text2') || #fields.hasErrors('textEmpty')} ? 'border-danger'">
<div class="text-danger" th:if="${#fields.hasErrors('text2')}" th:errors="*{text2}"></div>
<div class="text-danger" th:if="${#fields.hasErrors('textEmpty')}" th:errors="*{textEmpty}"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 m-3">
<button name="validated1" type="submit">入力チェック1</button>
</div>
</div>
</div>
</form>
</body>
</html>
3.Validationを自作する
アノテーションクラスを作成する
このクラスは実装はメッセージ内容のみを変更すれば、その他のアノテーションにも複製可能です。
package base.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = WordValidator.class)
@Documented
@Repeatable(Word.List.class)
public @interface Word {
String message() default "{value} のいずれかから選択してください";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value();
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
Word[] value();
}
}
Validationクラスを作成する。
実際のチェックロジックを実装するクラスです。
package base.validation;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class WordValidator implements ConstraintValidator<Word, String>{
Word word;
@Override
public void initialize(Word word) {
this.word = word;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
// 任意のバリデーション処理を実装
// NULLor空の場合はチェックしない(チェックは@NotNullに任せる)
if (StringUtils.isEmpty(value)) {
return true;
}
return Arrays.asList(word.value()).contains(value);
}
}
DTOにアノテーションを追加する
package base.dto;
import base.validation.Word;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Size;
@Getter
@Setter
public class T001ValidateForm {
@Size(min= 1, max = 4, message = "{0}は1~4桁を入力してください")
private String text1;
// messageはWord.javaに指定しているので不要(指定することも可能)
@Word({"あ", "い"})
private String text2;
@AssertTrue(message = "text1、text2どちらかを入力してください")
public boolean isTextEmpty(){
if (StringUtils.isEmpty(text1) && StringUtils.isEmpty(text2)) {
return false;
}
return true;
}
}
4.メッセージをプロパティファイルに定義する方法
実際の業務となると、フィールド数が増えて同じようなメッセージをいろいろなDTOに定義することになります。
メッセージが変更になった時にすべてのDTOを確認する必要がでてきますが、とても大変。。。
そんな時はメッセージプロパティファイルを利用しましょう。
messages.propertiesを作成する
Springのデフォルトはmain/resourcesの直下です。ここにmessages.propertiesファイルを作成しましょう。
t001.validation.range={0}は{min}~{max}桁を入力してください
t001.validation.multiRequired={0}どちらかを入力してください
custom.validation.word={value}のいずれかから選択してください
# 画面出力時、フィールド名を任意の値に変更する場合はこちらに指定
textEmpty=text1,text2
ちなみに。。。
htmlの任意のタグにmessages.propertiesに設定したプロパティを下記xxxxxxxに指定することで、プロパティファイルの内容を直接表示することもできます。
th:text="#{xxxxxxx}"
DTOのメッセージ指定を修正
package base.dto;
import base.validation.Word;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Size;
@Getter
@Setter
public class T001ValidateForm {
@Size(min= 1, max = 4, message = "{t001.validation.range}")
private String text1;
@Word({"あ", "い"})
private String text2;
@AssertTrue(message = "{t001.validation.multiRequired}")
// 必ずpublic booleanにする
// lombokの場合boolean型のgetterはisXXXXなのでメソッド名はisから始める
public boolean isTextEmpty(){
// ここに任意の相関チェックを実装
if (StringUtils.isEmpty(text1) && StringUtils.isEmpty(text2)) {
return false;
}
return true;
}
}
自作Validationクラスのメッセージ指定を修正
package base.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = WordValidator.class)
@Documented
@Repeatable(Word.List.class)
public @interface Word {
String message() default "{custom.validation.word}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value();
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
Word[] value();
}
}
5.バリデーショングループを利用して、submit毎にバリデーションを分ける方法
例えば、下記のように同じ画面に実行ボタンが二つ以上ある場合。
それぞれの入力チェック内容は分けたいがDTOは同じものを利用したいという時に活用する。
バリデーショングループを定義するクラスを作成する
package base.constants;
public interface ValidationGroup {
public static interface Button1{}
public static interface Button2{}
}
DTOにバリデーショングループを指定する。
「入力チェック1」ボタンを押したときは@Sizeと@AssertTrueのチェックを実行させ
「入力チェック2」ボタンを押したときは@Wordと@AssertTrueのチェックを実行させることとする
package base.dto;
import base.constants.ValidationGroup.Button1;
import base.constants.ValidationGroup.Button2;
import base.validation.Word;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Size;
@Getter
@Setter
public class T001ValidateForm {
@Size(min= 1, max = 4, message = "{t001.validation.range}", groups = Button1.class)
private String text1;
@Word(value = {"あ", "い"} , groups = {Button1.class, Button2.class})
private String text2;
@AssertTrue(message = "{t001.validation.multiRequired}" , groups = Button2.class)
// 必ずpublic booleanにする
// lombokの場合boolean型のgetterはisXXXXなのでメソッド名はisから始める
public boolean isTextEmpty(){
// ここに任意の相関チェックを実装
if (StringUtils.isEmpty(text1) && StringUtils.isEmpty(text2)) {
return false;
}
return true;
}
}
Controllerにバリデーショングループを指定する。
package base.controller;
import base.constants.ValidationGroup.Button1;
import base.constants.ValidationGroup.Button2;
import base.dto.T001ValidateForm;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.validation.Validator;
@Controller
public class T001ValidateController {
@Autowired
private Validator validator;
/**
* 初期表示(http://localhost:8080/t001)
* @param model
* @param form
* @return
*/
@GetMapping("/t001")
public String init(
Model model,
T001ValidateForm form) {
model.addAttribute("infoMessage", "初期処理完了");
model.addAttribute(form);
return "T001Validate";
}
/**
* 入力チェック1ボタン押下処理
* @param model
* @param form
* @param bindingResult
* @return
*/
@PostMapping(value="/t001", params="validated1")
public String validated1(
Model model,
// 入力チェック1を押したときの入力チェックはButton1に限定する
@ModelAttribute @Validated(Button1.class) T001ValidateForm form,
BindingResult bindingResult) {
// 入力チェック判定
if (!bindingResult.hasErrors()){
model.addAttribute("infoMessage", "エラーなし");
}
model.addAttribute(form);
return "T001Validate";
}
/**
* 入力チェック2ボタン押下処理
* @param model
* @param form
* @param bindingResult
* @return
*/
@PostMapping(value="/t001", params="validated2")
public String validated2(
Model model,
// 入力チェック2を押したときの入力チェックはButton2に限定する
@ModelAttribute @Validated(Button2.class) T001ValidateForm form,
BindingResult bindingResult) {
// 入力チェック判定
if (!bindingResult.hasErrors()){
model.addAttribute("infoMessage", "エラーなし");
}
model.addAttribute(form);
return "T001Validate";
}
}
以上
不明点、改善点がございましたら是非コメントお願いします。
他にもSpringbootに関する記事を投稿していますので、ご興味ある方は是非ご覧ください。