Spring Frameworkリファレンスの“6.7 Spring 3 Validation”に、Spring 3 は Bean Validation API(JSR-303)を完全サポートし、デフォルトのリファレンス実装として Hibernate Validator を採用している と記されています。
今回は、この JSR-303 Validator を使って「Validator と MessageSource」で作成した login フォームのバリデーションを作り変えます。また、簡単な機能ですが、独自の制約条件(custom constraint)を定義した、カスタムメイドのアノテーションも作ってみます。
JSR と Bean Validation
JSR(Java Specification Requests) とは、Java 標準として JCP(Java Community Process)にポストされたリクエストで、JCP メンバーのレビューを経て認可されたリクエストは仕様化ステージに入ります。JSR 303: Bean Validation もそうしたリクエストの一つで、JCPサイトから仕様の最終リリースをダウンロードできます。
事前準備
Hibernate Validator サイトからリンクを辿ると SourceForge.net の /hibernate-validator フォルダーに飛べるので、そこから必要な Distribution bundle をダウンロードします。私は以下のバージョンをダウンロードしました。
hibernate-validator-4.2.0.Final-dist.zip
JAR の登録
今回の作業で必要なのは次の 4 つです。
解凍先フォルダー
hibernate-validator-4.2.0.Final.jar
hibernate-validator-annotation-processor-4.2.0.Final.jar
解凍先フォルダー/lib/required
slf4j-api-1.6.1.jar
validation-api-1.0.0.GA.jar
これらを図のように /WEB-INF/lib にコピーし、それをビルドパスに追加します。因みに、私は Eclipse(Helios SR2) + JDK6 + Java EE6 SDK + Tomcat 7.0.25 という環境です。
<mvc:annotation-driven/>
リファレンスの 6.7.4.3 Configuring a JSR-303 Validator for use by Spring MVC に従って [servlet-name]-servlet.xml に <mvc:annotation-driven/>を追加します。
[servlet-name]-servlet.xml(抜粋)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> <mvc:annotation-driven/> <context:component-scan base-package="wrider"/> : </beans>
制約条件をアノテート
以上で準備は完了です。では早速、login フォームのデータを格納する IdCard クラスの各プロパティにビルトインのアノテーションで制約条件を定義してみます。
IdCard.java
まず「メールアドレス(email)」には@NotEmpty と @Email アノテーションで“必須”、“E-Mailのフォーマット”という制約条件を付けます。一方「パスワード(password)」については、@NotEmpty と @Pattern を使って“必須”、“8 文字以上 32 文字以下の半角英数”という制約条件を付加します。
package wrider.model; import javax.validation.constraints.Pattern; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; public class IdCard { @NotEmpty @Email private String email; public String getEmail() { return this.email; } public void setEmail(String email) { this.email = email; } @NotEmpty @Pattern(regexp = "^[a-zA-Z0-9]{8,32}$") private String password; public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } }
コントローラークラスの修正
独自に作成した IdCard 用のバリデーター(IdCardValidator)の代わりにアノテーションを使用することに伴う変更を施します。
AccountController.java(抜粋)
@RequestMapping で POST リクエストに紐付けられた login メソッドに渡す IdCard 型引数に @Valid アノテーションを付け、idCardValidator を呼び出す部分をコメントアウトしています。
package wrider.controller; import java.util.Map; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; : import wrider.model.IdCard; import wrider.model.UserProfile; @Controller @SessionAttributes({"idCard", "userProfile"}) public class AccountController { /* @Autowired private Validator idCardValidator; */ @Autowired private Validator userProfileValidator; @ModelAttribute("idCard") public IdCard makeIdCard() { return new IdCard(); } @ModelAttribute("userProfile") public UserProfile makeUserProfile() { return new UserProfile(); } @RequestMapping(value="/account/login.html", method=RequestMethod.GET) public String login() { return "account/login"; } @RequestMapping(value="/account/login.html", method=RequestMethod.POST) public ModelAndView login(@Valid IdCard idCard, BindingResult br) { // this.idCardValidator.validate(idCard, br); ModelAndView mav = new ModelAndView(); mav.getModel().putAll(br.getModel()); mav.setViewName("account/login"); return mav; } : }
エラーメッセージの登録
メッセージソースは「Validator と MessageSource」で行った ReloadableResourceBundleMessageSource の設定をそのまま使います。なので、Bean Validator が吐き出したエラーコードと、それらに対応するメッセージを errors.xml 追加するだけです。
バリデーションの結果、検出されたエラー情報は次のような感じで BindingResult に格納されます。
: codes [Pattern.idCard.password,Pattern.password,Pattern.java.lang.String,Pattern]; : codes [NotEmpty.idCard.password,NotEmpty.password,NotEmpty.java.lang.String,NotEmpty]; :
errors.xml(抜粋)
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>field and form level error messages</comment>
:
<entry key="Email.email">指定外の文字列</entry>
<entry key="NotEmpty.email">必須です</entry>
<entry key="Pattern.password">8-32文字の半角英数</entry>
<entry key="NotEmpty.password">必須です</entry>
</properties>
実行!
図は、以上の作業を経てできた login フォームに、メールアドレス「wrider」、パスワード「未入力」として submit した後の画面です。
メールアドレス欄には“指定外の文字列”と表示されています。ここを「未入力」にすると“必須です”と表示されます。
一方、パスワード欄は @NotEmpty で「未入力」が検出されたことを示す“必須です”と、@Patter の引数で指示した正規表現にマッチしていないことを示す「8-32文字の半角英数」の両方が表示されています。これではちょっとかっこ悪いです。
GrepCode を使って、@Email アノテーションで呼び出される EmailValidator のソースを調べて見ると、未入力(null または length() == 0)の場合は true を返すようになっています。つまり、「未入力チェックは @Email の仕事ではありません」ということなのでしょう。
カスタムアノテーションの実装
そこでパスワード(password)について、JSR-303: Bean Validator と Hibernate Validator リファレンスの「Chapter 3. Creating custom constraints」を参考に、@CheckPassword という独自の制約条件(Custom Constraint)を実装してみます。
CheckPassword.java
まず、アノテーション CheckPassword を定義します。@Target でアノテーションの付与対象を指定し、@Constraint の引数 validatedBy で、このアノテーションが呼び出すバリデーターを指示します。
package wrider.annotation; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; import wrider.validator.CheckPasswordValidator; @Target({FIELD, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = CheckPasswordValidator.class) @Documented public @interface CheckPassword { String message() default "{wrider.annotation.CheckPassword.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
CheckPaswordValidator.java
実際のバリデーション処理を行うクラスです。未入力チェックは他のアノテーションに譲り、正規表現によるフォーマットチェックの結果を返すようにしています。
package wrider.validator; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.springframework.util.StringUtils; import wrider.annotation.CheckPassword; public class CheckPasswordValidator implements ConstraintValidator<CheckPassword, String> { private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9]{8,32}$"; public void initialize(CheckPassword constraintAnnotation) { } public boolean isValid(String object, ConstraintValidatorContext constraintContext) { if (!StringUtils.hasLength(object)) { return true; } else { return this.patternMatching(PASSWORD_PATTERN, object); } } private boolean patternMatching(String patternStr, String targetStr) { Pattern pattern = Pattern.compile(patternStr); Matcher matcher = pattern.matcher(targetStr); return matcher.matches(); } }
IdCard.java(抜粋)
これで @CheckPassword の作成は完了です。これを password に対して @Pattern の変わりに付与します。
: @NotEmpty @CheckPassword private String password; :
errors.xml(抜粋)
後は、@CheckPassword に対応したエラーメッセージを errors.xml に追加します。
: <entry key="CheckPassword.password">8-32文字の半角英数</entry> :
再び実行!
今度は、未入力時には“必須です”、入力ルールに従っていない文字列の時には“8-32文字の半角英数”のメッセージだけがパスワード欄に表示されるようになりました。
様々な制約条件に柔軟に対応できる Bean Validation は、慣れてしまえば重宝する仕掛けだと感じました。今回は取り上げませんでしたがバリデーションの順序を指定できる @GroupSequence など、他にも色々と面白そうな機能があり、これからやみつきになりそうです。
0 件のコメント:
コメントを投稿