Spring3系におけるJSR-303 Bean Validationについて | サイバーエージェント 公式エンジニアブログ
みなさん、はじめまして。
Amebaの基盤改善チームでアプリケーションエンジニアをしている森です。
仕事ではAmebaの会員登録システムを担当しています。

唐突ですが、今回このブログではJSR-303 Bean Validationについて導入方法を解説します。
このテーマを選んだ理由はSeasar(SAStruts)のようなアノテーションで行うバリデーションがSpring MVCで出来ないかなぁと思った事です。幸いSpring3.0からJSR-303 Bean Validationが標準サポートされた事でアノテーションの利用が簡単になりました。以下に詳しく見ていきます。

(JSR-303 Bean Validationの概要)
●JSR-303 Bean Validationの目的
Java仕様を策定しているJCP(Java Community Process)が、JavaBeanオブジェクトに関わる検証ロジックの共通化を図る事を目的に、「JSR-303 Bean Validation」を策定しました。

●JSR-303 Bean Validationの対象
JSR-303 Bean Validationでは、文字通りJavaBeanオブジェクトの検証ロジックを対象としています。具体的な利用シーンとして、Webアプリケーションの入力値検証、JavaBeanオブジェクトの永続化時のデータ検証などがあります。

●JSR-303 Bean Validationの特色
一般的に使用される検証ロジック定義をアノテーションで用意している点が特色です。また独自に定義した検証ロジックを利用する機構も含まれています。

●JSR-303 Bean Validationの参照実装
現時点で最も人気がある実装は「Hibernate Validator」です。元々はHibernateによるJavaオブジェクトの永続化時のデータ検証を目的として開発されたものですが、単体での利用も可能であり、JSR-303の参照実装として利用されています。今回はこのHibernate Validatorを利用したBean Validationの導入方法を解説します。

(Webアプリケーションへの導入方法)
●Webアプリケーションへの導入について
実際にサンプルアプリケーションを作成してみました。このアプリケーションではJSR-303 Bean Validationで標準サポートされているアノテーションを利用し、画面から受け取ったパラメータについてデータ検証を実施しています。

●使用するフレームワーク・ライブラリ
(フレームワーク)
・DIコンテナ : Spring 3.1
・プレゼンテーション : Spring MVC
・テンプレートエンジン : FreeMarker
・Bean Validation : Hibernate Validator

(ライブラリ)
・JSR-303 Bean Validation
  validation-api-1.0.0.GA.jar
  hibernate-validator-4.2.0.Final.jar
・Spring
  spring-context-3.1.1.RELEASE.jar
  spring-aop-3.1.1.RELEASE.jar
  aopalliance-1.0.jar
  spring-beans-3.1.1.RELEASE.jar
  spring-core-3.1.1.RELEASE.jar
  spring-expression-3.1.1.RELEASE.jar
  spring-asm-3.1.1.RELEASE.jar
  spring-context-support-3.1.1.RELEASE.jar
  spring-webmvc-3.1.1.RELEASE.jar
  spring-web-3.1.1.RELEASE.jar
  spring-test-3.1.1.RELEASE.jar
・テンプレートエンジン
  freemarker-2.3.18.jar
・日付変換ライブラリ
  joda-time-2.0.jar

(サンプルWeb アプリケーションの作成)
●FreeMarker 画面ファイル
$サイバーエージェント 公式エンジニアブログ
図1:001.ftl


各検証ロジックに対応した入力フォームを用意します。
またエラーメッセージ表示用のFreeMarkerマクロコード(後述)を使ってメッセージを表示させます。

●Spring MVC コントローラ

/**
*
* @param form
* @return
*/
@RequestMapping(value = { "show1" }, method = { RequestMethod.GET })
public String show1(SampleValidationForm form) {
return "001.ftl";
}

/**
*
* @param form
* @return
*/
@RequestMapping(value = { "show2" }, method = { RequestMethod.GET })
public String show2(SampleValidationForm form) {
return "002.ftl";
}

/**
*
* @param form
* @return
*/
@RequestMapping(value = { "submit1" }, method = { RequestMethod.POST })
public String submit1(@Valid SampleValidationForm form,
BindingResult result, Model model) {
if (result.hasErrors()) {
log.error("エラーあり");
} else {
log.info("エラーなし");
}
return "001.ftl";
}

/**
*
* @param form
* @return
*/
@RequestMapping(value = { "submit2" }, method = { RequestMethod.POST })
public String submit2(@Valid SampleValidationForm form,
BindingResult result, Model model) {
if (result.hasErrors()) {
log.error("エラーあり");
} else {
log.info("エラーなし");
}
return "002.ftl";
}

図2:SampleValidationController.java

初回表示用のコントローラメソッド(show1,show2)およびサブミット時のコントローラメソッド(submit1,submit2)をそれぞれ用意します。

●Spring MVC 画面パラメータ格納用JavaBean

package sample.validation.web.form;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.springframework.format.annotation.DateTimeFormat;

public class SampleValidationForm {

@AssertTrue(message = "チェックが入っていません")
private boolean varTrue;

@AssertFalse(message = "チェックが入っています")
private boolean varFalse;

@DecimalMin(value = "2", message = "値が小さすぎます")
@DecimalMax(value = "10", message = "値が大きすぎます")
private String decimalMaxMin;

@Min(value = 6, message = "値が小さすぎます")
@Max(value = 10, message = "値が大きすぎます")
private String maxMin;

@Digits(integer = 3, fraction = 1, message = "数値形式が違います")
private String digitInt;

@Future(message = "日付が未来ではありません")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private Date future;

@Past(message = "日付が過去ではありません")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private Date past;

@Null(message = "値が存在しています")
private String isNull;

@NotNull(message = "値が存在していません")
private String isNotNull;

@Pattern(regexp = "test.*", message = "入力形式にマッチしていません")
private String testPattern;

@Size(min = 3, max = 10, message = "3桁以上10桁以下ではありません")
private String strSize;

public boolean isVarTrue() {
return varTrue;
}

public void setVarTrue(boolean varTrue) {
this.varTrue = varTrue;
}

public boolean isVarFalse() {
return varFalse;
}



図3:SampleValidationForm.java

JSR-303 Bean Validationで標準サポートされているアノテーションを使って、検証ロジックを記述しています。ここではロジックの他にエラー時に表示するメッセージも設定しています。

JSR-303 Bean Validationで標準サポートされている検証ロジックは以下の表にまとめました。
標準的なものは一通りそろっています。


表1:Bean Validation アノテーション一覧
$サイバーエージェント 公式エンジニアブログ


●エラーメッセージ表示用テンプレートマクロ

<#--
* bind
*
* Exposes a BindStatus object for the given bind path, which can be
* a bean (e.g. "person") to get global errors, or a bean property
* (e.g. "person.name") to get field errors. Can be called multiple times
* within a form to bind to multiple command objects and/or field names.
*
* This macro will participate in the default HTML escape setting for the given
* RequestContext. This can be customized by calling "setDefaultHtmlEscape"
* on the "springMacroRequestContext" context variable, or via the
* "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag).
* Also regards a "htmlEscape" variable in the namespace of this library.
*
* Producing no output, the following context variable will be available
* each time this macro is referenced (assuming you import this library in
* your templates with the namespace 'spring'):
*
* spring.status : a BindStatus instance holding the command object name,
* expression, value, and error messages and codes for the path supplied
*
* @param path : the path (string value) of the value required to bind to.
* Spring defaults to a command name of "command" but this can be overridden
* by user config.
-->
<#macro form path>
<#if htmlEscape?exists>
<#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
<#else>
<#assign status = springMacroRequestContext.getBindStatus(path)>

<#-- assign a temporary value, forcing a string representation for any
kind of variable. This temp value is only used in this macro lib -->
<#if status.value?exists && status.value?is_boolean>
<#assign stringStatusValue=status.value?string>
<#else>
<#assign stringStatusValue=status.value?default("")>



<#macro fieldErrors property separator classOrStyle="">
<#if status.errors.hasErrors()>
<#if status.errors.getFieldErrors(property)?? >
<#list status.errors.getFieldErrors(property) as error>
<#if classOrStyle == "">
${error.defaultMessage?html?default("")}
<#else>
<#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style">
${error.defaultMessage?html?default("")}

<#if error_has_next>${separator}





<#macro formError classOrStyle="">
<#if status.errors.hasErrors()>
<#if status.errors.getGlobalError()?? >
<#if classOrStyle == "">
${status.errors.getGlobalError().defaultMessage?html}
<#else>
<#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style">
${status.errors.getGlobalError().defaultMessage?html}






図4:validation.ftl

FreeMarkerにおいてエラーメッセージの表示を行うためのマクロを定義しました。

●動作確認
実際に動作確認を行います。

$サイバーエージェント 公式エンジニアブログ
図5:初回画面表示時


$サイバーエージェント 公式エンジニアブログ
図6:エラーとなる値を入力した画面


$サイバーエージェント 公式エンジニアブログ
図7:エラーメッセージ画面

各項目について検証エラーが発生するパターンを入力し(図6)、エラーメッセージが表示されている事を確認します(図7)。

(まとめ)
まず検証ロジックの共通化は生産性の向上に寄与するだけでなく、バグの発生も防ぐことが出来ます。
今回はライブラリで既に用意されている検証ロジックだけを使いましたが、独自に実装する事も可能です。プロジェクト単位で共通の検証ロジックおよびアノテーションを作成し、利用してもらうといったシーンにも十分活用できます。

また今回のサンプルではエラーメッセージの表示にFreeMarkerのマクロを作成しました。
最初はマクロ無しで普通に使おうと思ったのですが、オブジェクトリスト内のエラー順序が毎回変わるため、チェックする項目毎にメッセージが出せるようにしたかったためです。

解説は以上になります。

たぶん私の拙い解説だと「なんだかよく分からない」と思いますので、サンプルアプリケーションのソースコードをGitHubにて公開します。是非サンプルを動かしてほしいです。

最後までお付き合いいただきありがとうございました。

参考文献
[1] Gary Mak/Josh Long/Daniel Rubio, Spring Recipes, Apress (2010)
[2] SpringSource, 6. Validation, Data Binding, and Type Conversion, Spring
Framework Reference Documentation 3.1,
http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/validation.html

サンプルアプリケーションソースコード
https://github.com/heki1224/sample-validation-webapps