エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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 環境で使用するためには、以下のライブラリが必要となる。

また、メッセージで EL 式を使用できるため、本記事では EL 式のライブラリも利用する。

参考として、参照実装である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インスタンスは以下の方法で取得可能だ。

Validator validator 
      = Validation.buildDefaultValidatorFactory().getValidator();
  • Java EE 環境(CDIによるインジェクション)
@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つめのテストではSampleBeannameagenullを設定しているため、@NotNull制約の違反となる。両方のプロパティについてチェックするため、2件の違反が起きる。

違反(ConstraintViolation) に対する検証内容は以下の通りだ。

  1. プロパティ名(getPropertyPath メソッドで取得する) が、 SampleBeanage , name であること。
  2. エラーメッセージが、 @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つめのテストでは、@Patternmessage属性の可変部に埋め込む内容をConstraintValiolationから取得していることを確認して欲しい。

方法2:メッセージプロパティによる一部変更

2つめの方法は、メッセージプロパティファイルを使用して、メッセージに一部を書き換えるやり方である。

SampleBean@Minmessage属性の{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 は、単体でも利用できる。典型的なチェックロジックを削減できるので、活用してみてはどうだろうか。

次回は、カスタム制約の作成方法を紹介する。

[前多 賢太郎]