JSR 303 Bean Validationで遊んでみるよ!

その名のとおりJavaBeansの為のValidationの仕様であるJSR303ですが、近頃でもないですがHibernateはもちろん、その他SpringやOvalなどの周辺フレームワークの対応が進んでずいぶん使いやすくなってきました。

ところでアプリケーション作っててValidationの仕組みって毎回悩みませんか?私がJavaでWebアプリケーションつくりはじめた頃なんかだとStruts1.xが全盛期でvalidation.xml、validation-rule.xmlとか使って書いてましたが(今考えれば二度とやりたくないですねw)、今でも毎回どのチェックをどのレイヤ(アプリケーションレイヤ?ドメインレイヤ?)に持たせるかとか、データストアに問い合わせしないといけないValidationって画面の入力だけでチェックできるのとどう管理しようかなとか、色々と悩むこともしばしばです。最近DDD的に色々考える節もあり、基本的には検証ルールはドメインのそのEntity自身が知っているべきだと思うので、なるだけそこに検証ルールを集約させるようにしています。とは言いつつもその時使用してるフレームワークのValidationの仕組みに依存する部分が結構あるので、その時の状況次第なのですが。

と前置きが長くなりましたが、とりあえず今回はWebアプリとかO/Rマッパとか周辺の仕組みは置いていて、JSR303自体で遊んでみたいと思います。

下準備

まず必要なライブラリです。その辺は適当にMavenで。

  ...
  <repositories>
    <repository>
      <id>JBoss Repo</id>
      <name>JBoss Repo</name>
      <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
    </repository>
  </repositories>

  <dependencies>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>4.1.0.Final</version>
    </dependency>
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
      <version>1.0.0.GA</version>
    </dependency>
    <!-- Logging -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.6.1</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.6.1</version>
    </dependency>
   <!-- Test -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  ...

今回はJSR303の実装にhibernate-validatorを使用します。どうも最新のものはセントラルリポジトリにないみたいなのでjbossのリポジトリ追加してます。junitは動作確認使うのでおまけで。

はじめの一歩

基本的な使い方は対象のBeanに検証用のアノテーションを付与してValidatorに食わせます。
最初なので簡単なBeanで。

import javax.validation.constraints.AssertTrue;
public class Bean {
    @AssertTrue
    private boolean aaa;
    public boolean isAaa() {
        return aaa;
    }
    public void setAaa(boolean aaa) {
        this.aaa = aaa;
    }
}

@AssertTrueが検証用のアノテーションです。この例だとaaaがtureでなければ検証エラーです。
では実際にValidatiorを動かしてみます。

    @Test
    public void 最初の一歩() throws Exception {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        Bean bean = new Bean();
        bean.setAaa(false);
        Set<ConstraintViolation<Bean>> violations = validator.validate(bean);
        assertThat("aaaはtrueじゃないといけないので一つエラーがある", violations.size(), is(1));
        for (ConstraintViolation<Bean> violation : violations) {
            System.out.println(violation.getMessage());
        }
    }

ValidatorFactoryつくってValidator取得してvalidateする感じです。validateするとConstraintViolationのSetが返ってくるのでサイズ0だとエラーなし、エラーがあった場合はその分のConstraintViolationが返ってきます。ConstraintViolationからはgetMessageでエラーメッセージや、テンプレートのメッセージやら、エラーになった値とか、内部で他のBean持ってるなどの階層構造がある場合は、発生元のBeanだったりルートのBeanだったり色々とれます。
ちなみに今回の出力はこんな感じです。

must be true

デフォルトであるValidate用のアノテーション

だいたい使い方がわかったと思うので、デフォルトである各アノテーションの使い方を見てきます。javax.validation.constraintsには以下のアノテーションが定義されています。

  • @AssertFalse、@AssertTrue
  • @DecimalMax、@DecimalMin
  • @Max、@Min
  • @Digits
  • @Future、@Past
  • @Null、@NotNull
  • @Pattern
  • @Size

なんとなく名前から使い方が想像できますね。

@AssertFalse、@AssertTrue

Booleanとbooleanで使えます。なお該当アノテーションJavadocを見ればどんなTypeで使えるかだいたい書いてあるので忘れたら、それを参照するといいと思います。
最初の例で使用したものですが、そのまんまです。

public class AssertTrueAndFalseTest extends ValidationTestBase {
    @AssertTrue
    boolean mustTrue;
    @AssertFalse
    boolean mustFalse;
    @Test
    public void バリデーションしてみる() throws Exception {
        AssertTrueAndFalseTest bean = new AssertTrueAndFalseTest();
        bean.setMustFalse(true);
        bean.setMustTrue(false);
        assertThat(validator.validate(bean).size(), is(2));
    }
    // getter setter
}

validatorの生成(ValidationTestBaseなかで生成しています)とgetter setterは省略しています。あと、テストのたびにBeanクラス定義するのが面倒になったので一緒のクラスでw

@DecimalMax、@DecimalMin

BigDecimal、BigInteger、String、byte、short、int、longで使えます。
Decimalの最大値と最小値を定義して使用できます。

public class DecimalMaxAndMinTest extends ValidationTestBase {
    @DecimalMax("10")
    String stringValue;
    @DecimalMin("-10.0")
    int intValue;
    @DecimalMin("3.00")
    @DecimalMax("4.00")
    BigDecimal bigDecimalValue; 
    @Test
    public void バリデーションしてみる() throws Exception {
        DecimalMaxAndMinTest bean = new DecimalMaxAndMinTest();
        bean.setStringValue("100"); // 10より大きいからNG
        bean.setIntValue(-9); // -10.0より大きいからOK
        bean.setBigDecimalValue(new BigDecimal("3.5")); // 範囲内だからOK
        assertThat(validator.validate(bean).size(), is(1));
    }
    // getter setter
}
@Max、@Min

@DecimalMaxと@DecimalMinとだいたい一緒です。

public class MaxAndMinTest extends ValidationTestBase {
    @Max(10)
    String stringValue;
    @Min(-10)
    int intValue;
    @Min(3)
    @Max(4)
    BigDecimal bigDecimalValue;
    @Test
    public void バリデーションしてみる_02() throws Exception {
        MaxAndMinTest bean = new MaxAndMinTest();
        bean.setStringValue("11"); // 10より大きいからNG
        bean.setIntValue(-9); // -10より大きいからOK
        bean.setBigDecimalValue(new BigDecimal("3.5")); // 範囲内だからOK
        assertThat(validator.validate(bean).size(), is(1));
    }
    // getter setter
}
@Digits

BigDecimal、BigInteger、String、byte、short、int、longで使えます。integeに整数部の桁数、fractionに小数部の桁数を指定して使います。

public class DigitsTest extends ValidationTestBase {
    @Digits(integer = 3, fraction = 1)
    String stringValue;
    @Digits(integer = 4, fraction = 0)
    int intValue;
    @Digits(integer = 4, fraction = 3)
    BigDecimal bigDecimalValue;
    @Test
    public void バリデーションしてみる() throws Exception {
        DigitsTest bean = new DigitsTest();
        bean.setStringValue("101.000"); // 少数以下が3桁超えてるのでNG
        bean.setIntValue(1000000000); // 整数部が4桁超えてるのでNG
        bean.setBigDecimalValue(new BigDecimal("1111.123")); // 桁数内なのでOK
        assertThat(validator.validate(bean).size(), is(2));
    }
    // getter setter
}
@Future、@Past

DateとCalendarで使えます。@Futureは未来、@Pastは過去の時間でないといけません。個人的にはあまり利用シーンがないような気もw

public class FuturePastTest extends ValidationTestBase {
    @Past
    Date date;
    @Future
    Calendar calendar;
    @Test
    public void バリデーションしてみる() throws Exception {
        FuturePastTest bean = new FuturePastTest();
        bean.setDate(new Date(System.currentTimeMillis() - 10000)); // 過去時間なのでOK
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_MONTH, -2); // 未来じゃないのでのNG
        bean.setCalendar(calendar);
        assertThat(validator.validate(bean).size(), is(1));
    }
    // getter setter
}
@Null、@NotNull

値がNullか、もしくはNotNullであるかチェックできます。オブジェクトなら何でも使えます。Nullのチェックなんて必要なのかと思いますが後で記述するgroupsというのを使うとそれなりに意味を持ちます。

public class NullAndNotNullTest extends ValidationTestBase {
    @Null
    Object thisIsNull;
    @NotNull
    Object thisIsNotNull;
    @Test
    public void バリデーションしてみる() throws Exception {
        NullAndNotNullTest bean = new NullAndNotNullTest();
        bean.setThisIsNotNull(null); // nullなのでNG
        bean.setThisIsNull(new Object()); // nullじゃないのでNG
        assertThat(validator.validate(bean).size(), is(2));
    }
    // getter setter
}

あと注意点としてはこのアノテーションがあるということは、その他の検証アノテーションはnullの場合、基本的にはチェックを行いません(無条件でtrueになる)。だいたい各アノテーションjavadoc

<code>null</code> elements are considered valid.

とか書いてあります。

@Pattern

Stringだけで使えます。正規表現のチェックを行います。オプションでflagsなどを指定することも可能です。

public class PatternTest extends ValidationTestBase {
    @Pattern(regexp = "hoge")
    String hoge;
    @Pattern(regexp = "bar")
    String bar;
    @Test
    public void バリデーションしてみる() throws Exception {
        PatternTest bean = new PatternTest();
        bean.setHoge("bar"); // hogeじゃないからNG
        bean.setBar("bar"); // barだからOK
        assertThat(validator.validate(bean).size(), is(1));
    }
    // setter getter
}
@Size

String、Collection、Map、Arrayで使えます。型ごとに少し意味が異なってStringの場合はlength()、Collection、Mapの場合はsize()、Arrayの場合はlengthで評価されます。minとmaxが設定できてminは0、maxはデフォルトでInteger.MAX_VALUEが設定されています。

public class SizeTest extends ValidationTestBase {
    @Size(min = 10)
    String str;
    @Size(min = 3, max = 6)
    List<Integer> list;
    @Size
    Map<String, Integer> map;
    @Size(max = 100)
    int[] array;
    @Test
    public void バリデーションしてみる() throws Exception {
        SizeTest bean = new SizeTest();
        bean.setArray(new int[] { 1, 2, 3 }); // 100こ以下だからOK
        bean.setList(Arrays.asList(1, 2, 3, 4)); // 3から6こ以内だからOK
        bean.setMap(new HashMap<String, Integer>()); // minが0と同じでOK
        bean.setStr("hoge"); // 10桁以下だからNG
        assertThat(validator.validate(bean).size(), is(1));
    }
    // getter setter
}

メッセージをカスタマイズする

クラスパスにValidationMessages.propertiesというファイルを作成して以下のように記述します。

mykey={min}から{max}ではないな

そして以下のようなテストを実施してみます。

public class MessageTest extends ValidationTestBase {
    // 全部10桁以下でNG
    @Size(min = 10)
    String defaultMessage = "hoge";
    @Size(min = 10, message = "サイズが{min}と{max}の間ではないよ")
    String directMessage = "hoge";
    @Size(min = 10, message = "{mykey}")
    String fromPropMessage = "hoge";
    @Test
    public void エラーにしてメッセージをだす() throws Exception {
        Set<ConstraintViolation<MessageTest>> violations = validator.validate(this);
        for (ConstraintViolation<MessageTest> violation : violations) {
            System.err.println(violation.getMessage());
        }
    }

出力結果はこんな感じです。

10から2147483647ではないな
サイズが102147483647の間ではないよ
size must be between 10 and 2147483647

message属性にメッセージ設定をするとその値が出力されます。その際はアノテーションに設定してる属性を{}で囲むと、その設定値が埋めこまれます。残念なのはエラーになった値が埋め込めないことですね。。
また全部を{}で囲むとメッセージのキーになります。これはmykeyという値でValidationMessages.propertiesからメッセージを取得しています。
message属性を設定しないと検証アノテーションに定義されているmessage属性の値でメッセージを出力します。@Sizeアノテーションでは以下のように定義されているので

String message() default "{javax.validation.constraints.Size.message}";

javax.validation.constraints.Size.messageというキーでリソースファイルからメッセージを取得しています。デフォルトのメッセージはValidationAPIの実装に含まれていて、今回だとHibernate内のorg.hibernate.validatorパッケージの中にValidationMessages.propertiesが定義されていて以下のように記述されています。

javax.validation.constraints.AssertFalse.message=must be false
javax.validation.constraints.AssertTrue.message=must be true
javax.validation.constraints.DecimalMax.message=must be less than or equal to {value}
javax.validation.constraints.DecimalMin.message=must be greater than or equal to {value}
javax.validation.constraints.Digits.message=numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
javax.validation.constraints.Future.message=must be in the future
javax.validation.constraints.Max.message=must be less than or equal to {value}
javax.validation.constraints.Min.message=must be greater than or equal to {value}
javax.validation.constraints.NotNull.message=may not be null
javax.validation.constraints.Null.message=must be null
javax.validation.constraints.Past.message=must be in the past
javax.validation.constraints.Pattern.message=must match "{regexp}"
javax.validation.constraints.Size.message=size must be between {min} and {max}
org.hibernate.validator.constraints.Email.message=not a well-formed email address
org.hibernate.validator.constraints.Length.message=length must be between {min} and {max}
org.hibernate.validator.constraints.NotBlank.message=may not be empty
org.hibernate.validator.constraints.NotEmpty.message=may not be empty
org.hibernate.validator.constraints.Range.message=must be between {min} and {max}
org.hibernate.validator.constraints.URL.message=must be a valid URL
org.hibernate.validator.constraints.CreditCardNumber.message=invalid credit card number
org.hibernate.validator.constraints.ScriptAssert.message=script expression "{script}" didn't evaluate to true

このデフォルトのメッセージを上書きしたい場合は、自分で作成したValidationMessages.propertiesに任意のメッセージを定義することで変更できます。

javax.validation.constraints.Size.message=うわがきされちゃったよ
mykey={min}から{max}ではないな

とファイルを変更して再度テストを実行してみます。そうすると自分で定義したメッセージで上書きされます。

10から2147483647ではないな
うわがきされちゃったよ
サイズが102147483647の間ではないよ

フィールド以外でも使用できる

メソッドとかにも使えます。ただしJavaBeansのValidationの為、使用できるのはgetXXとかisXXXです。これを使うとずいぶんといろんな事ができるようになります。

public class MethodValidationTest extends ValidationTestBase {
    String pass1 = "pass";
    String pass2 = "passsssss";
    int a = 2;
    int b = 4;
    @Max(value = 5, message = "合計は{value}以下じゃないとダメ")
    public int getSum() {
        return a + b;
    }
    @AssertTrue(message = "パスワードが一致しません")
    public boolean isComparePass() {
        return pass1.equals(pass2);
    }
    @Test
    public void メソッドについてバリデーションしてみる() throws Exception {
        MethodValidationTest bean = new MethodValidationTest();
        Set<ConstraintViolation<MethodValidationTest>> violations = validator.validate(bean);
        for (ConstraintViolation<MethodValidationTest> violation : violations) {
            System.err.println(violation.getMessage());
        }
    }
    // getter setter
}

出力

パスワードが一致しません
合計は5以下じゃないとダメ

メソッドに付与すると複数のフィールドにアクセスできるようなるため、複数フィールドにアクセスするようなValidationや複雑なロジックも記述できるようになります。

カスタムValidatorを定義する

ユーザ独自にアノテーションを定義することで作成できます。

既存のアノテーションを組み合わせて作成する

まずはアノテーションを定義します。ここではユーザIDをチェックするようなアノテーションを作ってみます。

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@NotNull(message = "ユーザIDは必須")
@Size(min = 4, max=10, message = "ユーザIDは{min}から{max}文字")
public @interface UserId {
    String message() default "{org.yamkazu.jsr303_samples.customvalidator.UserId.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        UserId[] value();
    }
}

@Target、@Retention、@Documentedはアノテーション作るときのおまじないですね。@Constraintはvalidateするクラスを指定するのですが後で説明します。@NotNull、@Sizeは今まで見てきたやつですね。ここに定義すると@UserIdがこれらのConstraintを持っていることになります。中身の定義はメッセージを定義するmessage、後で説明しますがgroups、payload、Listはもうこれがおまじないのテンプレートだと思ってください。groupsとpayloadはdefault{}じゃないといけないと決まっています。Listにはその自分が定義するアノテーションの配列を持つようにします。

使用する際は今までと一緒です。

public class UserIdTest extends ValidationTestBase {
    @UserId
    String userId;
    @Test
    public void カスタムバリデーションを使う() throws Exception {
        UserIdTest bean = new UserIdTest();
        bean.setUserId("bar");
        Set<ConstraintViolation<UserIdTest>> violations = validator.validate(bean);
        for (ConstraintViolation<UserIdTest> violation : violations) {
            System.err.println(violation.getMessage());
        }
    }
    // setter getter
}

出力

ユーザIDは4から10文字

この@UserIdは

@NotNull(message = "ユーザIDは必須")
@Size(min = 4, max=10, message = "ユーザIDは{min}から{max}文字")
String userId;

とした場合と同じ意味ですが、自分でアノテーションを定義することで同じ設定を使い回すことができるようになります。同じ制約を複数でもつ場合なんかには非常に便利ですね。手間も省けますし、いろんな場所で同じ設定であることが保証されるので間違いも少なくなります。

Validatorクラスを作成する

自分でValidatorクラスを作成する場合はConstraintValidatorインタフェースの実装クラスを作成して、そのクラスを@ConstraintアノテーションのvalidatedByに指定します。ここでは現在時刻から一定の日数内であることをというルールを考えてみます。

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {PeriodValidator.class})
public @interface Period {
    int day() ;
    Class<?>[] groups() default {};
    String message() default "{day}日以内でないとだめです";
    Class<? extends Payload>[] payload() default {};
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Period[] value();
    }
    class PeriodValidator implements ConstraintValidator<Period, Date> {
        int day;        
        @Override
        public void initialize(Period period) {
            day = period.day();
        }
        @Override
        public boolean isValid(Date date, ConstraintValidatorContext context) {
            if (date == null) {
                return true;
            }
            Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.DATE, day);
            return date.before(calendar.getTime()); 
        }        
    }    
}

今回はアノテーションとValidatorクラスは1対1しか関係を持たないのでinterfaceの内部クラスとして作ってしまっています。ConstraintValidatorの実装クラスはinitializeでそのアノテーションに指定された属性値を取得して、isValidで対象のデータの検証を行います。@Null、@NotNullと併用できるように基本的にはnullだった場合はtrueを返すようにします。
テスト

    @Period(day = 1)
    Date date;
    @Test
    public void バリデーションしてみる() throws Exception {
        Calendar calendar = Calendar.getInstance();
        date = calendar.getTime();
        assertThat("今日は大丈夫", validator.validate(this).size(), is(0));

        calendar.add(Calendar.DATE, 1);
        date = calendar.getTime();
        assertThat("明日まで大丈夫", validator.validate(this).size(), is(0));

        calendar.add(Calendar.DATE, 1);
        date = calendar.getTime();
        assertThat("明後日はだめ", validator.validate(this).size(), is(1));
    }
   // ...
}

結構簡単ですね。

同じValidationで複数の条件を指定するList

カスタムValidatorを定義するときにも出てきましたが、Listを使うと同じ制約の中で複数の条件を指定出来ます。groupsと組み合わせると、もう少し複雑な意味を持つのですが一旦置いておきます。

    @Pattern.List({
        @Pattern(regexp = "^aaa.*"),
        @Pattern(regexp = ".*bbb$")
    })
    String str;

    @Test
    public void バリデーションしてみる() throws Exception {
        str = "ccc";
        assertThat(validator.validate(this).size(), is(2));
        str = "aaa";
        assertThat(validator.validate(this).size(), is(1));
        str = "bbb";
        assertThat(validator.validate(this).size(), is(1));
        str = "aaabbb";
        assertThat(validator.validate(this).size(), is(0));
    }
    // ...

ここではaaaから始まるというルールと、bbbで終わるというルールを指定してます(例なので一つとの正規表現で書けるというツッコミはなしでw)。

特定の検証グループを作成するgroups

このへんからだんだんややこしくなってきます。
各検証アノテーションでお決まりで定義していたgroupsという奴です。これを使うと、フィールドやメソッドなどにアノテーションを付与する際に一緒にgroupsを指定することで、特定の検証グループを設定できるようになります。

基本的な例とDefaultという概念

まずは以下のテストコードをみてくだい。

public class FirstGroupsTest extends ValidationTestBase {
    interface Group1 {}
    interface Group2 {}
    @NotNull
//  何も指定しないと暗黙的に@NotNull(groups = Default.class)
    Object aaa;

    @NotNull(groups = Group1.class)
    Object bbb;

    @Test
    public void Defaultグループをバリデーションしてみる() throws Exception {
        // 以下の二つは同じ意味
        // グループがDefaultのものだけ検証する
        // ここではaaaだけが検査対象となる
        assertThat(validator.validate(this).size(), is(1));
        assertThat(validator.validate(this, Default.class).size(), is(1));
    }
    @Test
    public void Group1バリデーションしてみる() throws Exception {
        // グループがGroup1のものだけが検証する
        // ここではbbbだけが検査対象となる
        assertThat(validator.validate(this, Group1.class).size(), is(1));
    }
    // ...
}

まず重要な概念としてDefaultという概念があるのですが、これはjavax.validation.groups.Defaultとして定義されているインタフェースです。groupsは基本的に省略可能なのですが、省略した場合は暗黙的にDefaultとし定義したものとして動作します。そしてValidator#validateの引数として、グループを指定してvalidateとすると、そのグループのみの検証を行うようになります。グループはClass... groupsで可変長配列で設定します。省略した場合はDefault.classを設定したのと同じ意味ですね。

groupsを組み合わせる

もう少し複雑な例を見ていきます。同じフィールドに対して複数のグループを指定した例です。

public class SecoundGroupsTest extends ValidationTestBase {
    interface Group1 {}
    @NotNull
    Object aaa;
    @NotNull(groups = { Group1.class, Default.class })
    Object bbb;    
    @NotNull(groups = Group1.class)
    Object ccc;
    @Test
    public void Defaultグループをバリデーションしてみる() throws Exception {
        // グループがDefaultのものだけ検証する
        // ここではaaa,bbbだけが検査対象となる
        assertThat(validator.validate(this).size(), is(2));
    }
    @Test
    public void Group1バリデーションしてみる() throws Exception {
        // グループがGroup1のものだけが検証する
        // ここではbbb, cccだけが検査対象となる
        assertThat(validator.validate(this, Group1.class).size(), is(2));
    }
    @Test
    public void ぜんぶバリデーションしてみる() throws Exception {
        // すべてのグループを検証する
        // ここではaaa, bbb, cccすべてが対象となる
        assertThat(validator.validate(this, Default.class, Group1.class).size(), is(3));
    }
    // ...
}
@GroupSequenceを使用して指定したグループを順番に検証する
public class GroupSequenceTest extends ValidationTestBase {
    @GroupSequence({ Default.class, Group1.class })
    interface All {
    }
    interface Group1 {
    }
    @NotNull
    Object aaa;
    @NotNull(groups = { Group1.class, Default.class })
    Object bbb;
    @NotNull(groups = Group1.class)
    Object ccc;
    @Test
    public void バリデーションしてみる() throws Exception {
        // 最初にDefaultが実施される
        validateThis(All.class);
        assertThat(validator.validate(this, All.class).size(), is(2));

        aaa = new Object();
        bbb = new Object();
        // Defaultが問題なけれがGroup1が検証される
        assertThat(validator.validate(this, All.class).size(), is(1));
    }
    // ...
}

@GroupSequenceを使用すると特定のグループを順番に検証することが出来ます。特定のグループで検証がNGだった場合はあとのグループの検証は実施されません。

Listと組み合わせる

これが非常に面白いです。こいつを使うと特定のコンテキストにおいて検証ルールを切り替えたりするような使い方ができます。
一般ユーザとプレミアムユーザでディスク容量の制限が違うという例を考えてみます。

public class GroupsWithList extends ValidationTestBase {

    interface NormalUser {
    }

    interface PremiumUser{
    }
    
    @Max.List({
        @Max(value = 10, groups = NormalUser.class),
        @Max(value = 100, groups = PremiumUser.class)
    })
    int diskSize;

    @Test
    public void NormalUserでバリデーションしてみる() throws Exception {
        diskSize = 9;
        assertThat(validator.validate(this, NormalUser.class).size(), is(0));
        
        diskSize = 50;
        assertThat("一般ユーザなので10超えててNG", validator.validate(this, NormalUser.class).size(), is(1));
    }
    
    @Test
    public void PremiumUserでバリデーションしてみる() throws Exception {
        diskSize = 9;
        assertThat(validator.validate(this, PremiumUser.class).size(), is(0));
        
        diskSize = 50;
        assertThat("プレミアムユーザなので100までOK", validator.validate(this, PremiumUser.class).size(), is(0));
    }
    // ...
}

まとめ

最後のGroupsなんかは非常に面白い機能です。ただ、他のWebフレームワークや、O/Rマッパと合わせて使う際には、他のフレームワークの連携機能の問題でGroupsがサポートされていないことが実はほとんどです(自動的にDefaultとして処理されるとか)。とはいいつつも独自で何かドメインロジック内に検証の仕組みを入れたいというようなときにはものすごい活用出来る気がします。

あと他のフレームワークと連携して使うときは、メッセージリソースの扱いが多少異なってたりする場合があるので、連携して使うときのそのドキュメントをしっかり参照の上使用することをおすすめします。

ここで書いたサンプルはこのへんに置いてあるので適当に遊んでみてください。
https://github.com/yamkazu/jsr303-samples

ではEnjoy Validation!!