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
について解説する。
[前多 賢太郎]