エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(24) - Bean Validationでカスタム制約を作る

Bean Validation では独自のバリデーションを行うカスタム制約を作成できる。今回はカスタム制約を作成する方法を紹介する。

カスタム制約は以下の2つの方法で作成可能だ。

  • 既存の制約を組み合わせる。
  • 独自のバリデーション処理を作成する。

方法1:既存の制約を組み合わせる

1つのプロパティに、組み込みの制約アノテーションを複数指定することは多々あり、その中にはよく使う組み合わせもあるだろう。 そのような場合には、複数の制約アノテーションをまとめたアノテーションを作成できる。

たとえば、次のコードではPersonNameという名前のアノテーションを定義している。

/**
 * Nullでない、10文字以内、スペースを一つ含む制約
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
// 制約アノテーションの定義。validateByはこの場合は空でよい。
@Constraint(validatedBy = {})
// 以降、組み合わせるアノテーションを指定
@NotNull
@Size(min=1, max = 10)
@Pattern(regexp = "^\\S+ \\S+$")
public @interface PersonName {
    
    Class<?>[] groups() default {};
    String message() default "未使用";
    Class<? extends Payload>[] payload() default {};
    
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        PersonName[] value();
    }
}

(2017/4/18 説明とコードの制約値の設定に齟齬があったため、コードを修正しました。)

制約アノテーションには、@Constraintアノテーションを付与する。(validateBy属性にはこのアノテーションで実行するバリデーションを指定するが、今回はこの制約自体の処理は無いため空配列を指定している。)

そして、このアノテーション自体に、@NotNullなどの制約アノテーションを付与する。ここで付与したアノテーションがバリデーション時にチェックされる。

また、前回説明したとおり、制約アノテーションは、message, groups, payloadおよび、Listを設定しなければ実行時エラーとなる。

このアノテーションを以下のように JavaBeans に設定すると、バリデーションが可能となる。

public class Hoge {
    @PersonName
    private String name;
}

実行される内容は、@PersonNameに定義されている@NotNull, @Size, @Patternを個々に書いたものと全く同じで、それぞれの制約チェックでエラーとなった内容が通知される。 @PersonName制約自体のエラーが発生するわけではないので、@PersonNameに設定したメッセージは使用されない。

方法2:独自のバリデーション処理を作成する

次に、文字列のバイト長をチェックする独自のバリデーションを作成してみよう。

まずは@Byteという名前のアノテーションを定義する。

/**
 * 文字セットに基づくバイト長制約
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
// バリデーション処理のクラスを設定
@Constraint(validatedBy = {ByteValidator.class})
public @interface Byte {
    // 独自属性の定義
    int min() default 0;
    int max() default Integer.MAX_VALUE;
    String charset() default "UTF-8";
    
    Class<?>[] groups() default {};
    String message() default 
            "{charset}で{min}バイトから{max}バイトにしてください。";
    Class<? extends Payload>[] payload() default {};
    
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Byte[] value();
    }
}

ここでは、チェック対象の文字セットや最小および最大桁数などを属性として設定している。また、@ConstraintアノテーションvalidateByにはByteValidator.classというクラスを指定している。ここで指定しているのは、この制約で実行するバリデーション処理である。

ByteValidatorのコードを以下に示す。

/**
 * バイト制約のバリデーター
 */
public class ByteValidator 
        implements ConstraintValidator<Byte, String> {

    private int min;
    private int max;
    private String charset;
    
    /** アノテーションから情報を受け取る */
    @Override
    public void initialize(Byte annotation) {
       min = annotation.min();
       max = annotation.max();
       charset = annotation.charset();
    }

    /** 
     * 検証処理
     * 
     * @param value 入力値
     * @param ctx コンテキスト
     * @return true - 制約を満たす。 false - 制約を満たさない。
     */
    @Override
    public boolean isValid(String value, 
            ConstraintValidatorContext ctx) {
        
        // nullは対象外
        if (value == null){return true;}
        
        byte[] b = value.getBytes(Charset.forName(charset));
        
        return min <= b.length && b.length <= max;
        
    }
}

バリデータは、ConstraintValidator<A,T>を実装する必要がある。Aは制約アノテーションで、Tは入力値の型を示す。それぞれは実装するメソッドのinitializeおよびisValidの引数の型となる。

initializeでは JavaBeans のプロパティに設定された制約アノテーションの属性値を取得できる。

isValidで検証を行い、戻り値がfalseなら、検証失敗としてConstraintVaiolationが生成される。

このようにしておくと、以下のように JavaBeans に設定可能となる。

public class Hoge {
    @NotNull
    @Byte(charset = "Windows-31J", min=2, max=10)
    private name;
}

上記のプロパティに、例えば"ああああああ"などを設定してバリデーションを実行すると、以下のようになる。

@Test
public void 検証失敗() {
    
    Set<ConstraintViolation<Sample2>> res = 
            validator.validateValue(
                Sample2.class, "name", "ああああああ");
    
    assertThat(res.size(), is(1));
    assertThat(res.iterator().next().getMessage(),
            is("Windows-31Jで2バイトから10バイトにしてください。"));
}

相関チェック

getterメソッドにも制約アノテーションが付与できることは前回説明した。 これを利用すると、相関チェックのような複数プロパティの入力チェックが可能となる。

事前準備として以下のようなクラスを用意する。

public class Pair<T,U> {
    
    public final T _1;
    public final U _2;
    
    public Pair(T t, U u) {
        _1 = t;
        _2 = u;
    }
    
    public T getOne(){
        return _1;
    }
    
    public U getTwo(){
        return _2;
    }
}

JavaBeans に、上記のPairを返す getter メソッドを用意する。

public class Hoge {
    
    private String password;
    private String confirmPassword;
    
    @Equal // 両方の値が同じであるカスタム制約
    public Pair<String,String> getPasswordPair() {
       return new Pair(password, confirmPassword);
    }
}

これで、@Equal制約のバリデーションの入力値として、Pairオブジェクトが使用可能となる。

@Equalアノテーションの定義は他とほぼ同じなので割愛するが、Pairに対応するバリデータは以下のようになる。

public class EqualValidator implements
        ConstraintValidator<Equal, Pair<?, ?>> {

    @Override
    public void initialize(Equal an) {
    }

    @Override
    public boolean isValid(Pair<?, ?> value,
            ConstraintValidatorContext context) {
        
        return value._1.equals(value._2);
    }
}

注意点

このような、getter に設定した制約は、Bean Validation を組み込んだフレームワークによっては動作しないことがある。

例えば、JPA の場合はValidator#validateによるクラス単位のバリデーションを行うので動作する。しかし、JSF の場合、1つの入力値に対してValidate#validatePropertyによるプロパティ単位でのバリデーションを行うため、入力値の存在しないプロパティのバリデーションは行ってくれない。

相関チェックを実装するにあたって、フレームワークが Bean Validation をどのように使用するのかを調べた方がよいだろう。

CDI との関連

Java EE7 より カスタムバリデーター内で CDI Beanのインジェクションが可能となった。

これにより、データベースやファイルなどの使用が簡単になる。

EntityManager をインジェクションして、データベースに保持している日付と比較を行う例を示す。

なお、CDI を使用したバリデータを使用する場合、javax.validation.Validatorインスタンスも、インジェクション経由で取得しなければならない。

/** 入力された日付が、データベースに保持している基準日より後かを検証する */
@Dependent
public class DBValidator implements 
       ConstraintValidator<DBValid, Date>{
    
    // Producer経由でインジェクション
    @Inject
    private EntityManager em;
    
    @Override
    public void initialize(DBValid valid) {

    }
    @Override
    public boolean isValid(Date value,
            ConstraintValidatorContext context) {
        
        // SETTINGテーブルの、BASEDATE列を取得。
        Date base = em.find(Setting.class, "data")
                      .getBaseDate();
        
        return value.after(base);
    }
}

まとめ

カスタム制約の作成方法を記述した。一度カスタム制約を作成すると、既存の組み込み制約と同じように利用できる。

また、カスタム制約は自作せずとも、OSS で公開されているものもある。 実際、Bean Validation の参照実装である、 Hibernate Validator 自体にも、@Email, @CreditCardNumber, @URLのような利用頻度の高い制約が定義されている。

次回は、制約アノテーションList, groupsについて解説する。

[前多 賢太郎]