Java
BeanValidation
入力チェック
TERASOLUNA5

Bean Validationで簡単入力チェック!

1. はじめに

今回はBean Validationを利用した入力チェックの方法について説明したいと思います。

  • Bean Validationの使い方
    • 入力チェックの対象クラスにチェックに応じたアノテーションを付与する
    • チェック対象となるインスタンスを生成する
    • Validatorvalidateメソッドにチェック対象のインスタンスを指定し、入力チェックを実行する

2. ライブラリの用意

必要となるライブラリを依存関係に追加します。
hibernate-validatorのメッセージ解決で必要となるためjavax.elも一緒に追加しています。
なお、Bean Validationのライブラリ(groupId:javax.validation、artifactId:validation-api)を記述していませんが、依存関係があるため自動で依存関係に追加されます。

pom.xml
  <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.9.Final</version>
  </dependency>
  <dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b09</version>
  </dependency>

3. モデルの定義

チェック対象となるモデルクラスを定義します。この際、各チェックに応じたアノテーションを付与します。付与可能なアノテーションは以下の3種類です。

数値の最大値を制限する@Maxのように、チェックによっては制限値が必要になりますが、これはアノテーションの属性として設定します。設定可能な属性はアノテーションごとに異なります。
チェックでエラーになった際に表示するメッセージを変更したい場合、message属性でオーバーライドします。message属性で設定したメッセージが最優先されます。

Book.java
package com.example.beanvalidation.app;

import java.io.Serializable;
import java.util.Date;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotNull
    // @Pattern(regexp = "[0-9,-]+")
    @Pattern(regexp = "[0-9,-]+", message = "入力された値  ${validatedValue} は正しくありません。ISBMの形式で入力してください。")
    private String isbn;

    @NotNull
    @Size(max = 200)
    private String title;

    @Max(9999)
    @Min(1)
    private int pageCount;

    @NotNull
    @Past
    private Date publishedDate;

    // constructor, setter, getter omitted
}

4. エラーメッセージの変更

Bean Validationは仕様のためのエラーメッセージは実装ライブラリに依存します。今回であればhibernate-validatorが定義したメッセージがデフォルトになります。
このデフォルトメッセージを変更したい場合、クラスパス直下に配置したValidationMessages.propertiesでオーバライドすることが可能です。

  • プロパティのキー : アノテーションのFQCN.message
  • プロパティの値 : デフォルトメッセージの内容

今回は@Pastのデフォルトメッセージを変更してみたいと思います。

クラスパス直下のValidationMessages_ja.properties(日本語用)
# javax.validation.constraints.Past.message=入力された日付 ${validatedValue} は正しくありません。過去日を入力してください。
javax.validation.constraints.Past.message=\u5165\u529B\u3055\u308C\u305F\u65E5\u4ED8 ${validatedValue} \u306F\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002\u904E\u53BB\u65E5\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002

(注意)

  • STS(Spring Tool Suite)のプロパティエディタでは自動で変換してくれるため日本語がunicode変換されています。
  • メッセージ内で入力された値は${validatedValue}で参照可能です。
  • ValidationMessages.propertiesは多言語対応可能で日本語の場合はValidationMessages_ja.propertiesになります。

5. 基本的な使い方

前述で定義したBookクラスにいろいろ値を設定して、実際に入力チェックを実施してみたいと思います。
DIコンテナを利用するとValidatorをインジェクションするのが一般的ですが、今回はmainメソッドのある普通のJavaアプリとします。
といってもValidatorの使い方は同じです。

ValidationDemo.java
package com.example.beanvalidation.app;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

public class ValidationDemo {

    public static void main(String[] args) {
        // 1. create validator
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        // 2. create target object
        Book book = createBook();

        // 3. validate
        Set<ConstraintViolation<Book>> constraintViolations = validator
                .validate(book);

        // 4. check result
        int errorCount = constraintViolations.size();
        System.out.println("validate error count : " + errorCount);
        if (errorCount > 0) {
            showErrorDetails(constraintViolations);
        }

        System.out.println("=== demo : validate error ===");

        // 2. create target object
        book = createNgBook();

        // 3. validate
        constraintViolations = validator.validate(book);

        // 4. check result
        errorCount = constraintViolations.size();
        System.out.println("validate error count : " + errorCount);
        if (errorCount > 0) {
            showErrorDetails(constraintViolations);
        }
    }

    private static <T> void showErrorDetails(
            Set<ConstraintViolation<T>> constraintViolations) {
        for (ConstraintViolation<T> violation : constraintViolations) {
            System.out.println("----------");
            System.out.println(
                    "MessageTemplate : " + violation.getMessageTemplate());
            System.out.println("Message : " + violation.getMessage());
            System.out.println("InvalidValue : " + violation.getInvalidValue());
            System.out.println("PropertyPath : " + violation.getPropertyPath());
            System.out.println("RootBeanClass : " + violation.getRootBeanClass());
            System.out.println("RootBean : " + violation.getRootBean());
        }
    }

    private static Book createBook() {
        Date publishedDate = Date.from(LocalDateTime.of(2017, 7, 21, 0, 0, 0)
                .toInstant(ZoneOffset.ofHours(9)));
        return new Book("978-4798142470",
                "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 744,
                publishedDate);
    }

    private static Book createNgBook() {
        Date publishedDate = Date.from(LocalDateTime.of(2999, 7, 21, 0, 0, 0)
                .toInstant(ZoneOffset.ofHours(9)));
        return new Book("ERROR-ISBN-9999", null, 0, publishedDate);
    }
}

6. 実行結果(おまけ)

参考までに、前述のプログラムを実行した際の結果を以下に示します。

実行結果
validate error count : 0
=== demo : validate error ===
validate error count : 4
----------
MessageTemplate : {javax.validation.constraints.Min.message}
Message : must be greater than or equal to 1
InvalidValue : 0
PropertyPath : pageCount
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
----------
MessageTemplate : {javax.validation.constraints.Past.message}
Message : 入力された日付 Sun Jul 21 00:00:00 JST 2999 は正しくありません。過去日を入力してください。
InvalidValue : Sun Jul 21 00:00:00 JST 2999
PropertyPath : publishedDate
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
----------
MessageTemplate : {javax.validation.constraints.NotNull.message}
Message : must not be null
InvalidValue : null
PropertyPath : title
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]
----------
MessageTemplate : 入力された値  ${validatedValue} は正しくありません。ISBMの形式で入力してください。
Message : 入力された値  ERROR-ISBN-9999 は正しくありません。ISBMの形式で入力してください。
InvalidValue : ERROR-ISBN-9999
PropertyPath : isbn
RootBeanClass : class com.example.beanvalidation.app.Book
RootBean : Book [isbn=ERROR-ISBN-9999, title=null, pageCount=0, publishedDate=Sun Jul 21 00:00:00 JST 2999]

7. さいごに

今回はBean Validationによる入力チェックの方法について説明しました。
チェック用のアノテーションを付与するだけで、入力チェックの仕様を定義できるのでシンプルで分かりやすいかと思います。