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

# １．はじめに
### この記事で書くこと
* Spring Boot で Bean Validation を使って入力チェックをする方法
* Validationを自作する方法
* 出力メッセージを自作する方法
* バリデーショングループを作成して、submit毎にバリデーションを分ける方法

### この記事で書かないこと
* プロジェクトの作成方法
* Spring MVCに関して（Model、View、Controllerに関する説明）
* 細かい実装方法（コピペで利用できるサンプルソースは掲載します。）

### 説明に利用する画面
本記事ではこの画面にエラーメッセージを表示するさまざまな方法を紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/bf4c616a-235e-47b1-3340-5d1fb9bbc96a.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

```

# ２．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>
```

:::note info
その他使用するライブラリ
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";
    }

    /**
     * 入力チェック１ボタン押下処理
     * @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の作成（パターン１：画面上部にまとめて表示する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/3e718016-9ffe-f849-c074-72b855eb79c7.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">入力チェック１</button>
                </div>
            </div>
        </div>
    </form>
</body>
</html>
```

## HTMLの作成（パターン２：項目ごとに個別に表示する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/5018aa9b-3153-2388-55c4-3cf898f9d5fd.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>
                    <!-- １．th:errorclass="border-danger" -->
                    <!-- ２．th:classappend="${#fields.hasErrors('textEmpty')} ? 'border-danger'" -->
                    <!-- １と２はどちらもエラーの場合テキストボックスを赤枠に返る処理。１の方がシンプルだが、
                    "text1"というフィールドに対するエラーしか拾えない。今回は相関チェックで"textEmpty"という
                    フィールドも拾いたいので２も使用した。 -->
                    <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">入力チェック１</button>
                </div>
            </div>
        </div>
    </form>
</body>
</html>
```

# ３．Validationを自作する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/2a03c182-4d98-6ae2-861d-7ad6d95ac152.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;
    }
}
```

# ４．メッセージをプロパティファイルに定義する方法
実際の業務となると、フィールド数が増えて同じようなメッセージをいろいろな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
```

:::note info
ちなみに。。。
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();
    }
}

```

# ５．バリデーショングループを利用して、submit毎にバリデーションを分ける方法
例えば、下記のように同じ画面に実行ボタンが二つ以上ある場合。
それぞれの入力チェック内容は分けたいがDTOは同じものを利用したいという時に活用する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/0aa5eaef-9701-1f6a-fce3-5f10b1b55d27.png)

入力チェック１を押した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/3cc03fbd-d97f-f79d-ac62-92dcc5ee8400.png)

入力チェック２を押した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1035894/85f5ab79-5d36-9407-c1f6-a4e6fc688921.png)

## バリデーショングループを定義するクラスを作成する
```ValidationGroup.java
package base.constants;

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

```

## DTOにバリデーショングループを指定する。
「入力チェック１」ボタンを押したときは@Sizeと@AssertTrueのチェックを実行させ
「入力チェック２」ボタンを押したときは@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";
    }

    /**
     * 入力チェック１ボタン押下処理
     * @param model
     * @param form
     * @param bindingResult
     * @return
     */
    @PostMapping(value="/t001", params="validated1")
    public String validated1(
            Model model,
            // 入力チェック１を押したときの入力チェックはButton1に限定する
            @ModelAttribute @Validated(Button1.class) T001ValidateForm form,
            BindingResult bindingResult) {
        // 入力チェック判定
        if (!bindingResult.hasErrors()){
            model.addAttribute("infoMessage", "エラーなし");
        }
        model.addAttribute(form);
        return "T001Validate";
    }

    /**
     * 入力チェック２ボタン押下処理
     * @param model
     * @param form
     * @param bindingResult
     * @return
     */
    @PostMapping(value="/t001", params="validated2")
    public String validated2(
            Model model,
            // 入力チェック２を押したときの入力チェックはButton2に限定する
            @ModelAttribute @Validated(Button2.class) T001ValidateForm form,
            BindingResult bindingResult) {
        // 入力チェック判定
        if (!bindingResult.hasErrors()){
            model.addAttribute("infoMessage", "エラーなし");
        }
        model.addAttribute(form);
        return "T001Validate";
    }
}
```

以上

不明点、改善点がございましたら是非コメントお願いします。
他にもSpringbootに関する記事を投稿していますので、ご興味ある方は是非ご覧ください。

https://qiita.com/shinjimorimoto/items/5b74fc61bfd7cd57e5a7

https://qiita.com/shinjimorimoto/items/dcf733c562bc3fac34c7