開発中にカスタムバリデーションを見かけたので、理解のために自分でも作ってみました。
以下の3つを作ります。
- シンプルなもの
- パスワードの一致チェック
- @Size的なものを自作
参考サイト
Spring カスタムアノテーションに出会った話
Spring書き込み編.独自のバリデーションを作って入力チェックをする。
Jakarta EE 8 仕様 API
まずは文字数チェックのみのシンプルなのから
簡単なのから作ります。
5文字以上入力すればOKというバリデーションです。
アノテーションを作る
@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})
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という内容にしました。
アノテーションを使う
@Data
public class Mail {
@MailCheck(message = "5文字以上入力して下さい。")
private String mail;
@MailCheck(message = "5文字以上入力して下さい。")
private String mailConfirm;
}
アノテーションをFormクラスに付けます。
今回は対象がFIELD・StringなのでStringのフィールドに付けています。
@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クラスを渡します。
<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>
メールアドレス一致チェックを作ってみる
@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 }) は対象がクラス・インタフェース・アノテーションです。
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フィールドだけに"メールが一致しません。"と表示する。
@Data
@MailSameCheck
public class Mail {
private String mail;
private String mailConfirm;
}
今度は対象が@Target({ ElementType.TYPE })(クラス・インタフェース・アノテーション)なのでアノテーションをクラスの頭に付けます。
MailController.javaは変更なし。
@Size的なものを作る
何となく作りたくなったので作ります。
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つのフィールドにアノテーションを複数付けられるようです。使い所はわかりません。
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;
}
}
@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は変更なし。
まとめ
よく使うものをカスタムバリデーションとして作っておくと便利ですね。
メールやパスワードの一致チェック、メールの2重登録チェックなんかもあると色々な所で使えそうです。