フォームバリデーションのカスタマイズ【Spring Boot】

#概要#
フォームバリデーションをする際、Bean Validation APIの標準の制約（@NotBlanckなど）を使うほかに、独自のチェックをする制約を利用したくなる場面があると思います。そのような場合、これまではコントローラで実装してきましたが、特定のFormオブジェクトのチェック処理を集約したバリデータを利用すると、コントローラからチェック処理を切り離すことができることが分かりました。

#前提#
社内システムのの管理者登録を実装する場合を想定します。

チェック項目は以下の通りとします。
・空欄ではないこと(@NotBlanck / @NotNull)
・従業員テーブルに存在する社員であること(存在チェック)
・パスワードと確認用パスワードが一致していること(相関チェック)

```register.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>管理者登録</title>
</head>
<body>
<h4>管理者登録</h4>
<form th:action="@{/admin/register}" method="post" th:object="${registerForm}">
社員番号：<br>
<div th:errors="*{employeeId}" style="color:red"></div>
<input type="text" th:field="*{employeeId}">
<br>
パスワード：<br>
<div th:errors="*{password}" style="color:red"></div>
<input type="password" th:field="*{password}">
<br>
確認用パスワード:<br>
<div th:errors="*{confirmPassword}" style="color:red"></div>
<input type="password" th:field="*{confirmPassword}">
<br><br>
<button>登録</button>
</form>
</body>
</html>
```

```RegisterForm.java
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class RegisterForm {
	
	@NotNull(message="社員番号を入力してください")
	private Integer employeeId;
	
	@NotBlank(message="パスワードを入力してください")
	private String password;
	
	@NotBlank(message="確認用パスワードを入力してください")
	private String confirmPassword;

//以下Getter,Setter
```

#コントローラにチェック処理を実装した場合#

```RegisterController.java
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.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
@RequestMapping("admin")
public class RegisterController {
	
	@Autowired
	private EmployeeService employeeService;
	
	@ModelAttribute
	public RegisterForm setUpRegisterForm() {
		return new RegisterForm();
	}
	
	@RequestMapping("/index")
	public String index() {
		return "register";
	}
	
	@RequestMapping("/register")
	public String register(@Validated RegisterForm form, BindingResult result, Model model) {
		if(result.hasErrors()) {
		
			if(!form.getPassword().equals(form.getConfirmPassword())){
				result.rejectValue("password", "", "パスワードが一致していません");
			}
			
			if(employeeService.findById(form.getEmployeeId()) == null) {
				result.rejectValue("employeeId", "", "存在しない社員番号です");
			}
			
			return "register";
		}
		
		
		if(!form.getPassword().equals(form.getConfirmPassword())){
			result.rejectValue("password", "", "パスワードが一致していません");
			return "register";
		}
		
		if(employeeService.findById(form.getEmployeeId()) == null) {
			result.rejectValue("employeeId", "", "存在しない社員番号です");
			return "register";
		}
		
		//管理者登録の処理（省略）
			
		return "complete";//登録完了画面
	}
}
```

```if(result.hasErrors()) {　…　}```の中と外にチェック処理を書く必要がありました。
これは、@NotBlanckなどの標準の制約と独自に設定した制約の両方に違反している場合に、両方のエラー文を表示させるためです。（社員番号が空欄かつ、パスワードと確認用パスワードが一致していない場合など）

しかし、なんとなくスマートな書き方ではない感じがします。

#バリデータにチェック処理を切り分ける#
バリデーターを作成してコントローラに実装していたチェック処理を切り分けます。


```RegisterValidator.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;


@Component
public class RegisterValidator implements Validator{
	
	@Autowired//DBにアクセスするため設定
	private EmployeeService employeeService;	
	
	@Override
    public boolean supports(Class<?> clazz) {

		return RegisterForm.class.isAssignableFrom(clazz);
    }
	
	@Override
	    public void validate(Object form, Errors errors) {
		RegisterForm validationForm = (RegisterForm)form;
		
	        if(!validationForm.getPassword().equals(validationForm.getConfirmPassword())){
				errors.rejectValue("password", "", "パスワードが一致していません");
			}
			if(employeeService.findById(validationForm.getEmployeeId()) == null) {
				errors.rejectValue("employeeId", "", "存在しない社員番号です");
			}
			
		}
}

```

コントローラーも修正します。
@InitBinderで```RegisterValidator.java```をWebDataBinderオブジェクトに設定することで、フォームクラスの入力チェックのタイミングで、このバリデーターも呼ばれます。

```RegisterController.java
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.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
@RequestMapping("admin")
public class RegisterController {
	
	@Autowired
	private RegisterValidator registerValidator;
	
	@InitBinder("registerForm")
    public void initBinderSearchForm(WebDataBinder binder) {
        binder.addValidators(registerValidator);
    }
	
	@ModelAttribute
	public RegisterForm setUpRegisterForm() {
		return new RegisterForm();
	}
	
	@RequestMapping("/index")
	public String index() {
		return "register";
	}
	
	@RequestMapping("/register")
	public String register(@Validated RegisterForm form, BindingResult result, Model model) {
		if(result.hasErrors()) {
			
			return "register";
		}		
		
		//管理者登録の処理（省略）
		
		return "complete";

	}
}

```

registerメソッドの中がかなり簡潔になったと思います。

画面イメージです。いくつか入力パターンを試してみました。
![Videotogif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2521820/c7a4bebc-26d7-51c7-6d09-ed2890ed89b9.gif)



最後まで閲覧いただきありがとうございました。
間違いなどありましたらご指摘いただけると幸いです。

#参考#
Spring Bootでチェック処理にバリデーターを利用してみた
https://www.purin-it.com/spring-boot-web-check-validator

Spring MVCにおけるフォームバリデーションの適用事例【後編】
https://qiita.com/kenhori/items/72f3821bef62a3ebd1cf