親バカエンジニアのナレッジ帳

webのエンジニアをやっており、日頃の開発で詰まったことや書き残しておきたいことを載せています。

Spring BootでBean Validation (3) バリデーション処理を自作


ti-tomo-knowledge.hatenablog.com
ti-tomo-knowledge.hatenablog.com

上記2つのような流れで、BeanValidationにおける基本的なバリデーションの流れを見ていきましたが、
これらはあくまでBeanValidationやHibernateで事前に用意されたものです。
実際にサービスを作り始めると、チェックしなければならないパターンはもっと多く、また、項目間の値の関係チェック(例えば大小関係)をしなければいけないこともあるでしょう。

今回は、そのようなパターンを2つ用意して、
バリデーションの実装方法について深堀りしていきたいと思います。

1. 項目の値が一意である必要がある場合
例えばアカウント作成時のメールアドレスが挙げられますね。
基本的にメールアドレスは1つのサービスで同一のものが使えないことが多いでしょう。
よって、アカウント作成時に登録済みのメールアドレスを登録しようとしたら、エラーを出す必要があります。

まずは自作のバリデーションを行うためのファイルを2つ用意します。
今回は、Unusedというアノテーションクラスと、実際にバリデーションチェックを行うUnusedValidatorというクラスです。

Unused

package パッケージ名;
 
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
 
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@Documented
@Constraint(validatedBy = {UnusedValidator.class}) // ここは後に作るバリデーションクラスです。
@Target({FIELD}) // 項目に対してバリデーションをかける場合はFIELDを選びます。
@Retention(RUNTIME)
public @interface Unused {
 
    String message() default "すでに登録済みのメールアドレスです"; // エラーメッセージです。アノテーションの引数にmessageを設定していない場合は、この値が出力されています。
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    @Target({FIELD})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        Unused[] value(); // インターフェース名[] value()としておいてください
    }
}

UnusedValidator

package パッケージ名;
 
import test.Account;
import test.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
 
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
public class UnusedValidator implements ConstraintValidator<Unused, String> {
 
    @Autowired
    AccountService accountService; // ここは各自の設定に合わせてください。
 
    public void initialize(Unused constraintAnnotation) {
    }
 
    public boolean isValid(String value, ConstraintValidatorContext context) {
 
        Account account = accountService.findByEmail(value); // ここのvalueは入力値になります
        if(account == null){
            return true;
        }
        return false;
    }
}

ここでは詳細は省きますが、AccountServiceというサービスクラス内でメールアドレスからユーザを取得できるようにしてあります。
上記で返り値がfalseだった場合、Unusedクラスのmessageに設定しているメッセージが表示されるようになります。

最後にアノテーションの配置ですが、他のバリデーションと同様に、
入力値をチェックしたい項目の上に作成したアノテーションを設置してください。

@Unused
private String email;

もちろんgroupsなどのパラメータも設定できますよ。



2. 項目間の一致を比較する場合
アカウント作成時のメールアドレスが挙げられますね。
誤入力を避けるために、入力したパスワードと同じものを再度入力させるパターンも多いでしょう。
よって、アカウント作成時に1つ目と2つ目のメールアドレスの値が異なった場合、エラーを出す必要があります。

先ほどと同様に、まずは自作のバリデーションを行うためのファイルを2つ用意します。
今回は、Confirmというアノテーションクラスと、実際にバリデーションチェックを行うConfirmValidatorというクラスです。

Confirm

package パッケージ名;
 
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
 
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
 
@Documented
@Constraint(validatedBy = {ConfirmValidator.class})
@Target({TYPE})
@Retention(RUNTIME)
public @interface Confirm {
 
    String message() default "2つの入力値が異なります";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    @Target({TYPE})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        Confirm[] value();
    }
}

ConfirmValidator
※注意して欲しいのは、「ConstraintValidator」の箇所になります。
ここはどの型で引数を受け取るかというところですが、項目間での比較はフォーム全体にバリデーションをかけることになりますので、型はObjectにしてください。ここをObjectにしないと、「No validator could be found for constraint...」などとエラーが発生します。

package パッケージ名;
 
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.StringUtils;
 
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Objects;
 
public class ConfirmValidator implements ConstraintValidator<Confirm, Object> {
 
    private String field1;
    private String field2;
    private String message;
 
    public void initialize(Confirm constraintAnnotation) {
        field1 = "email1"; // 下記のisValidで使うので、ここでメンバ変数に項目名を入れておいてください。
        field2 = "email2"; // ここも同じ
        message = constraintAnnotation.message(); // Confirmクラスのmessage()です。isValidで使用します。
    }
 
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        BeanWrapper beanWrapper = new BeanWrapperImpl(value);
        Object field1Value = beanWrapper.getPropertyValue(field1);
        Object field2Value = beanWrapper.getPropertyValue(field2);
 
        if (Objects.equals(field1Value, field2Value)) {
            return true;
        } else {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(message) // このようにmessageの設定を入れないと、エラー内容が出力されません。
                    .addPropertyNode(field2Value).addConstraintViolation(); // field2の箇所にエラー内容が出力されるようにしています。
            return false;
        }
    }
}

最後にアノテーションの配置ですが、ここは注意です。
特定の項目のチェックではなく、フォーム全体の中でemail1とemail2の値をチェックするので、
Serializableクラスの上に、アノテーションを設置してください。

@Confirm
public class TestForm implements Serializable {
 
    private String email1;
 
    private String email2;
}

こうしないと、フォーム全体から値を引っ張ることができないので、
email1もemail2も取得することができません。