JSR303 Bean Validation 戻る
JSR303とは?

JSR 303はJava Beanオブジェクトのためのバリデーション機構を実現する目的で発足し、実装はJDK1.6(1.5?)から。

JSRの解説になってしまいますが、ServletもJSRで定義されており、BeanValidationもServlet同様にAPIの概要(Java的表現でいうとインタフェース)を定義したもの。定義後しばらく放置。その後誰かが作成した実装を標準実装に取り込むことがあります。

今回のBeanValidationではHibernateValidatorをJavaの標準実装としています。SpringにおけるValidation機能もHibernateを利用しています。他の実装で知名度が高いものは「Apache Foundation」「Jakarta Commons」の「Commons Validator」が存在します。

Bean Validation に用意されている精査

Bean Validationでは以下のようなアノテーションが用意されています。後述しますが自作も可能です。

Validation用アノテーション一覧(利用頻度の高いもの)
No. 名称 概要
1 @Null, @NotNull 項目がnullである(もしくはnullでない)ことを検証
2 @Digits 項目が数値であり設定された範囲内
3 @DecimalMin, @DecimalMax, @Min, @Max 項目が数値であり、設定値より小さい(大きい)
4 @Future, @Past 項目が日付であり設定値より未来(過去)
5 @Pattern 項目が正規表現のパターンにマッチしている
6 @Size 項目が設定値のサイズに収まっている
7 @AssertTrue, @AssertFalse 項目がTrue(False)

* @Null, @NotNull 以外は、項目がnullの時には実行されません。

Bean Validation の実装イメージ

Bean Validationではこちらのソースのように実装できます。アノテーションを用いて実装します。インタフェースにも記述可能です。

Validator#validateにて第1引数は精査するインスタンス、第2引数(以降も)は精査ルールの記述してあるインタフェースを指定します。この時第2引数を指定しなければ精査するインスタンスのクラス(implementsのインタフェースも)の精査を実行。第2引数を指定していればそのインタフェースに記述されている精査のみを実行します(精査対象のインスタンスが、指定したインタフェースをimplementsしていなければなりません)

単純な実装例 public class PersonImpl implements Serializable { @NotNull // Fieldに記述可能 private String personId; private String personName; public String getPersonId(){ return this.personId; } @NotNull // メソッドに記述可能 public String getPersonName(){ return this.personName; } }
インタフェースによる実装例 public interface IPerson implements Serializable { @NotNull(message="id.required") public String getPersonId(); // 複数可 @NotNull(message="name.required") @Size(min=10,max=50 ,message="name.len {min}_{max}") public String getPersonName(); } public class PersonImpl implements IPerson { // 内容は省略 }
実際に呼び出す IPerson dto = new PersonImpl(); dto.setPersonName("abc"); ValidatorFactory f = Validation.buildDefaultValidatorFactory(); Validator v = f.getValidator(); Set<ConstraintViolation<IPerson>> violations = v.validate(dto, IPerson.class); // インタフェースを複数指定可 for (ConstraintViolation<IPerson> violation : violations) { System.out.print(violation.getPropertyPath()); System.out.print("["); System.out.print(violation.getInvalidValue()); System.out.print("]="); System.out.print(violation.getMessage()); System.out.print("["); System.out.print(violation.getMessageTemplate()); System.out.println("]"); } もしくは Set<ConstraintViolation<IPerson>> violations = v.validate(dto); こちらの場合PersonImplに記述されている精査を実施
実際に呼び出した結果 personId[null]=id.required[id.required] personName[abc]=name.len 10_50[name.len {min}_{max}]
Validateアノテーションを自作
組合せ実装
組合せ実装のサンプル @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) @Constraint(validatedBy = {}) @NotNull(message = "validate.personId.required") @Size(min = 12, max = 12 , message = "validate.personId.size") @Pattern(regexp = "[0-9]*" , message = "validate.personId.invalid") public @interface APersonId { String message() default "validate.personId"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) @interface List { APersonId[] value(); } }

ID(12文字,数字のみ)を組み合わせで実装してみました。

コチラのアノテーションは「APersonId」という名称なので利用する際は「@APersonId」と記述します。フィールド、メソッド共に記述可能です。

独自実装
独自実装のサンプル @Documented @Constraint(validatedBy = PhoneValidator.class) @Target({ ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface APhone { String message() default "validate.phone"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; boolean onlyNumber() default false; public class PhoneValidator implements ConstraintValidator<APhone, String> { public void initialize(APhone aPhone) { this.onlyNumber = aPhone.onlyNumber(); } private boolean onlyNumber; public boolean isValid(String phone, ConstraintValidatorContext context) { if (phone == null) { return true; } if (onlyNumber) { return phone.matches("[0-9]*"); } else { return phone.matches("[0-9()-]*"); } } } }

電話番号を自作してみました。既存の組み合わせでなく、自作精査を行うには「@Constraint」アノテーションの「validatedBy」にてクラスを指定します。「validatedBy」には1つだけクラスを指定します。今回はインナークラスとしimplements ConstraintValidator しました。

初期値はinitializeメソッドで取得しisValidの第1引数で精査する値を取得できるようです。

自作すると考えられる処理は「相関項目チェック(パスワードと確認用等)」や「業務ロジック(未使用のIDかどうか等)」といった処理を記述することはできそうだが、現時点では不明。

項目間精査
アノテーション package com.hogehoge; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import javax.validation.Payload; import com.hogehoge.Same.ConfirmValidator; @Documented @Constraint(validatedBy = { ConfirmValidator.class }) @Target({ TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) public @interface Same { public class ConfirmValidator implements ConstraintValidator<Same, Object> { private String field1; // 項目名1 private String field2; // 項目名2 private String message; public void initialize(Same constraintAnnotation) { message = constraintAnnotation.message(); field1 = constraintAnnotation.field1(); field2 = constraintAnnotation.field2(); } public boolean isValid(Object value, ConstraintValidatorContext context) { Object val1 = field1を用いて値を取得する処理; Object val2 = field2を用いて値を取得する処理; if (val1.equals(val2)) { return true; } else { context.disableDefaultConstraintViolation(); context .buildConstraintViolationWithTemplate (message).addConstraintViolation(); return false; } } } @Target({ TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented public @interface List { Same[] value(); } String field1(); String field2(); Class<?>[] groups() default {}; String message() default "{validate.diff}"; Class<? extends Payload>[] payload() default {}; }
利用例 package com.hogehoge; @Same(message = "{diff.confirm}", field1 = "password", field2 = "confirmPassword") public class PasswordImpl implements Serializable { private String confirmPassword; private String password; // 不要なプロパティやgetter等は省略 }
プロパティファイル diff.confirm=パスワードとパスワード(確認)が一致しません。

強引ではありますが、項目間精査のアノテーションを作成しました。

項目間精査なので「@Target({ TYPE, ANNOTATION_TYPE })」にしました。

よりよい方法がある場合、教えてください。