【Spring】カスタムバリデーション作ってみた

開発中にカスタムバリデーションを見かけたので、理解のために自分でも作ってみました。
以下の3つを作ります。

1. シンプルなもの
1. パスワードの一致チェック
1. @Size的なものを自作

# 参考サイト
[Spring カスタムアノテーションに出会った話](https://qiita.com/b1ueskydragon/items/07ac1f9bc8c55dffc541)
[Spring書き込み編.独自のバリデーションを作って入力チェックをする。](https://qiita.com/shibafu/items/104e920ad87fe9763b45)
[Jakarta EE 8 仕様 API](https://spring.pleiades.io/specifications/platform/8/apidocs/javax/validation/constraintvalidatorcontext)

# まずは文字数チェックのみのシンプルなのから
簡単なのから作ります。
5文字以上入力すればOKというバリデーションです。

## アノテーションを作る

```java:MailCheck.java
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MailCheckImpl.class)
public @interface MailCheck {

	String message() default "";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}
```

+ @Targetでアノテーションをつける対象を設定。
+ @Retentionでアノテーションのアノテーションをどの期間保持するかを設定。
+ @Constraintでバリデーションを実装するクラスを設定。
+ @interfaceをつけるとこのクラスをアノテーションとして使えるようになります。
+ message() エラーメッセージ。こういうの→`@MailCheck(message={"5文字以上入力してください。"})`
+ gropus()　バリデーションをグループ化する。`@MailCheck(groups={XXX.class})`
+ payload()　メタ情報を与える。アノテーションをつける対象をカテゴライズするのに使うようです。`@MailCheck(payload={XXX.Marker.class})`


```java:MailCheckImpl.java
public class MailCheckImpl implements ConstraintValidator<MailCheck, String> {

	private MailCheck mailCheck;

	//初期化処理。
	@Override
	public void initialize(MailCheck annotation) {
		this.mailCheck = annotation;
	}

	@Override
	public boolean isValid(String value, ConstraintValidatorContext context) {
		if (value.length() >= 5) {
			return true;
		}
		return false;
	}
}
```
バリデーションの実装クラスです。

+ `ConstraintValidator<A, T>` Aに作成したインターフェース（MailCheck）、Tにアノテーションをつける対象クラス（String）を指定。
+ initialize() 初期化処理。
+ isVlid() バリデーション処理を実装します。5文字以上入力したらOKという内容にしました。

## アノテーションを使う

```java:Mail.java
@Data
public class Mail {

	@MailCheck(message = "5文字以上入力して下さい。")
	private String mail;

	@MailCheck(message = "5文字以上入力して下さい。")
	private String mailConfirm;
}
```

アノテーションをFormクラスに付けます。
今回は対象がFIELD・StringなのでStringのフィールドに付けています。

```java:MailController.java
@Controller
public class MailController {

	@GetMapping("/hello")
	public String getHello(@ModelAttribute Mail mail) {
		return "hello";
	}

	@PostMapping("/mail")
	public String getPassword(Model model, @Valid Mail mail, BindingResult result) {

		if (result.hasErrors()) {
			return "hello";
		}
		return "success";
	}
}
```
引数で@Valid Formクラスを渡します。

```html:hello.html
<body>
<form method="POST" action="/mail" name="nameform">
		<p>・メール <input type="text" name="mail"> <span
			th:if="${#fields.hasErrors('mail.mail')}"
			th:errors="*{mail.mail}"></span>

		<p>・メール確認<input type="text" name="mailConfirm">
		<span th:if="${#fields.hasErrors('mail.mailConfirm')}"
			th:errors="*{mail.mailConfirm}"></span>

		<br><input type="submit" value="送信">
	</form>
</body>
```

結果はこんな感じ↓
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566570/f2d7569b-17be-abd7-2010-5aee52923d4e.png)

# メールアドレス一致チェックを作ってみる

```java:MailSameCheck.java
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MailSameCheckImpl.class)
public @interface MailSameCheck {

	String message() default "デフォルトメッセージ";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

}
```

@Target({ ElementType.TYPE }) は対象がクラス・インタフェース・アノテーションです。

```MailSameCheckImpl.java
public class MailSameCheckImpl implements ConstraintValidator<MailSameCheck, Mail> {

	private MailSameCheck mailSameCheck;

	@Override
	public void initialize(MailSameCheck annotation) {
		this.mailSameCheck = annotation;
	}

	@Override
	public boolean isValid(Mail value, ConstraintValidatorContext context) {

		//デフォルトのエラーメッセージを無効にする。
		context.disableDefaultConstraintViolation();

		if (value.getMail().isBlank() | value.getMailConfirm().isBlank()) {

			//時間的な制約を付けたい時に使うらしい。今回は紹介のみで使いません。
			ClockProvider cp = context.getClockProvider();

			//エラーメッセージの変更　＆　メッセージ表示対象をmailのみにする
			context.buildConstraintViolationWithTemplate("メールが入力されていません。")
					.addPropertyNode("mail").addConstraintViolation();
			return false;
		}

		if (!value.getMail().equals(value.getMailConfirm())) {

			//エラーメッセージの変更　＆　メッセージ表示対象をmailConfirmのみにする
			context.buildConstraintViolationWithTemplate("メールが一致しません。")
					.addPropertyNode("mailConfirm").addConstraintViolation();
			return false;
		}
		return true;
	}
}
```

+ buildConstraintViolationWithTemplate() エラーメッセージの変更。他にも色々できるっぽいですが、よくわかりませんでした。詳しい方よかったら教えてください。
+ addPropertyNode() エラーメッセージを出力したいフィールド名を指定する。
+ addConstraintViolation()　ConstraintViolation を作成するには、このメソッドを呼び出す必要があります。

今回やっているのは
メールアドレスが未入力の時はmailフィールドだけに"メールが入力されていません。"と表示する。
メールアドレスが一致しない時はmailConfirmフィールドだけに"メールが一致しません。"と表示する。

```java:Mail.java
@Data
@MailSameCheck
public class Mail {
	private String mail;
	private String mailConfirm;
}

```
今度は対象が@Target({ ElementType.TYPE })（クラス・インタフェース・アノテーション）なのでアノテーションをクラスの頭に付けます。
MailController.javaは変更なし。

実行するとこんな感じになります。↓
![mail2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566570/0187f11b-2244-f2cf-e79d-577175ef3174.png)

# @Size的なものを作る
何となく作りたくなったので作ります。

```java:Sizeppoino.java
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;

import com.example.demo.Sizeppoino.List;

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
@Constraint(validatedBy = SizeppoinoImpl.class)
public @interface Sizeppoino {

	String message() default "1文字以上10文字以下で入力してください。";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	int max() default 10;

	int min() default 1;

	//1つのフィールドにアノテーションを複数付けられる
	@Target({ ElementType.FIELD })
	@Retention(RetentionPolicy.RUNTIME)
	@interface List {

		Sizeppoino[] value();

	}
}
```

+ int max() 入力文字の上弦設定用。
+ int min() 入力文字の下弦の設定用。
+ @interface List{A[] value();} で1つのフィールドにアノテーションを複数付けられるようです。使い所はわかりません。

```java:SizeppoinoImpl.java
public class SizeppoinoImpl implements ConstraintValidator<Sizeppoino, String> {
	int max;
	int min;

	@Override
	public void initialize(Sizeppoino annotation) {
		this.max = annotation.max();
		this.min = annotation.min();
	}

	@Override
	public boolean isValid(String value, ConstraintValidatorContext context) {
		if (value.length() > this.max | value.length() < this.min) {
			return false;
		}
		return true;
	}
}
```

```java:Mail.java
@Data
public class Mail {

	@Sizeppoino(min = 3, max = 15, message = "3文字以上15文字以下で入力してください")
	@Sizeppoino(min = 6, max = 20, message = "6文字以上20文字以下で入力してください")
	private String mail;

	@Sizeppoino(min = 3, max = 15, message = "3文字以上15文字以下で入力してください")
	private String mailConfirm;
}
```

MailController.javaは変更なし。

実行結果↓
![mail3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/566570/ac83e1a7-f437-8989-ffae-1e7653d67335.png)

# まとめ
よく使うものをカスタムバリデーションとして作っておくと便利ですね。
メールやパスワードの一致チェック、メールの2重登録チェックなんかもあると色々な所で使えそうです。