JavaEE使い方メモ(Bean Validation)

  • 88
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

環境構築

コード

Bean Validation とは

バリデーション(入力チェック)用のフレームワーク。

入力チェックは様々なレイヤに分散されやすい。
例えば、桁数やフォーマットのような形式チェックはプレゼンテーションレイヤに、マスタの存在や他のデータとの関連が正しいかなどのビジネスロジックのチェックはそれより深いレイヤなどに分かれたりすることがある。

こういった分散されやすい入力チェックを、一箇所にまとめまられるようにしようという目標のもと作られたものらしい。

入力チェックのルール(制約)はアノテーションで定義する。
null チェックなどの汎用的なものはあらかじめ定義されている。もし標準のアノテーションでは足りない場合は、独自にアノテーションやバリデーションロジックを定義することもできる。

この仕様は、単体で利用するよりかは他のフレームワーク(JPA, JAX-RS, JSF など)と連携することで利用されることが多い(と思う)。

Hello World

HelloEjb.java
package sample.bean_validation.ejb;

import java.util.Set;
import javax.ejb.Stateless;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import sample.bean_validation.bean.HelloBean;

@Stateless
public class HelloEjb {

    public void hello() {
        // Validator を取得
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        // Bean を作成
        HelloBean bean = new HelloBean();

        // バリデーションを実行
        Set<ConstraintViolation<HelloBean>> result = validator.validate(bean);

        // 結果の確認
        System.out.println("size = " + result.size());
        System.out.println("message = " + result.iterator().next().getMessage());
    }
}
HelloBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class HelloBean {
    @NotNull
    private String hoge;
}
実行結果
情報:   size = 1
情報:   message = may not be null
  • Bean Validation を利用するには、まず Validator オブジェクトを取得する。
    • Validator オブジェクトは、Validation.buildDefaultValidatorFactory() でファクトリを取得し、
    • ファクトリの getValidator() メソッドで取得する。
  • バリデーションの実行は、 Validator クラスの validate() メソッドを使う。
    • 引数に検証したいオブジェクトを渡す。
  • 検証結果は ConstraintViolationSet で返される。
  • Bean 側(HelloBean)は、 @NotNull でフィールドをアノテートすることで制約を定義している。
    • 制約を定義するためのアノテーションを Constraint Annotation と呼ぶ(ここでは制約アノテーションと呼称する)。

フィールドを直接検証対象にする

FieldValidationBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class FieldValidationBean {
    @NotNull
    private String value;

    public String getValue() {
        System.out.println("FieldValidationBean#getValue()");
        return value;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

import java.util.Set;
import javax.ejb.Stateless;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import sample.bean_validation.bean.FieldValidationBean;

@Stateless
public class TestEjb {

    public void fieldValidation() {
        FieldValidationBean bean = new FieldValidationBean();

        this.validate(bean);
    }

    private <T> void validate(T bean) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Set<ConstraintViolation<T>> constraintViolations = validator.validate(bean);

        System.out.println("size = " + constraintViolations.size());

        constraintViolations.forEach(constraintViolation -> {
            System.out.println("message = " + constraintViolation.getMessage());
        });
    }
}
GlassFishコンソール出力
情報:   size = 1
情報:   message = may not be null
  • 制約アノテーションでフィールドを直接アノテートすると、そのフィールドは検証対象になる。
  • フィールドを直接アノテートした場合、アクセサーメソッド(getValue())を経由することなく検証が行われる。

プロパティを検証対象にする

PropertyValidationBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class PropertyValidationBean {

    private String value = "xxx";

    @NotNull
    public String getValue() {
        System.out.println("PropertyValidationBean.getValue()");
        return null;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.FieldValidationBean;

@Stateless
public class TestEjb {

    public void propertyValidation() {
        PropertyValidationBean bean = new PropertyValidationBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   PropertyValidationBean.getValue()
情報:   size = 1
情報:   message = may not be null
  • ゲッターメソッドを制約アノテーションでアノテートすると、そのゲッターメソッドで取得できる値(JavaBeans におけるプロパティ)が検証対象になる。
  • プロパティは、アクセサーメソッドを経由して値が取得される。

他の Bean をフィールドとして持つときに、その Bean も検証対象にする

まずはデフォルトの動きを確認

FooBean.java
package sample.bean_validation.bean;

public class FooBean {

    private BarBean bar = new BarBean(); // ★他の Bean を持つ
}
BarBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class BarBean {
    @NotNull
    private String value;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.FooBean;

@Stateless
public class TestEjb {

    public void objectGraph() {
        FooBean foo = new FooBean();

        this.validate(foo);
    }

    ...
}
GlassFishコンソール出力
情報:   size = 0
  • エラー無しと判定される。
  • つまり、デフォルトだと FooBean クラスをチェックしても、そのフィールドである BarBean まで検証は行われない。

BarBean も検証対象にする

FooBean.java
package sample.bean_validation.bean;

import javax.validation.Valid;

public class FooBean {
    @Valid
    private BarBean bar = new BarBean();
}
GlassFishコンソール出力
情報:   size = 1
情報:   message = may not be null
  • bar フィールドを @Valid でアノテートすることで、 BarBean の検証が実行されるようになる。

コレクションを検証対象にする

BarListBean.java
package sample.bean_validation.bean;

import java.util.Arrays;
import java.util.List;
import javax.validation.Valid;

public class BarListBean {
    @Valid
    private List<BarBean> barList = Arrays.asList(new BarBean(), new BarBean());
}
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.BarListBean;

@Stateless
public class TestEjb {

    public void listField() {
        BarListBean bean = new BarListBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   size = 2
情報:   message = may not be null
情報:   message = may not be null
  • 配列、 Iterable を実装したコレクション、 Map のいずれかのフィールドを @Valid でアノテートすると、各要素に対して検証が行われるようになる。

ConstraintViolation から取得できる情報

ConstraintViolation は直訳すると制約違反。

検証エラーになったときに、エラーの情報が格納されている。

RootBean.java
package sample.bean_validation.bean;

import javax.validation.Valid;

public class RootBean {

    @Valid
    private LeafBean leaf = new LeafBean();
}
LeafBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Min;

public class LeafBean {
    @Min(10)
    private int number = 9;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.RootBean;

@Stateless
public class TestEjb {

    public void constraintVioletion() {
        RootBean root = new RootBean();

        this.debugValidate(root);
    }

    ...

    private <T> void debugValidate(T bean) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Set<ConstraintViolation<T>> constraintViolations = validator.validate(bean);

        System.out.println("size = " + constraintViolations.size());

        constraintViolations.forEach(constraintViolation -> {
            String msg =
                "message = " + constraintViolation.getMessage() + "\n" +
                "messageTemplate = " + constraintViolation.getMessageTemplate() + "\n" +
                "rootBean = " + constraintViolation.getRootBean() + "\n" +
                "rootBeanClass = " + constraintViolation.getRootBeanClass() + "\n" +
                "invalidValue = " + constraintViolation.getInvalidValue() + "\n" +
                "propertyPath = " + constraintViolation.getPropertyPath() + "\n" +
                "leafBean = " + constraintViolation.getLeafBean() + "\n" +
                "descriptor = " + constraintViolation.getConstraintDescriptor() + "\n"
            ;

            System.out.println(msg);
        });
    }
}
GlassFishコンソール出力
情報:   size = 1
情報:   message = must be greater than or equal to 10
messageTemplate = {javax.validation.constraints.Min.message}
rootBean = sample.bean_validation.bean.RootBean@77e97fb4
rootBeanClass = class sample.bean_validation.bean.RootBean
invalidValue = 9
propertyPath = leaf.number
leafBean = sample.bean_validation.bean.LeafBean@7fe7844b
descriptor = ConstraintDescriptorImpl{annotation=javax.validation.constraints.Min, payloads=[], hasComposingConstraints=true, isReportAsSingleInvalidConstraint=false, elementType=FIELD, definedOn=DEFINED_LOCALLY, groups=[interface javax.validation.groups.Default], attributes={groups=[Ljava.lang.Class;@6b8228bd, message={javax.validation.constraints.Min.message}, value=10, payload=[Ljava.lang.Class;@ec712ca}, constraintType=GENERIC}
プロパティ 説明
message エラーメッセージ。
messageTemplate エラーメッセージを生成するための元となったテンプレート。
rootBean @Valid を使ってオブジェクトグラフを検証した場合、そのルートとなったクラスのインスタンス。
rootBeanClass rootBeanClass オブジェクト。
invalidValue 検証エラーになった対象の、実際の値。
propertyPath 検証エラーになった対象を示すパス文字列。
leafBean 検証エラーとなった Bean の Class オブジェクト。
constraintDescriptor 制約アノテーションに設定されていた値など、制約に関するメタデータを持ったオブジェクト。

エラーメッセージ

任意のエラーメッセージを設定する

CustomErrorMessageBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class CustomErrorMessageBean {

    @NotNull(message="null はダメ!")
    private String value;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.CustomErrorMessageBean;

@Stateless
public class TestEjb {

    public void customErrorMessage() {
        CustomErrorMessageBean bean = new CustomErrorMessageBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   message = null はダメ!
  • 制約アノテーションには message という属性が定義されており、そこにエラーメッセージを設定することができるようになっている。

リソースバンドルを使用する

ValidationMessages_ja.properties
# ★ 実際は native2ascii でエンコードする
sample.bean_validation.notNull=null ダメ!絶対!
ResourceBundleMessageBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class ResourceBundleMessageBean {

    @NotNull(message="{sample.bean_validation.notNull}")
    private String value;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.ResourceBundleMessageBean;

@Stateless
public class TestEjb {

    public void resourceBundleMessage() {
        ResourceBundleMessageBean bean = new ResourceBundleMessageBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   message = null ダメ!絶対!
  • 基底名を ValidationMessages にしてリソースバンドルのファイルを作成する。
    • ファイルは、クラスパス直下に配置する。
  • 制約アノテーションの message に、 {} で括る形でリソースバンドルのキーを指定する。
    • {}メッセージパラメータと呼ぶ。

ビルトインの制約アノテーションのデフォルトエラーメッセージを上書きする

ビルトインの制約アノテーションには、全てデフォルトのエラーメッセージが定義されている。
これらのエラーメッセージの定義にも、リソースバンドルが使用されている。

GlassFish の場合、 Bean Validation の実装には RI である Hibernate Validator が使われており、デフォルトのリソースバンドルファイルはその中に存在している。

具体的には <GlassFish インストールディレクトリ>\glassfish\modules\bean-validator.jar の中の org.hibernate.validator の下に格納されている。

bean-validation.JPG

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 ${inclusive == true ? 'or equal to ' : ''}{value}
javax.validation.constraints.DecimalMin.message  = must be greater than ${inclusive == true ? '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}

...

これらと同じキーで ValidationMessages.properties を作成すれば、デフォルトのエラーメッセージを上書きすることができる。

ValidationMessages_ja.properties
javax.validation.constraints.Null.message = null じゃないとダメだよ!
OverrideDefaultMessageBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Null;

public class OverrideDefaultMessageBean {
    @Null
    private String value = "xxx";
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.OverrideDefaultMessageBean;

@Stateless
public class TestEjb {

    public void overrideDefaultMessage() {
        OverrideDefaultMessageBean bean = new OverrideDefaultMessageBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   message = null じゃないとダメだよ!

制約アノテーションに設定されている属性値を参照する

ReferenceAnnotationAttributeBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Max;

public class ReferenceAnnotationAttributeBean {
    @Max(value=50, message="{value} 以下じゃないとダメです")
    private int number = 51;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.ReferenceAnnotationAttributeBean;

@Stateless
public class TestEjb {

    public void referenceAnnotationAttribute() {
        ReferenceAnnotationAttributeBean bean = new ReferenceAnnotationAttributeBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   message = 50 以下じゃないとダメです
  • {<属性名>} とすることで、制約アノテーションに設定した属性値を参照することができる。

EL 式を使う

ELExpressionMessageBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Max;

public class ELExpressionMessageBean {

    @Max(value=30, message="{value} より ${validatedValue - value} も大きい値が渡された!")
    private int number = 42;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.ELExpressionMessageBean;

@Stateless
public class TestEjb {

    public void elExpressionMessage() {
        ELExpressionMessageBean bean = new ELExpressionMessageBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   message = 30 より 12 も大きい値が渡された!
  • エラーメッセージの定義には EL 式が使える(${})。
  • {} のときと同じように、制約アノテーションの属性を参照できる(value)。
  • validatedValue で、検証対象の実際の値を参照できる。

{}${} の評価順序

上述の EL 式を用いたメッセージの定義を、以下のように宣言したとする。

ELExpressionMessageBean.java
    @Max(value=30, message="${value} より ${validatedValue - value} も大きい値が渡された!")
    private int number = 42;

先頭の {value}${value} という形で EL 式にしている。

すると、次のようにエラーメッセージが出力される。

GlassFishコンソール出力
情報:   message = $30 より 12 も大きい値が渡された!

$30 と出力されてしまっている。

これは、 メッセージパラメータ({})の評価が EL 式(${})より先に実行されていることが原因となっている。

制約アノテーションの属性値を単独で参照する場合は、メッセージパラメータを使うのがいい。

フォーマッターを使用する

ELFormatterMessageBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Max;

public class ELFormatterMessageBean {

    @Max(value=40, message="${formatter.format('%d 以下のみ可(実際=%d)', value, validatedValue)}")
    private int number = 49;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.ELFormatterMessageBean;

@Stateless
public class TestEjb {

    public void elFormatterMessage() {
        ELFormatterMessageBean bean = new ELFormatterMessageBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   message = 40 以下のみ可(実際=49)
  • formatter という名前でフォーマッター参照できる。
  • 使い方は java.util.Formatter と同じ。

制約をグルーピングする

基本

HogeGroup.java
package sample.bean_validation.bean.group;

// ★グループは interface で定義しなければならない
public interface HogeGroup {
}
GroupingBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import sample.bean_validation.bean.group.HogeGroup;

public class GroupingBean {
    @NotNull
    private String string;

    @Max(30)
    private int integer = 31;

    @AssertTrue(groups=HogeGroup.class)
    private boolean bool;

    @DecimalMin(value="19.9", groups=HogeGroup.class)
    private double decimal = 19.8;
}
TestEjb.java
package sample.bean_validation.ejb;

...
import javax.validation.groups.Default;
import sample.bean_validation.bean.GroupingBean;
import sample.bean_validation.bean.group.HogeGroup;

@Stateless
public class TestEjb {

    public void grouping() {
        GroupingBean bean = new GroupingBean();

        this.validate(bean); // ★ グループ指定無し
        this.validate(bean, HogeGroup.class); // ★ HogeGroup を指定
        this.validate(bean, Default.class); // ★ Default を指定
    }

    ...

    private <T> void validate(T bean, Class<?>... groups) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        // ★ validate() メソッドの第二引数で groups を指定
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(bean, groups);

        if (groups.length == 0) {
            System.out.println("[no group]");
        } else {
            System.out.println("[group=" + Arrays.toString(groups) + "]");
        }
        System.out.println("size = " + constraintViolations.size());

        constraintViolations.forEach(constraintViolation -> {
            System.out.println("message = " + constraintViolation.getMessage());
        });
    }

    ...
}
GlassFishコンソール出力
情報:   [no group]
情報:   size = 2
情報:   message = may not be null
情報:   message = must be less than or equal to 30

情報:   [group=[interface sample.bean_validation.bean.group.HogeGroup]]
情報:   size = 2
情報:   message = must be greater than or equal to 19.9
情報:   message = must be true

情報:   [group=[interface javax.validation.groups.Default]]
情報:   size = 2
情報:   message = may not be null
情報:   message = must be less than or equal to 30
  • 制約アノテーションの groups 属性に、任意のインターフェースの Class オブジェクトを設定する。
    • グループに指定する Class オブジェクトは、インターフェースのものでなければならない(普通のクラスだとエラーになる)。
  • Validator#validate(Object, Class...) メソッドの第二引数以降で、 groups に指定した Class オブジェクトを渡す。
  • すると、指定した Class オブジェクトが groups で設定されているターゲットだけが、検証の対象となる。
  • groups が未指定の場合、暗黙的に Default というグループが使用される。

複数のグループを指定する

MultiGroupingBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;
import sample.bean_validation.bean.group.HogeGroup;

public class MultiGroupingBean {
    @NotNull
    public String getString() {
        System.out.println("getString()");
        return null;
    }

    @Max(value=30, groups=HogeGroup.class)
    public int getNumber() {
        System.out.println("getNumber()");
        return 31;
    }

    @AssertTrue(groups={Default.class, HogeGroup.class})
    public boolean isBool() {
        System.out.println("isBool()");
        return false;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.MultiGroupingBean;

@Stateless
public class TestEjb {

    public void multiGrouping() {
        MultiGroupingBean bean = new MultiGroupingBean();

        this.validate(bean, HogeGroup.class);
        this.validate(bean, HogeGroup.class, Default.class);
    }

    ...
}
GlassFishコンソール出力
情報:   getNumber()
情報:   isBool()
情報:   [group=[interface sample.bean_validation.bean.group.HogeGroup]]
情報:   size = 2
情報:   message = must be true
情報:   message = must be less than or equal to 30

情報:   getNumber()
情報:   isBool()
情報:   getString()
情報:   [group=[interface sample.bean_validation.bean.group.HogeGroup, interface javax.validation.groups.Default]]
情報:   size = 3
情報:   message = must be true
情報:   message = may not be null
情報:   message = must be less than or equal to 30

  • 制約アノテーションの groups に複数のグループを指定した場合、「それぞれのグループに所属する」という意味になる。
  • validate() メソッドの第二引数以降で複数のグループを指定した場合、ぞれぞれのグループで検証が行われる。

グループの検証順序を指定する

DefaultHoge.java
package sample.bean_validation.bean.group;

import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({Default.class, HogeGroup.class})
public interface DefaultHoge {
}
HogeDefault.java
package sample.bean_validation.bean.group;

import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({HogeGroup.class, Default.class})
public interface HogeDefault {
}
GroupSequenceBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import sample.bean_validation.bean.group.HogeGroup;

public class GroupSequenceBean {
    @NotNull
    public String getString() {
        System.out.println("getString()");
        return null; // ★こっちはエラーになる
    }

    @Max(value=30, groups=HogeGroup.class)
    public int getNumber() {
        System.out.println("getNumber()");
        return 29; // ★こっちはエラーにならない
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.GroupSequenceBean;
import sample.bean_validation.bean.group.HogeDefault;
import sample.bean_validation.bean.group.DefaultHoge;

@Stateless
public class TestEjb {

    public void groupSequence() {
        GroupSequenceBean bean = new GroupSequenceBean();

        this.validate(bean, DefaultHoge.class);
        this.validate(bean, HogeDefault.class);
    }

    ...
}
GlassFishコンソール出力
情報:   getString()
情報:   [group=[interface sample.bean_validation.bean.group.DefaultHoge]]
情報:   size = 1
情報:   message = may not be null

情報:   getNumber()
情報:   getString()
情報:   [group=[interface sample.bean_validation.bean.group.HogeDefault]]
情報:   size = 1
情報:   message = may not be null
  • 任意のインターフェースを定義して、 @GroupSequence でアノテートする。
  • value 属性に、検証のグループを表す Class オブジェクトを配列で設定する。
  • この @GroupSequence でアノテートしたインターフェースの Class オブジェクトを validate() メソッドの第二引数に渡す。
  • すると、 value 属性で指定した順序で検証が行われるようになる。
  • また、先に実行したグループで検証エラーがあった場合は、そこで検証が中断され、後続のグループの検証は行われない。

デフォルトの検証順序を設定する

DefaultGroupSequenceBean.java
package sample.bean_validation.bean;

import javax.validation.GroupSequence;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import sample.bean_validation.bean.group.HogeGroup;

// ★ Default.class の代わりに自分自身の Class を指定する
@GroupSequence({HogeGroup.class, DefaultGroupSequenceBean.class})
public class DefaultGroupSequenceBean {
    @NotNull
    public String getString() {
        System.out.println("getString()");
        return null;
    }

    @Max(value=30, groups=HogeGroup.class)
    public int getNumber() {
        System.out.println("getNumber()");
        return 29;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.DefaultGroupSequenceBean;

@Stateless
public class TestEjb {

    public void defaultGroupSequence() {
        DefaultGroupSequenceBean bean = new DefaultGroupSequenceBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   [no group]
情報:   getNumber()
情報:   getString()
情報:   size = 1
情報:   message = may not be null
  • Bean を直接 @GroupSequence でアノテートすることで、デフォルトの検証順序を設定できる。
  • Default のグループを指定する場合は、 Default.class の代わりに Bean 自身の Class オブジェクトを指定する。

関連する他の Bean を検証するときに、検証対象のグループを一時的に差し替える

まず、差し替えを行わない通常パターン。

ConvertGroupBean.java
package sample.bean_validation.bean;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

public class ConvertGroupBean {
    @NotNull
    private String string;
    @Valid
    private OtherGroupBean other = new OtherGroupBean();
}
OtherGroupBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Min;
import sample.bean_validation.bean.group.HogeGroup;

public class OtherGroupBean {
    @Min(value=10, groups=HogeGroup.class)
    private int number = 9;
}
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.ConvertGroupBean;

@Stateless
public class TestEjb {

    public void convertGroup() {
        ConvertGroupBean bean = new ConvertGroupBean();

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   [no group]
情報:   size = 1
情報:   message = may not be null

ConvertGroupBeanother フィールドを @Valid で紐付けている。
しかし、 OtherGroupBean の方は number フィールドが HogeGroup で定義されている。

このため、 ConvertGroupBeanvalidate() で検証しても、グループの指定が実質 Default になっており、 HogeGroup である OtherGroupBean.number は検証対象外になっている。

OtherGroupBean を、 Default ではなく HogeGroup で検証させるには、以下のようにアノテーションを設定する。

ConvertGroupBean.java
package sample.bean_validation.bean;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.groups.ConvertGroup;
import javax.validation.groups.Default;
import sample.bean_validation.bean.group.HogeGroup;

public class ConvertGroupBean {
    @NotNull
    private String string;
    @Valid @ConvertGroup(from=Default.class, to=HogeGroup.class) // ★
    private OtherGroupBean other = new OtherGroupBean();
}
  • @ConvertGroup アノテーションを other フィールドに追加している。
  • from 属性に差し替え前のグループを、 to 属性に差し替え後のグループを指定する。
  • こうすると、 other を検証するときのグループが HogeGroup になり、 number フィールドも検証対象になる。
GlassFishコンソール出力
情報:   [no group]
情報:   size = 2
情報:   message = must be greater than or equal to 10
情報:   message = may not be null

1つのターゲットに複数のアノテーションを設定する

例えばあるフィールドに、 Default グループの場合は 30 以下の数値でないといけないが、 HogeGroup の場合は 40 以下まで OK という制約があったとする。

これを定義するには、単純に同じフィールドに @Max アノテーションを2つ設定すればよさそうだが、 Java 8 より前では同じアノテーションを同一箇所に複数設定することができなかった。

このため、 Bean Validation では制約アノテーションに List というインナーアノテーションを定義し、それを使うことで複数のアノテーションを1つのターゲットに付与できるようにしている。

MultiConstraintFieldBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.Max;
import sample.bean_validation.bean.group.HogeGroup;

public class MultiConstraintFieldBean {
    @Max.List({
        @Max(30),
        @Max(value=40, groups=HogeGroup.class)
    })
    private int value;

    public MultiConstraintFieldBean(int value) {
        this.value = value;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.MultiConstraintFieldBean;
import sample.bean_validation.bean.group.HogeGroup;

@Stateless
public class TestEjb {

    public void multiConstraintField() {
        MultiConstraintFieldBean bean = new MultiConstraintFieldBean(35);

        this.validate(bean);
        this.validate(bean, HogeGroup.class);
    }

    ...
}
GlassFishコンソール出力
情報:   [no group]
情報:   size = 1
情報:   message = must be less than or equal to 30

情報:   [group=[interface sample.bean_validation.bean.group.HogeGroup]]
情報:   size = 0
  • @Max.List を使って、複数の @Max アノテーションを設定している。
  • Default 指定で検証すると 35 はエラーになるが、 HogeGroup を指定して検証すると 35 は非エラーとなっている。

制約をまとめる

複数箇所で同じ組み合わせの制約アノテーションを使用する場合、その組み合わせを任意の自作アノテーションにまとめることができる。

SummarizeConstraint.java
package sample.bean_validation.constraint;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;

@Min(10)
@Max(30)
@Constraint(validatedBy = {})
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SummarizeConstraint {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
SummarizeConstraintBean.java
package sample.bean_validation.bean;

import sample.bean_validation.constraint.SummarizeConstraint;

public class SummarizeConstraintBean {
    @SummarizeConstraint
    private int number;

    public SummarizeConstraintBean(int number) {
        this.number = number;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.SummarizeConstraintBean;

@Stateless
public class TestEjb {

    public void summarizeConstraint() {
        SummarizeConstraintBean bean = new SummarizeConstraintBean(5);

        this.validate(bean);

        bean = new SummarizeConstraintBean(35);

        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   [no group]
情報:   size = 1
情報:   message = must be greater than or equal to 10

情報:   [no group]
情報:   size = 1
情報:   message = must be less than or equal to 30
  • 制約をまとめるアノテーションを作るには、まずそのアノテーション自体を以下のようにアノテートする。
    • @Constraint でアノテートする。
      • validatedBy 属性は、とりあえず空({})で定義する。
    • @RetentionRUNTIME にする。
    • @Target に、このアノテーションを設定する場所を指定する。
    • まとめる対象の制約アノテーション(@Max, @Min)でアノテートする。
  • 次に、 message, groups, payload という3つの属性を定義する。
    • これらは制約アノテーションが持たなければならない必須の属性になっている。
    • 今回は特に使用する予定はないので、全て空で宣言している。

自作のバリデータと制約アノテーションを作成する

検証処理(バリデータ)と制約アノテーションは、任意のものを自作できる。

基本

CustomValidation.java
package sample.bean_validation.constraint;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import sample.bean_validation.validator.CustomValidator;

@Constraint(validatedBy = CustomValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CustomValidation {

    String value();

    String message() default "{sample.bean_validation.constraint.CustomValidation.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
ValidationMessages_ja.properties
sample.bean_validation.constraint.CustomValidation.message="{value}" と "${validatedValue}" は別物
CustomValidator.java
package sample.bean_validation.validator;

import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import sample.bean_validation.constraint.CustomValidation;

public class CustomValidator implements ConstraintValidator<CustomValidation, String>{

    private String value;

    @Override
    public void initialize(CustomValidation annotation) {
        System.out.println("initialize() : " + hashCode());
        this.value = annotation.value();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        System.out.println("isValid() : " + hashCode());
        if (value == null) {
            return true;
        }

        return Objects.equals(this.value, value);
    }
}
CustomValidationBean.java
package sample.bean_validation.bean;

import sample.bean_validation.constraint.CustomValidation;

public class CustomValidationBean {
    @CustomValidation("hoge")
    private String value = "Hoge";
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.CustomValidationBean;

@Stateless
public class TestEjb {

    public void customValidation() {
        CustomValidationBean bean = new CustomValidationBean();

        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        this.validate(validator, bean);
        this.validate(validator, bean);
        this.validate(validator, bean);
    }

    ...

    private <T> void validate(Validator validator, T bean, Class<?>... groups) {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(bean, groups);

        if (groups.length == 0) {
            System.out.println("[no group]");
        } else {
            System.out.println("[group=" + Arrays.toString(groups) + "]");
        }
        System.out.println("size = " + constraintViolations.size());

        constraintViolations.forEach(constraintViolation -> {
            System.out.println("message = " + constraintViolation.getMessage());
        });
    }

    ...
}
GlassFishコンソール出力
情報:   initialize() : 189695458

情報:   isValid() : 189695458
情報:   [no group]
情報:   size = 1
情報:   message = "hoge" と "Hoge" は別物

情報:   isValid() : 189695458
情報:   [no group]
情報:   size = 1
情報:   message = "hoge" と "Hoge" は別物

情報:   isValid() : 189695458
情報:   [no group]
情報:   size = 1
情報:   message = "hoge" と "Hoge" は別物

制約アノテーションの実装

  • value 属性で指定した文字列と等しいことをチェックする、自作の制約アノテーションを実装している。
  • 制約アノテーションの基本的な定義は、 制約アノテーションをまとめたアノテーションの作り方 と同じようにする(というか、どちらも同じもの)。
    • 大きく異なるのは、 @ConstraintvalidatedBy 属性に CustomValidator.class を設定している点。
    • validatedBy で、検証処理を実装したクラス(バリデータ)の Class オブジェクトを指定することで、その制約アノテーションと検証処理とを紐付けている。
  • message 属性には、デフォルトで {<制約アノテーションのFQCN>.message} を設定している。
    • このフォーマットは、 Bean Validation の仕様書で推奨されている。

バリデーターの実装

  • 自作のバリデーターは、 ConstraintValidator インターフェースを実装して作成する。
  • 型引数の1つ目には対応する制約アノテーション(CustomValidation)を、2つ目には検証対象の値の型(String)を指定する。
  • ConstraintValidator インターフェースには、 initialize()isValid() という2つのメソッドが定義されている。
    • initialize() メソッドにはアノテーションのインスタンスが渡されるので、アノテーションの属性に設定された値を取得するのに利用する。
      • このメソッドは、 isValid() が呼ばれる前に1度だけ呼ばれる。
    • isValid() メソッドでは実際に検証処理を行う。
      • 検証 OK の場合は true を返し、 NG の場合は false を返す。
      • バリデーターのインスタンスは同じものが使いまわされており、 isValid() は複数のスレッドから呼ばれることがあり得る。
      • よって、このメソッドはスレッドセーフになるように作成しなければならない。

値が null の場合は検証 OK にする

isValid() メソッドに渡された値が null の場合、検証は OK にすることが推奨されている。

値が null のときにエラーとするのは @NotNull で明示的に宣言すべきだからだと、仕様書に記載されている。

@List インナーアノテーション

複数のアノテーションを同じターゲットに指定できるようにList というアノテーションを制約アノテーションのインナーアノテーションとして定義することが推奨されている。

CustomValidation.java
package sample.bean_validation.constraint;

...

@Constraint(validatedBy = CustomValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CustomValidation {

    String value();

    String message() default "{sample.bean_validation.constraint.CustomValidation.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Constraint(validatedBy = CustomValidator.class)
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface List { // ★ List アノテーション
        CustomValidation[] values();
    }
}

名前は別に List でなくても構わないが、仕様書では List という名前が推奨されている。

複数のフィールドをまたがる検証処理を実装する

ここまでの制約は、単一のフィールド(プロパティ)に対してのみ有効なものだった。

しかし、制約の中には複数のフィールドが関連し合ったものもあり得る。

バリデーターを自作することで、そういった検証処理も行えるようになる。

基本

ClassLevelValidation.java
package sample.bean_validation.constraint;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import sample.bean_validation.validator.ClassLevelValidator;

@Constraint(validatedBy = ClassLevelValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassLevelValidation {

    String message() default "{sample.bean_validation.constraint.ClassLevelValidation.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
ValidationMessages_ja.properties
sample.bean_validation.constraint.ClassLevelValidation.message=フィールドの関係が不正です。
ClassLevelValidator.java
package sample.bean_validation.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import sample.bean_validation.bean.ClassLevelValidationBean;
import sample.bean_validation.constraint.ClassLevelValidation;

public class ClassLevelValidator implements ConstraintValidator<ClassLevelValidation, ClassLevelValidationBean>{

    @Override
    public void initialize(ClassLevelValidation annotation) {}

    @Override
    public boolean isValid(ClassLevelValidationBean bean, ConstraintValidatorContext context) {
        if (bean == null) {
            return true;
        }

        if (bean.isBool()) { // ★ bool の値によって、検証内容を切り替えている
            if (bean.getString() == null) {
                return false;
            } else if (bean.getNumber() < 10) {
                return false;
            }
        } else {
            if (bean.getString() != null) {
                return false;
            } else if (10 <= bean.getNumber()) {
                return false;
            }
        }

        return true;
    }
}
ClassLevelValidationBean.java
package sample.bean_validation.bean;

import sample.bean_validation.constraint.ClassLevelValidation;

@ClassLevelValidation
public class ClassLevelValidationBean {

    private boolean bool;
    private String string;
    private int number;

    public ClassLevelValidationBean(boolean bool, String string, int number) {
        this.bool = bool;
        this.string = string;
        this.number = number;
    }

    public boolean isBool() {
        return bool;
    }

    public String getString() {
        return string;
    }

    public int getNumber() {
        return number;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.ClassLevelValidationBean;

@Stateless
public class TestEjb {

    public void classLevelValidation() {
        ClassLevelValidationBean bean = new ClassLevelValidationBean(true, null, 5);
        this.validate(bean);

        bean = new ClassLevelValidationBean(false, null, 15);
        this.validate(bean);
    }

    ...
}
GlassFishコンソール出力
情報:   [no group]
情報:   size = 1
情報:   message = フィールドの関係が不正です。

情報:   [no group]
情報:   size = 1
情報:   message = フィールドの関係が不正です。
  • 制約アノテーションの @TargetTYPE にする。
    • 制約アノテーションは、 Bean のクラスに直接付与する。
  • バリデーターの検証対象の型を、対象の Bean の型にする。
  • あとは、 isValid() メソッドに Bean のインスタンスが渡されるので、フィールドの値を取得して検証を行う。

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

上記の実装だと、エラーメッセージが分かりにくくてやや不親切。

バリデーターの中で、具体的なエラー箇所が分かるようにメッセージを構築する。

ClassLevelValidator.java
package sample.bean_validation.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import sample.bean_validation.bean.ClassLevelValidationBean;
import sample.bean_validation.constraint.ClassLevelValidation;

public class ClassLevelValidator implements ConstraintValidator<ClassLevelValidation, ClassLevelValidationBean>{

    @Override
    public void initialize(ClassLevelValidation annotation) {}

    @Override
    public boolean isValid(ClassLevelValidationBean bean, ConstraintValidatorContext context) {
        if (bean == null) {
            return true;
        }

        if (bean.isBool()) {
            if (bean.getString() == null) {
                this.rebuildConstraintViolation(context, "bool が true の場合、 string に null は指定できません。");
                return false;
            } else if (bean.getNumber() < 10) {
                this.rebuildConstraintViolation(context, "bool が true の場合、 number は 10 未満でなければなりません。");
                return false;
            }
        } else {
            if (bean.getString() != null) {
                this.rebuildConstraintViolation(context, "bool が false の場合、 string は null でなければなりません。");
                return false;
            } else if (10 <= bean.getNumber()) {
                this.rebuildConstraintViolation(context, "bool が false の場合、 number は 10 以上でなければなりません。");
                return false;
            }
        }

        return true;
    }

    private void rebuildConstraintViolation(ConstraintValidatorContext context, String template) {
        context.disableDefaultConstraintViolation(); // ★ デフォルトの制約違反情報を破棄
        context.buildConstraintViolationWithTemplate(template).addConstraintViolation(); // ★ 新規に制約違反情報を追加
    }
}
GlassFishコンソール出力
情報:   [no group]
情報:   size = 1
情報:   message = bool が true の場合、 string に null は指定できません。

情報:   [no group]
情報:   size = 1
情報:   message = bool が false の場合、 number は 10 以上でなければなりません。
  • isValid() の引数に渡されている ConstraintValidatorContext を使うと、制約違反の情報(ConstraintViolation)を任意に構築することができる。
  • 自作の ConstraintViolation を構築する場合は、まず disableDefaultConstraintViolation() でデフォルトの結果を破棄する。
    • こうしないと、エラーのカウントがデフォルトの実装が追加したものとで重複してカウントされたりしてしまう。
  • buildConstraintViolationWithTemplate() で、新しい ConstraintViolation の構築を開始する。
    • 引数には、エラーメッセージのテンプレートを渡す。
    • ここに渡した文字列でも、メッセージパラメータや EL 式は利用できる。
  • 最後に addConstraintViolation() で、 ConstraintViolation の構築を完了させる。

メソッドの検証

メソッドの検証もできる。

ただし、 API の作りを見る限り、他のフレームワークから利用されることを前提としており、直接利用することは無いと思う(個人的な見解)。

コンストラクタも MethodConstructor にすれば同じ要領で検証できる。

引数の検証

MethodParameterValidationBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class MethodParameterValidationBean {

    public void method(@NotNull String value) {
    }
}
package sample.bean_validation.ejb;

...
import java.lang.reflect.Method;
import javax.validation.executable.ExecutableValidator;
import sample.bean_validation.bean.MethodParameterValidationBean;

@Stateless
public class TestEjb {

    public void methodParameterValidation() throws Exception {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        // ★ メソッド検証用の Validator を取得する
        ExecutableValidator validator = factory.getValidator().forExecutables();

        MethodParameterValidationBean bean = new MethodParameterValidationBean();
        // ★ 対象の Method インスタンスを取得
        Method method = MethodParameterValidationBean.class.getMethod("method", String.class);

        Object[] parameters = {null};

        Set<ConstraintViolation<MethodParameterValidationBean>> constraintViolations
                = validator.validateParameters(bean, method, parameters);

        this.showConstraintViolation(constraintViolations);
    }

    ...

    private <T> void showConstraintViolation(Set<ConstraintViolation<T>> constraintViolations) {
        System.out.println("size = " + constraintViolations.size());

        constraintViolations.forEach(constraintViolation -> {
            System.out.println("message = " + constraintViolation.getMessage());
        });
    }

    ...
}
GlassFishコンソール出力
情報:   message = may not be null
  • メソッドのパラメータにも、 @NotNull などの制約アノテーションを設定できる。
  • 検証には、 Validator#forExecutables() で取得できる専用のバリデーターを使う。
  • パラメータの検証には ExecutableValidator#validateParameters() メソッドを使用する。

戻り値の検証

MethodReturnValueValidationBean.java
package sample.bean_validation.bean;

import javax.validation.constraints.NotNull;

public class MethodReturnValueValidationBean {

    @NotNull
    public String method() {
        return null;
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.MethodReturnValueValidationBean;

@Stateless
public class TestEjb {

    public void methodReturnValueValidation() throws Exception {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        ExecutableValidator validator = factory.getValidator().forExecutables();

        MethodReturnValueValidationBean bean = new MethodReturnValueValidationBean();
        Method method = MethodReturnValueValidationBean.class.getMethod("method");

        // ★ メソッドを実行して戻り値を取得する
        Object returnValue = bean.method();

        Set<ConstraintViolation<MethodReturnValueValidationBean>> constraintViolations
                = validator.validateReturnValue(bean, method, returnValue); // ★検証

        this.showConstraintViolation(constraintViolations);
    }

    ...
}
GlassFishコンソール出力
情報:   message = may not be null
  • メソッドの戻り値を検証する場合は、メソッド自体に制約アノテーションを設定する。
  • バリデーションの実行には、 ExecutableValidator#validateReturnValue() メソッドを使用する。

複数の引数にまたがる検証

引数が複数あり、それぞれの引数間に関連がある検証は、以下のように自作のバリデータと制約アノテーションを作成する。

CrossParameterValidation.java
package sample.bean_validation.constraint;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import sample.bean_validation.validator.CrossParameterValidator;

@Constraint(validatedBy = CrossParameterValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CrossParameterValidation {

    String message() default "{sample.bean_validation.constraint.CrossParameterValidation.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
properties.ValidationMessages_ja.properties
sample.bean_validation.constraint.CrossParameterValidation.message=第一引数 < 第二引数 となるようにしてください。
CrossParameterValidator.java
package sample.bean_validation.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import sample.bean_validation.constraint.CrossParameterValidation;

// ★ @SupportedValidationTarget でアノテート
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class CrossParameterValidator implements ConstraintValidator<CrossParameterValidation, Object[]>{

    @Override
    public void initialize(CrossParameterValidation annotation) {}

    @Override
    public boolean isValid(Object[] args, ConstraintValidatorContext context) {
        if (args.length != 2) {
            throw new IllegalArgumentException("Illegal method signature");
        }

        if (args[0] == null || args[1] == null) {
            return true;
        }

        int a = (int)args[0];
        int b = (int)args[1];

        return a < b;
    }
}
CrossParameterValidationBean.java
package sample.bean_validation.bean;

import sample.bean_validation.constraint.CrossParameterValidation;

public class CrossParameterValidationBean {

    @CrossParameterValidation
    public void method(int a, int b) {
    }
}
TestEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.CrossParameterValidationBean;

@Stateless
public class TestEjb {

    public void crossParameterValidation() throws Exception {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        ExecutableValidator validator = factory.getValidator().forExecutables();

        CrossParameterValidationBean bean = new CrossParameterValidationBean();
        Method method = CrossParameterValidationBean.class.getMethod("method", int.class, int.class);

        Object[] parameters = {20, 10};

        Set<ConstraintViolation<CrossParameterValidationBean>> constraintViolations
                = validator.validateParameters(bean, method, parameters);

        this.showConstraintViolation(constraintViolations);
    }

    ...
}
GlassFishコンソール出力
情報:   message = 第一引数 < 第二引数 となるようにしてください。
  • 複数の引数をまたがる検証を Cross-parameter Constraints と呼ぶ。
  • Cross-parameter Constraints は、バリデーターを以下のように定義する。
    • クラスを @SupportedValidationTarget(ValidationTarget.PARAMETERS) でアノテートする。
    • 検証対象の型を Object[] で定義する。
    • 引数の数が想定と異なる場合は IllegalArgumentException を投げとく。
    • 検証対象の引数のいずれかが null の場合は、検証 OK にする(null 不可のチェックは、 @NotNull で明示する)。

他のフレームワークとの連携

他の Java EE フレームワークと連携すると、どんな感じになるのか。
ざっくり確認してみる。

CDI

CdiBean.java
package sample.bean_validation.bean.cdi;

import javax.enterprise.context.RequestScoped;
import javax.validation.constraints.NotNull;

@RequestScoped
public class CdiBean {
    @NotNull
    private String value;

    public void setValue(@NotNull String value) {
        this.value = value;
    }

    @NotNull
    public String method() {
        return this.value;
    }
}
CdiIntegrationEjb.java
package sample.bean_validation.ejb;

import java.util.Set;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import sample.bean_validation.bean.cdi.CdiBean;

@Stateless
public class CdiIntegrationEjb {
    @Inject
    private Validator validator; // ★ Validator をインジェクション
    @Inject
    private CdiBean bean;

    public void execute() {
        Set<ConstraintViolation<CdiBean>> constraintViolations = this.validator.validate(this.bean);
        System.out.println("size = " + constraintViolations.size());
        constraintViolations.forEach(cv -> System.out.println("message = " + cv.getMessage()));

        this.invoke(() -> this.bean.setValue(null));
        this.invoke(this.bean::method);
    }

    private void invoke(Runnable runnable) {
        try {
            runnable.run();
        } catch (ConstraintViolationException e) {
            System.out.println(e.getMessage());
        }
    }
}
GlassFishコンソール出力
情報:   size = 1
情報:   message = may not be null

情報:   1 constraint violation(s) occurred during method validation.
Constructor or Method: public void sample.bean_validation.bean.cdi.CdiBean.setValue(java.lang.String)
Argument values: [null]
Constraint violations: 
 (1) Kind: PARAMETER
 parameter index: 0
 message: may not be null
 root bean: sample.bean_validation.bean.cdi.CdiBean$Proxy$_$$_WeldSubclass@2010a8e5
 property path: setValue.arg0
 constraint: @javax.validation.constraints.NotNull(message={javax.validation.constraints.NotNull.message}, groups=[], payload=[])

情報:   1 constraint violation(s) occurred during method validation.
Constructor or Method: public java.lang.String sample.bean_validation.bean.cdi.CdiBean.method()
Argument values: []
Constraint violations: 
 (1) Kind: RETURN_VALUE
 message: may not be null
 root bean: sample.bean_validation.bean.cdi.CdiBean$Proxy$_$$_WeldSubclass@2010a8e5
 property path: method.<return value>
 constraint: @javax.validation.constraints.NotNull(message={javax.validation.constraints.NotNull.message}, groups=[], payload=[])
  • Java EE 環境で使う場合、コンテナにすでに Validator が登録されているので、 @Inject でインジェクションできる。
  • CDI 管理ビーンの場合、メソッドを実行すれば勝手にパラメータや戻り値の検証が行われる。
    • 検証の結果、エラーがあった場合は ConstraintViolationException がスローされる。

自作バリデーター

CdiIntegrateValidation.java
package sample.bean_validation.constraint;

...
import sample.bean_validation.validator.CdiIntegrateValidator;

@Constraint(validatedBy = CdiIntegrateValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CdiIntegrateValidation {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
CdiIntegrateValidator.java
package sample.bean_validation.validator;

...
import sample.bean_validation.bean.cdi.Hoge;
import sample.bean_validation.constraint.CdiIntegrateValidation;

// ★ スコープアノテーションは設定していない
public class CdiIntegrateValidator implements ConstraintValidator<CdiIntegrateValidation, String>{

    @Inject
    private Hoge hoge; // ★ @Inject でインジェクションしてみる

    @Override
    public void initialize(CdiIntegrateValidation constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        this.hoge.method();
        return true;
    }
}
Hoge.java
package sample.bean_validation.bean.cdi;

import javax.enterprise.context.RequestScoped;

@RequestScoped // ★ただの CDI 管理ビーン
public class Hoge {

    public void method() {
        System.out.println("Hoge.method()");
    }
}
CdiIntegrateValidationBean.java
package sample.bean_validation.bean.cdi;

import sample.bean_validation.constraint.CdiIntegrateValidation;

public class CdiIntegrateValidationBean {
    @CdiIntegrateValidation // ★制約を設定
    private String value;
}
CdiIntegrationEjb.java
package sample.bean_validation.ejb;

...
import sample.bean_validation.bean.cdi.CdiIntegrateValidationBean;

@Stateless
public class CdiIntegrationEjb {
    @Inject
    private Validator validator;

    ...

    public void validator() {
        CdiIntegrateValidationBean bean = new CdiIntegrateValidationBean();
        this.validator.validate(bean); // ★検証実行
    }

    ...
}
GlassFishコンソール出力
情報:   Hoge.method()
  • バリデーターを作成すると、普通に @Inject で他のビーンをインジェクションできるようになっている。

JPA

Sample.java
package sample.bean_validation.entity;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;

@Entity
@Table(name="sample_table")
public class Sample {
    @Id
    private int id;
    @NotNull // ★ 制約を設定
    private String value;
}
JpaIntegrationEjb.java
package sample.bean_validation.ejb;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.validation.ConstraintViolationException;
import sample.bean_validation.entity.Sample;

@Stateless
public class JpaIntegrationEjb {
    @PersistenceContext(unitName="sample")
    private EntityManager em;

    public void execute() {
        Sample sample = new Sample();
        try {
            this.em.persist(sample); // ★ 永続化を試みる
        } catch (ConstraintViolationException e) {
            System.out.println(e.getMessage());
            e.getConstraintViolations().forEach(cv -> System.out.println("message = " + cv.getMessage()));
        }
    }
}
GlassFishコンソール出力
情報:   Bean Validation constraint(s) violated while executing Automatic Bean Validation on callback event:'prePersist'. Please refer to embedded ConstraintViolations for details.
情報:   message = may not be null
  • エンティティに制約アノテーションを設定することで、検証ができる。
  • pre-persist, pre-update, pre-remove のライフサイクルイベントのタイミングで、バリデーションが自動で実行される。
  • バリデーション処理は、 @PrePersist などのライフサイクルメソッドの実行が完了したあとで行われる。

JSF

JsfIntegrationBean.java
package sample.bean_validation.bean.jsf;

import javax.enterprise.inject.Model;
import javax.validation.constraints.Pattern;

@Model
public class JsfIntegrationBean {
    @Pattern(regexp="[a-z]+") // ★半角英字のみ可
    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public void method() {
        System.out.println("value=" + value);
    }
}
index.html
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:jsfc="http://xmlns.jcp.org/jsf">
  <head>
    <meta charset="UTF-8" />
    <title>JSF &amp; Bean Validation</title>
  </head>
  <body>
    <h:messages />

    <form jsfc:id="form">
      <input type="text" jsfc:value="#{jsfIntegrationBean.value}" />
      <input type="submit" jsfc:action="#{jsfIntegrationBean.method()}" value="Submit" />
    </form>
  </body>
</html>

bean-validation.JPG

↓ submit

bean-validation.JPG

  • バッキングビーンに制約アノテーションを設定することで、検証ができる。

JAX-RS

MyApplication.java
package sample.bean_validation.jaxrs;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/jaxrs")
public class MyApplication extends Application {
}
JaxrsIntegrationResource.java
package sample.bean_validation.jaxrs;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Path("/integration")
public class JaxrsIntegrationResource {

    @GET // ★クエリパラメータを受け取る引数に、制約アノテーションのを合わせて設定
    public void method(@QueryParam("value") @Pattern(regexp="[A-Z]+") String value) {
    }

    @GET @Path("/return") @NotNull // ★戻り値を検証
    public String method2() {
        return null;
    }
}
curlで検証
> curl -I http://localhost:8080/bean-validation/jaxrs/integration?value=Hoge
HTTP/1.1 400 Bad Request
...

> curl -I http://localhost:8080/bean-validation/jaxrs/integration/return
HTTP/1.1 500 Internal Server Error
...
  • パラメータを検証してエラーになった場合、自動的に 400 Bad Request が返される。
  • 戻り値を検証してエラーになった場合、自動的に 500 Internal Server Error が返される。

ビルトインのアノテーション

アノテーション 検証内容
@AssertFalse 値が false であることを検証する。
@AssertTrue 値が true であることを検証する。
@DecimalMax 実数項目が、指定した値以下であることを検証する。
@DecimalMin 実数項目が、指定した値以上であることを検証する。
@Digits integer で実数部の桁数を、 fraction で小数部の桁数を検証する。
@Future 日付項目が未来日付であることを検証する。
@Past 日付項目が過去日付であることを検証する。
@Max 整数項目が、指定した値以下であることを検証する。
@Min 整数項目が、指定した値以上であることを検証する。
@NotNull 値が null でないことを検証する。
@Null 値が null であることを検証する。
@Pattern 文字列が、指定した正規表現のパターンに一致することを検証する。
@Size 文字列またはコレクションのサイズ(最大・最小)を検証する。

参考