JavaEE7をはじめよう(23) - Bean Validationの基本
今回から数回に分けて、Bean Validation を紹介する。
Bean Validation(JSR-303)は JavaBeans のバリデーションを行う仕組みである。Java EE6 から追加され、Java EE7 でのバージョンは1.1である。
Bean Validationとは
Bean Validation は、JavaBeans のプロパティが取り得る値や条件を、制約のアノテーションとして設定することで、バリデーション内容を定義する仕組みである。そのため、これを利用すれば、バリデーションの定義やロジックを色々な場所に書く必要がなくなり、一元的な管理が可能になる。また、バリデーションに関するエラー時のメッセージ設定や、独自バリデーションの作成も行える。
バリデーションの仕組みとして独立しているため、様々なフレームワークに組み込むことができる。Java EE では、JSF(2.0以降)、JAX-RS および JPA(2.0以降)に組み込まれている。Spring などにも導入されており、Java SE 環境でも利用できる。
今回は、Bean Validation の基本的な使用方法である組み込みのアノテーションやメッセージの定義方法を紹介する。
ライブラリの設定
Bean Validation は、Java EE 環境ではサーバーランタイムに含まれているが、 Java SE 環境で使用するためには、以下のライブラリが必要となる。
- Bean Validation API
Bean Validation: Bean Validation - Bean Validation の実装ライブラリ
参照実装として、Hibernate Validator がある。
また、メッセージで EL 式を使用できるため、本記事では EL 式のライブラリも利用する。
- EL式の実装ライブラリ
Expression Language Specification — Project Kenai など。
参考として、参照実装であるHibernate Validatorを使用する Maven の設定例を以下に示す。
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.2.0.Alpha1</version> </dependency> <dependency> <groupId>org.glassfish</groupId> <artifactId>javax.el</artifactId> <version>3.0.0</version> </dependency>
アノテーションの設定例
まず JavaBeans に Bean Validation のアノテーションを設定した例を示そう。
public class SampleBean { @NotNull @Size(min = 3, max = 20) @Pattern(regexp = "^.+ .+$", message = "[${validatedValue}]は[{regexp}]に一致しません。" +"姓と名の間にスペースを入れてください。") private String name; @NotNull @Min(value = 10, message = "{age.minimum}") private Integer age; public SampleBean(String name, Integer age) { this.name = name; this.age = age; } // getter/setter }
組み込みアノテーションの概要
Bean Validation にあらかじめ組み込まれたアノテーションには、上記の@NotNull
, @Size
, @Pattern
, @Min
などがあり、javax.validation.constraints
パッケージに定義されている。
型ごとに適用可能な制約をまとめると以下のようになる。これらがどのような制約なのかは、アノテーション名から想像できるだろう。
- 参照型全般用 -
@NotNull
,@Null
- boolean用 -
@AssertTrue
,@AssertFalse
- 文字列用 -
@Size
,@Pattern
- 数値用 -
@Digits
,@Min
,@Max
,@DecimalMin
,@DecimalMax
- 日付用 -
@Future
,@Past
- コレクション、マップ、配列用 -
@Size
(文字列のものと同様)
詳しい内容は、仕様や Javadoc を参照いただきたい。
Bean Validation: Bean Validation 1.1 Specification
アノテーションはそれぞれの制約に応じた属性を持つ。たとえば@Size
であれば、文字列の最小桁数(min
)と最大桁数(max
)、@Pattern
であれば正規表現(regex
)などだ。
また、以下の共通属性も定義されている。
message
- 制約違反時のメッセージ。後述する。groups
- 状況に応じて制約チェックの実行の是非を判別させるための属性。次回以降の記事で解説する。payload
- 制約違反に対して重要度などの任意のカテゴリを付与する属性。必要に応じて使用する。List
- ネストしたアノテーションで、同じ制約を異なる条件で複数定義する場合に用いる。次回以降の記事で解説する。
注意すべきは、これらの共通属性は独自のアノテーションを作成する際にも定義する必要があることだ。(定義しなくても、コンパイラは警告を出さないため、忘れないように注意する必要がある。)
またアノテーションは、JavaBeans のプロパティに対して付与するものなので、フィールドに設定してもよいし、getterメソッド(getXX, isXX)にも付与してもよい。後者の場合、getter メソッドの戻り値に対してチェックが行われるので、この仕組みを利用して相関チェックなどの複数フィールドの制約チェックを行うことも可能だ。(詳細は次回解説する。)
Validator
の取得
バリデーションは、javax.validation.Validator
のインスタンスメソッドを呼び出すことで行う。
Validator
のインスタンスは以下の方法で取得可能だ。
- Java SE 環境
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Inject Validator validator
バリデーションを行うためのメソッド
バリデーションを行うためのメソッドは3つある。いずれのメソッドも戻り値として、ConstraintViolation
という制約違反の内容を格納したオブジェクトのセットを返す。違反が無ければ、セットのサイズは0件である。
validate(T t, Class<?>... groups)
JavaBeans のインスタンス
t
を受け取り、JavaBeans の全てのプロパティ(フィールド、getter メソッド)の制約チェックを行う。validateProperty(T t, String property, Class<?>... groups)
JavaBeans のインスタンス
t
について、引数property
で指定した単一のプロパティのみの制約チェックを行う。validateValue(Class<T> clazz, String property, Object value, Class<?>... groups)
引数の
clazz
で指定された JavaBeans のプロパティ(property
)に、指定した値(value
)を設定できるかどうかをチェックする。
ただし、value
とプロパティの型が合わない場合(整数型のプロパティに文字列を渡した場合など)は、実行時例外(ValidationException
)が発生するので、最低限でも型はあわせておく必要がある。
最初の2つは、JavaBeans のプロパティが設定されている前提でバリデーションを行う。
いずれのメソッドも最後に可変長引数groups
を取っている。これは、Bean Validation のアノテーションに保持する属性groups
と関係があり、これを指定すると制約チェックの内容を変えることができる。可変長引数なので、何もしない場合は指定不要である。
バリデーションの実行例
JUnit のテストクラスによる実行例を示す。
最初のテストでは、JavaBeans のプロパティに適切な値を設定しているため、違反は0件である。
public class SampleBeanTest { // 1. validatorの取得 static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); @Test public void 検証成功テスト() { Set<ConstraintViolation<SampleBean>> result = validator.validate(new SampleBean("yamada taro", 23)); // 制約違反無し。 assertThat(result.size(), is(0)); }
次に示す2つめのテストではSampleBean
のname
とage
にnull
を設定しているため、@NotNull
制約の違反となる。両方のプロパティについてチェックするため、2件の違反が起きる。
違反(ConstraintViolation
) に対する検証内容は以下の通りだ。
- プロパティ名(
getPropertyPath
メソッドで取得する) が、SampleBean
のage
,name
であること。 - エラーメッセージが、
@NotNull
制約 のデフォルトメッセージであるmay not be null
であること。
なお、その他の組み込み制約もチェックは実行しているが、入力値がnull
の場合はチェックされない。
@Test public void null値のテスト() { Set<ConstraintViolation<SampleBean>> result = validator.validate(new SampleBean(null, null)); // Nullチェックのみの違反が計上 assertThat(result.size(), is(2)); List<ConstraintViolation<SampleBean>> act = sort(result); // 違反となったプロパティとメッセージの検証 // null チェック違反のメッセージはデフォルト assertThat( act.get(0).getPropertyPath().toString(), is("age")); assertThat(act.get(0).getMessage(), is("may not be null")); assertThat( act.get(1).getPropertyPath().toString(), is("name")); assertThat(act.get(1).getMessage(), is("may not be null")); } // プロパティ、メッセージ順でソートする内部メソッド private <T> List<ConstraintViolation<T>> sort( Set<ConstraintViolation<T>> set) { return set.stream().sorted((o1, o2) ->{ int c = o1.getPropertyPath().toString().compareTo( o2.getPropertyPath().toString()); return c == 0 ? o1.getMessage().compareTo(o2.getMessage()) : c; }).collect(Collectors.toList()); }
3つめのテストではnull
値以外の制約違反となることを検証している。name
プロパティについては、@Size
, @Pattern
の両方で違反しているので、合計3件の違反が計上される。
@Test public void null値以外の検証失敗テスト() { Set<ConstraintViolation<SampleBean>> result = validator.validate(new SampleBean("EE",9)); for (ConstraintViolation<SampleBean> v : result) { System.out.println(v.getPropertyPath() + v.getMessage()); } // Nullチェック以降の制約違反が計上される assertThat(result.size(), is(3)); List<ConstraintViolation<SampleBean>> act = sort(result); // 違反となったプロパティとメッセージの検証 assertThat( act.get(0).getPropertyPath().toString(), is("age")); // このメッセージはプロパティファイルから取得 assertThat(act.get(0).getMessage(), is("10以上で入力してください。1少ないです。")); assertThat( act.get(1).getPropertyPath().toString(), is("name")); // このメッセージはデフォルトメッセージを上書きしたもの。 assertThat(act.get(1).getMessage(), is("3以上20以下で入力してください。")); assertThat( act.get(2).getPropertyPath().toString(), is("name")); // このメッセージは、アノテーションのmessage属性から。 assertThat(act.get(2).getMessage(), is("[EE]は[^.+ .+$]に一致しません。" + "姓と名の間にスペースを入れてください。")); } }
このテストを実行すると、age
属性については次のメッセージが生成される。
10以上で入力してください。1少ないです。
またname
属性については、以下の2つのメッセージが生成される。
3以上20以下で入力してください。
[EE]は[^.+ .+$]に一致しません。姓と名の間にスペースを入れてください。
制約違反時のメッセージについて
@NotNull
制約違反時のデフォルトのメッセージは、2つめのテストの結果を見ればわかるとおり、may not be null
である。
これらのメッセージはカスタマイズできる。以下に3つのカスタマイズ方法を示す。
方法1:メッセージ属性の指定
1つめの方法はアノテーションにmessage
属性を指定することである。
対象とするアノテーションは、前述のSampleBean
の@Pattern
や@Min
が該当する。以下にサンプルコードを再掲する。
@NotNull @Size(min = 3, max = 20) @Pattern(regexp = "^.+ .+$", message = "[${validatedValue}]は[{regexp}]に一致しません。" +"姓と名の間にスペースを入れてください。") private String name;
ここでは@Pattern
に対してmessage
属性を指定しており、{regexp}
のように{}
で囲むことで、メッセージ内にアノテーションの属性の値を埋め込んでいる。
また、${}
による EL 式も指定できる。EL 式の中ではアノテーションの属性だけでなく、${validatedValue}
で入力値を取得することもできる。
先ほどの3つめのテストでは、@Pattern
のmessage
属性の可変部に埋め込む内容をConstraintValiolation
から取得していることを確認して欲しい。
方法2:メッセージプロパティによる一部変更
2つめの方法は、メッセージプロパティファイルを使用して、メッセージに一部を書き換えるやり方である。
SampleBean
の@Min
のmessage
属性の{age.minimum}
は、プロパティファイルからメッセージを取得することを指す。
{}
には、アノテーションの属性のほか、メッセージプロパティのキーを指定することができる。
プロパティファイルは、クラスパスのルート直下に、ValidationMessages.properties
として用意する必要がある。
(ValidationMessages_ja.properties
のようにロケールをつけて、多言語化することも可能だ。)
今回定義したValidationMessages.properties
では、以下のように定義している。
age.minimum={value}以上で入力してください。${value - validatedValue}少ないです。
EL式ではある程度の演算も可能で、上記メッセージでは${value - validatedValue}
のように、入力数値から足りない数を求めている。
方法3:メッセージ全体の変更
3つめの方法は、メッセージプロパティファイルを使用して、メッセージ全体を書き換えるやり方だ。
今回のValidationMessages.properties
では、次のようにjavax.validation.constraints.Size.message
というメッセージ定義を行っている。
javax.validation.constraints.Size.message = {min}以上{max}以下で入力してください。
これは、@Size
アノテーションのデフォルトのメッセージ属性である。
(@Size
のメッセージ属性のデフォルト値は、{javax.validation.constraints.Size.message}
である。)
このように、ValidationMessages.properties
で組み込みアノテーションのデフォルトメッセージプロパティのメッセージ全体を上書きすることもできる。
最も汎用性が高いのはこの方法だろう。
注意点
上記のメッセージ文言を見て気づかれたかもしれないが、Bean Validation のメッセージには、${validatedValue}
というパラメータで違反を起こした入力値を設定することはできても、違反を起こしたプロパティ名を設定することはできない。
(組み込みアノテーションには、プロパティ名を設定する属性は無い。また、プロパティ名を取得できたとしても、日本語の項目名称が取れるわけではない。)
もしプロパティ名(あるいはユーザーインターフェース上の項目名など)が必要なら、アノテーションのメッセージ属性に、固定で名称を書いてしまうのも方法の一つだ。ただし、この方法は多言語対応ができないのが欠点である。
あるいは、前述の JUnit の結果で示したように、制約違反の情報を格納してあるConstraintValiolation
にはメッセージの他に、プロパティ名なども取得できるので、それを使ってメッセージを編集する処理を入れるとよいだろう。
実際に、JSF や Spring MVC など、Bean Validation を組み込むいくつかのフレームワークでは、名称の取得をサポートしている。
まとめ
Bean Validation の組み込みアノテーションとメッセージの定義について説明した。
Bean Validation は、単体でも利用できる。典型的なチェックロジックを削減できるので、活用してみてはどうだろうか。
次回は、カスタム制約の作成方法を紹介する。
[前多 賢太郎]