23
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SpringBoot基礎 バリデーション編(入力チェック)

Last updated at Posted at 2022-09-07

1.はじめに

この記事で書くこと

  • Spring Boot で Bean Validation を使って入力チェックをする方法
  • Validationを自作する方法
  • 出力メッセージを自作する方法
  • バリデーショングループを作成して、submit毎にバリデーションを分ける方法

この記事で書かないこと

  • プロジェクトの作成方法
  • Spring MVCに関して(Model、View、Controllerに関する説明)
  • 細かい実装方法(コピペで利用できるサンプルソースは掲載します。)

説明に利用する画面

本記事ではこの画面にエラーメッセージを表示するさまざまな方法を紹介します。
image.png

パッケージ構成

説明の中でサンプルソースを紹介しますが、下記の構成で作成いただければ

└─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の場合

build.gradle
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-validation'
}

Mavenの場合

pom.xml
<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)が設定されます。

T001ValidateForm.java
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つ以上の項目の相関的な入力チェックを行いたい場合は下記のように実装します。

T001ValidateForm.java
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の作成

T001ValidateController.java
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:画面上部にまとめて表示する)

image.png

T001Validate.html
<!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:項目ごとに個別に表示する)

image.png

T001Validate.html
<!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を自作する

image.png

アノテーションクラスを作成する

このクラスは実装はメッセージ内容のみを変更すれば、その他のアノテーションにも複製可能です。

Word.java
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クラスを作成する。

実際のチェックロジックを実装するクラスです。

WordValidator.java
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にアノテーションを追加する

T001ValidateForm.java
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ファイルを作成しましょう。

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のメッセージ指定を修正

T001ValidateForm.java
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クラスのメッセージ指定を修正

Word.java
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は同じものを利用したいという時に活用する。
image.png

入力チェック1を押した場合
image.png

入力チェック2を押した場合
image.png

バリデーショングループを定義するクラスを作成する

ValidationGroup.java
package base.constants;

public interface ValidationGroup {
    public static interface Button1{}
    public static interface Button2{}
}

DTOにバリデーショングループを指定する。

「入力チェック1」ボタンを押したときは@Size@AssertTrueのチェックを実行させ
「入力チェック2」ボタンを押したときは@Word@AssertTrueのチェックを実行させることとする

T001ValidateForm.java
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にバリデーショングループを指定する。

T001ValidateController.java
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に関する記事を投稿していますので、ご興味ある方は是非ご覧ください。

23
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?