Bean Validation とは
バリデーション(入力チェック)用のフレームワーク。
入力チェックは様々なレイヤに分散されやすい。
例えば、桁数やフォーマットのような形式チェックはプレゼンテーションレイヤに、マスタの存在や他のデータとの関連が正しいかなどのビジネスロジックのチェックはそれより深いレイヤなどに分かれたりすることがある。
こういった分散されやすい入力チェックを、一箇所にまとめまられるようにしようという目標のもと作られたものらしい。
入力チェックのルール(制約)はアノテーションで定義する。
null チェックなどの汎用的なものはあらかじめ定義されている。もし標準のアノテーションでは足りない場合は、独自にアノテーションやバリデーションロジックを定義することもできる。
この仕様は、単体で利用するよりかは他のフレームワーク(JPA, JAX-RS, JSF など)と連携することで利用されることが多い(と思う)。
Hello World
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());
}
}
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()
メソッドを使う。- 引数に検証したいオブジェクトを渡す。
- 検証結果は
ConstraintViolation
のSet
で返される。 - Bean 側(
HelloBean
)は、@NotNull
でフィールドをアノテートすることで制約を定義している。- 制約を定義するためのアノテーションを Constraint Annotation と呼ぶ(ここでは制約アノテーションと呼称する)。
フィールドを直接検証対象にする
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;
}
}
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());
});
}
}
情報: size = 1
情報: message = may not be null
- 制約アノテーションでフィールドを直接アノテートすると、そのフィールドは検証対象になる。
- フィールドを直接アノテートした場合、アクセサーメソッド(
getValue()
)を経由することなく検証が行われる。
プロパティを検証対象にする
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;
}
}
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);
}
...
}
情報: PropertyValidationBean.getValue()
情報: size = 1
情報: message = may not be null
- ゲッターメソッドを制約アノテーションでアノテートすると、そのゲッターメソッドで取得できる値(JavaBeans におけるプロパティ)が検証対象になる。
- プロパティは、アクセサーメソッドを経由して値が取得される。
他の Bean をフィールドとして持つときに、その Bean も検証対象にする
まずはデフォルトの動きを確認
package sample.bean_validation.bean;
public class FooBean {
private BarBean bar = new BarBean(); // ★他の Bean を持つ
}
package sample.bean_validation.bean;
import javax.validation.constraints.NotNull;
public class BarBean {
@NotNull
private String value;
}
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);
}
...
}
情報: size = 0
- エラー無しと判定される。
- つまり、デフォルトだと
FooBean
クラスをチェックしても、そのフィールドであるBarBean
まで検証は行われない。
BarBean
も検証対象にする
package sample.bean_validation.bean;
import javax.validation.Valid;
public class FooBean {
@Valid
private BarBean bar = new BarBean();
}
情報: size = 1
情報: message = may not be null
-
bar
フィールドを@Valid
でアノテートすることで、BarBean
の検証が実行されるようになる。
コレクションを検証対象にする
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);
}
...
}
情報: size = 2
情報: message = may not be null
情報: message = may not be null
- 配列、
Iterable
を実装したコレクション、Map
のいずれかのフィールドを@Valid
でアノテートすると、各要素に対して検証が行われるようになる。
ConstraintViolation から取得できる情報
ConstraintViolation
は直訳すると制約違反。
検証エラーになったときに、エラーの情報が格納されている。
package sample.bean_validation.bean;
import javax.validation.Valid;
public class RootBean {
@Valid
private LeafBean leaf = new LeafBean();
}
package sample.bean_validation.bean;
import javax.validation.constraints.Min;
public class LeafBean {
@Min(10)
private int number = 9;
}
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);
});
}
}
情報: 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 |
rootBean の Class オブジェクト。 |
invalidValue |
検証エラーになった対象の、実際の値。 |
propertyPath |
検証エラーになった対象を示すパス文字列。 |
leafBean |
検証エラーとなった Bean の Class オブジェクト。 |
constraintDescriptor |
制約アノテーションに設定されていた値など、制約に関するメタデータを持ったオブジェクト。 |
エラーメッセージ
任意のエラーメッセージを設定する
package sample.bean_validation.bean;
import javax.validation.constraints.NotNull;
public class CustomErrorMessageBean {
@NotNull(message="null はダメ!")
private String value;
}
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);
}
...
}
情報: message = null はダメ!
- 制約アノテーションには
message
という属性が定義されており、そこにエラーメッセージを設定することができるようになっている。
リソースバンドルを使用する
# ★ 実際は native2ascii でエンコードする
sample.bean_validation.notNull=null ダメ!絶対!
package sample.bean_validation.bean;
import javax.validation.constraints.NotNull;
public class ResourceBundleMessageBean {
@NotNull(message="{sample.bean_validation.notNull}")
private String value;
}
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);
}
...
}
情報: message = null ダメ!絶対!
- 基底名を
ValidationMessages
にしてリソースバンドルのファイルを作成する。- ファイルは、クラスパス直下に配置する。
- 制約アノテーションの
message
に、{}
で括る形でリソースバンドルのキーを指定する。-
{}
をメッセージパラメータと呼ぶ。
-
ビルトインの制約アノテーションのデフォルトエラーメッセージを上書きする
ビルトインの制約アノテーションには、全てデフォルトのエラーメッセージが定義されている。
これらのエラーメッセージの定義にも、リソースバンドルが使用されている。
GlassFish の場合、 Bean Validation の実装には RI である Hibernate Validator が使われており、デフォルトのリソースバンドルファイルはその中に存在している。
具体的には <GlassFish インストールディレクトリ>\glassfish\modules\bean-validator.jar
の中の org.hibernate.validator
の下に格納されている。
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
を作成すれば、デフォルトのエラーメッセージを上書きすることができる。
javax.validation.constraints.Null.message = null じゃないとダメだよ!
package sample.bean_validation.bean;
import javax.validation.constraints.Null;
public class OverrideDefaultMessageBean {
@Null
private String value = "xxx";
}
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);
}
...
}
情報: message = null じゃないとダメだよ!
制約アノテーションに設定されている属性値を参照する
package sample.bean_validation.bean;
import javax.validation.constraints.Max;
public class ReferenceAnnotationAttributeBean {
@Max(value=50, message="{value} 以下じゃないとダメです")
private int number = 51;
}
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);
}
...
}
情報: message = 50 以下じゃないとダメです
-
{<属性名>}
とすることで、制約アノテーションに設定した属性値を参照することができる。
EL 式を使う
package sample.bean_validation.bean;
import javax.validation.constraints.Max;
public class ELExpressionMessageBean {
@Max(value=30, message="{value} より ${validatedValue - value} も大きい値が渡された!")
private int number = 42;
}
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);
}
...
}
情報: message = 30 より 12 も大きい値が渡された!
- エラーメッセージの定義には EL 式が使える(
${}
)。 -
{}
のときと同じように、制約アノテーションの属性を参照できる(value
)。 -
validatedValue
で、検証対象の実際の値を参照できる。
{}
と ${}
の評価順序
上述の EL 式を用いたメッセージの定義を、以下のように宣言したとする。
@Max(value=30, message="${value} より ${validatedValue - value} も大きい値が渡された!")
private int number = 42;
先頭の {value}
を ${value}
という形で EL 式にしている。
すると、次のようにエラーメッセージが出力される。
情報: message = $30 より 12 も大きい値が渡された!
$30
と出力されてしまっている。
これは、 メッセージパラメータ({}
)の評価が EL 式(${}
)より先に実行されていることが原因となっている。
制約アノテーションの属性値を単独で参照する場合は、メッセージパラメータを使うのがいい。
フォーマッターを使用する
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;
}
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);
}
...
}
情報: message = 40 以下のみ可(実際=49)
-
formatter
という名前でフォーマッター参照できる。 - 使い方は java.util.Formatter と同じ。
制約をグルーピングする
基本
package sample.bean_validation.bean.group;
// ★グループは interface で定義しなければならない
public interface HogeGroup {
}
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;
}
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());
});
}
...
}
情報: [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
というグループが使用される。
複数のグループを指定する
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;
}
}
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);
}
...
}
情報: 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()
メソッドの第二引数以降で複数のグループを指定した場合、ぞれぞれのグループで検証が行われる。
グループの検証順序を指定する
package sample.bean_validation.bean.group;
import javax.validation.GroupSequence;
import javax.validation.groups.Default;
@GroupSequence({Default.class, HogeGroup.class})
public interface DefaultHoge {
}
package sample.bean_validation.bean.group;
import javax.validation.GroupSequence;
import javax.validation.groups.Default;
@GroupSequence({HogeGroup.class, Default.class})
public interface HogeDefault {
}
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; // ★こっちはエラーにならない
}
}
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);
}
...
}
情報: 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
属性で指定した順序で検証が行われるようになる。 - また、先に実行したグループで検証エラーがあった場合は、そこで検証が中断され、後続のグループの検証は行われない。
デフォルトの検証順序を設定する
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;
}
}
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);
}
...
}
情報: [no group]
情報: getNumber()
情報: getString()
情報: size = 1
情報: message = may not be null
- Bean を直接
@GroupSequence
でアノテートすることで、デフォルトの検証順序を設定できる。 -
Default
のグループを指定する場合は、Default.class
の代わりに Bean 自身のClass
オブジェクトを指定する。
関連する他の Bean を検証するときに、検証対象のグループを一時的に差し替える
まず、差し替えを行わない通常パターン。
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();
}
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);
}
...
}
情報: [no group]
情報: size = 1
情報: message = may not be null
ConvertGroupBean
の other
フィールドを @Valid
で紐付けている。
しかし、 OtherGroupBean
の方は number
フィールドが HogeGroup
で定義されている。
このため、 ConvertGroupBean
を validate()
で検証しても、グループの指定が実質 Default
になっており、 HogeGroup
である OtherGroupBean.number
は検証対象外になっている。
OtherGroupBean
を、 Default
ではなく HogeGroup
で検証させるには、以下のようにアノテーションを設定する。
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
フィールドも検証対象になる。
情報: [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つのターゲットに付与できるようにしている。
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;
}
}
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);
}
...
}
情報: [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
は非エラーとなっている。
制約をまとめる
複数箇所で同じ組み合わせの制約アノテーションを使用する場合、その組み合わせを任意の自作アノテーションにまとめることができる。
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 {};
}
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;
}
}
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);
}
...
}
情報: [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
属性は、とりあえず空({}
)で定義する。
-
-
@Retention
はRUNTIME
にする。 -
@Target
に、このアノテーションを設定する場所を指定する。 - まとめる対象の制約アノテーション(
@Max
,@Min
)でアノテートする。
-
- 次に、
message
,groups
,payload
という3つの属性を定義する。- これらは制約アノテーションが持たなければならない必須の属性になっている。
- 今回は特に使用する予定はないので、全て空で宣言している。
自作のバリデータと制約アノテーションを作成する
検証処理(バリデータ)と制約アノテーションは、任意のものを自作できる。
基本
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 {};
}
sample.bean_validation.constraint.CustomValidation.message="{value}" と "${validatedValue}" は別物
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);
}
}
package sample.bean_validation.bean;
import sample.bean_validation.constraint.CustomValidation;
public class CustomValidationBean {
@CustomValidation("hoge")
private String value = "Hoge";
}
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());
});
}
...
}
情報: 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
属性で指定した文字列と等しいことをチェックする、自作の制約アノテーションを実装している。 - 制約アノテーションの基本的な定義は、 制約アノテーションをまとめたアノテーションの作り方 と同じようにする(というか、どちらも同じもの)。
- 大きく異なるのは、
@Constraint
のvalidatedBy
属性に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()
は複数のスレッドから呼ばれることがあり得る。 - よって、このメソッドはスレッドセーフになるように作成しなければならない。
- 検証 OK の場合は
-
値が null
の場合は検証 OK にする
isValid()
メソッドに渡された値が null
の場合、検証は OK にすることが推奨されている。
値が null
のときにエラーとするのは @NotNull
で明示的に宣言すべきだからだと、仕様書に記載されている。
@List
インナーアノテーション
複数のアノテーションを同じターゲットに指定できるように、 List
というアノテーションを制約アノテーションのインナーアノテーションとして定義することが推奨されている。
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
という名前が推奨されている。
複数のフィールドをまたがる検証処理を実装する
ここまでの制約は、単一のフィールド(プロパティ)に対してのみ有効なものだった。
しかし、制約の中には複数のフィールドが関連し合ったものもあり得る。
バリデーターを自作することで、そういった検証処理も行えるようになる。
基本
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 {};
}
sample.bean_validation.constraint.ClassLevelValidation.message=フィールドの関係が不正です。
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;
}
}
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;
}
}
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);
}
...
}
情報: [no group]
情報: size = 1
情報: message = フィールドの関係が不正です。
情報: [no group]
情報: size = 1
情報: message = フィールドの関係が不正です。
- 制約アノテーションの
@Target
をTYPE
にする。- 制約アノテーションは、 Bean のクラスに直接付与する。
- バリデーターの検証対象の型を、対象の Bean の型にする。
- あとは、
isValid()
メソッドに Bean のインスタンスが渡されるので、フィールドの値を取得して検証を行う。
エラーメッセージをカスタマイズする
上記の実装だと、エラーメッセージが分かりにくくてやや不親切。
バリデーターの中で、具体的なエラー箇所が分かるようにメッセージを構築する。
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(); // ★ 新規に制約違反情報を追加
}
}
情報: [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 の作りを見る限り、他のフレームワークから利用されることを前提としており、直接利用することは無いと思う(個人的な見解)。
コンストラクタも Method
を Constructor
にすれば同じ要領で検証できる。
引数の検証
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());
});
}
...
}
情報: message = may not be null
- メソッドのパラメータにも、
@NotNull
などの制約アノテーションを設定できる。 - 検証には、
Validator#forExecutables()
で取得できる専用のバリデーターを使う。 - パラメータの検証には
ExecutableValidator#validateParameters()
メソッドを使用する。
戻り値の検証
package sample.bean_validation.bean;
import javax.validation.constraints.NotNull;
public class MethodReturnValueValidationBean {
@NotNull
public String method() {
return null;
}
}
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);
}
...
}
情報: message = may not be null
- メソッドの戻り値を検証する場合は、メソッド自体に制約アノテーションを設定する。
- バリデーションの実行には、
ExecutableValidator#validateReturnValue()
メソッドを使用する。
複数の引数にまたがる検証
引数が複数あり、それぞれの引数間に関連がある検証は、以下のように自作のバリデータと制約アノテーションを作成する。
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 {};
}
sample.bean_validation.constraint.CrossParameterValidation.message=第一引数 < 第二引数 となるようにしてください。
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;
}
}
package sample.bean_validation.bean;
import sample.bean_validation.constraint.CrossParameterValidation;
public class CrossParameterValidationBean {
@CrossParameterValidation
public void method(int a, int b) {
}
}
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);
}
...
}
情報: message = 第一引数 < 第二引数 となるようにしてください。
- 複数の引数をまたがる検証を Cross-parameter Constraints と呼ぶ。
- Cross-parameter Constraints は、バリデーターを以下のように定義する。
- クラスを
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
でアノテートする。 - 検証対象の型を
Object[]
で定義する。 - 引数の数が想定と異なる場合は
IllegalArgumentException
を投げとく。 - 検証対象の引数のいずれかが
null
の場合は、検証 OK にする(null
不可のチェックは、@NotNull
で明示する)。
- クラスを
他のフレームワークとの連携
他の Java EE フレームワークと連携すると、どんな感じになるのか。
ざっくり確認してみる。
CDI
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;
}
}
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());
}
}
}
情報: 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
がスローされる。
- 検証の結果、エラーがあった場合は
自作バリデーター
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 {};
}
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;
}
}
package sample.bean_validation.bean.cdi;
import javax.enterprise.context.RequestScoped;
@RequestScoped // ★ただの CDI 管理ビーン
public class Hoge {
public void method() {
System.out.println("Hoge.method()");
}
}
package sample.bean_validation.bean.cdi;
import sample.bean_validation.constraint.CdiIntegrateValidation;
public class CdiIntegrateValidationBean {
@CdiIntegrateValidation // ★制約を設定
private String value;
}
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); // ★検証実行
}
...
}
情報: Hoge.method()
- バリデーターを作成すると、普通に
@Inject
で他のビーンをインジェクションできるようになっている。
JPA
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;
}
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()));
}
}
}
情報: 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
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);
}
}
<?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 & 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>
↓ submit
- バッキングビーンに制約アノテーションを設定することで、検証ができる。
JAX-RS
package sample.bean_validation.jaxrs;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/jaxrs")
public class MyApplication extends Application {
}
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 -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 |
文字列またはコレクションのサイズ(最大・最小)を検証する。 |