最近Javaの面倒臭さに耐性ができてきて何も感じなくなってきた oinume です。こんにちは。今日はSpringMVC + JSR-303 Bean Validation + FreeMarkerでいわゆるフォームのバリデーション+エラーメッセージ表示を試してみたので、そのまとめをば。サンプルコードはGitHubにあげてある。
使ったソフトウェアのバージョン
- Spring MVC 3.2.3
- FreeMarker 2.3.19
- Hibernate Validator 4.3.1.Final
Hibernate Validatorは5.0.1.Finalというのが最新なんだけど、これを使うとWebアプリ起動時にNoClassDefFoundErrorで怒られてしまったので1世代古いやつを使ってる。
Caused by: java.lang.NoClassDefFoundError: org/hibernate/validator/method/MethodConstraintViolationException
at org.springframework.validation.beanvalidation.MethodValidationPostProcessor.afterPropertiesSet(MethodValidationPostProcessor.java:102)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1541)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1479)
... 58 more
Caused by: java.lang.ClassNotFoundException: org.hibernate.validator.method.MethodConstraintViolationException
at org.codehaus.plexus.classworlds.strategy.SelfFirstStrategy.loadClass(SelfFirstStrategy.java:50)
at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass(ClassRealm.java:244)
at org.codehaus.plexus.classworlds.realm.ClassRealm.loadClass(ClassRealm.java:230)
at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:430)
at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:383)
... 61 more
ディレクトリ構成
src
├── main
│ ├── java
│ │ └── net
│ │ └── lampetty
│ │ └── samples
│ │ ├── spring
│ │ │ └── mvc
│ │ │ ├── controller
│ │ │ │ ├── FormController.java
│ │ │ └── form
│ │ │ └── UserForm.java
│ ├── resources
│ │ ├── database.xml
│ │ ├── freemarker.xml
│ └── webapp
│ ├── WEB-INF
│ │ ├── spring-context.xml
│ │ ├── view
│ │ │ ├── common
│ │ │ ├── form
│ │ │ │ ├── complete.ftl
│ │ │ │ └── input.ftl
│ │ │ ├── index.ftl
│ │ │ └── spring.ftl
│ │ ├── web.xml
│ │ └── webapp-context.xml
│ └── static
│ └── bootstrap
画面遷移
画面遷移はこんな感じ。普通のWebアプリだとバリデーションがOKだったらデータベース更新したりするだしょう。処理が完了したら完了画面へHTTPリダイレクトする。なるべく説明をシンプルにするために、今回はCSRF対策は省いてる。
application-context.xml的なヤツ
ここを参照。Validation関連で必要なのは下記の2つ。
FormController
今回用意したControllerは下記のようなシンプルなもの。/form/process でバリデーションを行なっている。
package net.lampetty.samples.spring.mvc.controller;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import javax.validation.Valid;
import net.lampetty.samples.spring.mvc.form.UserForm;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* FormとValidationのサンプル
*/
@Controller
public class FormController {
// 入力画面
@RequestMapping(value = "/form/input", method = GET)
public String index(Model model) {
model.addAttribute("userForm", new UserForm());
return "/form/input";
}
// バリデーションを行う
@RequestMapping(value = "/form/process", method = POST)
public String process(
Model model,
@Valid UserForm userForm, // フォームで入力された値がセットされている
BindingResult result) {
if (result.hasErrors()) {
// エラーがある場合は入力画面を表示する
model.addAttribute("userForm", userForm);
return "/form/input";
}
// データベースの更新とか
// 終わったら完了画面へリダイレクト
return "redirect:/form/complete";
}
// 完了画面
@RequestMapping(value = "/form/complete", method = GET)
public String complete(Model model) {
return "/form/complete";
}
}
UserForm
こんな感じのBeanを作る。プロパティに対してアノテーションをつけて、どんなバリデーションをかけるかを定義する。使えるアノテーションに関してはここなどを参照。JSR-303で用意されているものだけだと使いづらい(@NotNullが空文字列チェックしてくれなかったり)ので、Hibernate Validatorのアノテーションも使わざるを得ないかなぁと思った。
package net.lampetty.samples.spring.mvc.form;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
public class UserForm {
@NotEmpty
@Size(max = 10)
private String name;
@NotEmpty
@Size(max = 255)
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
入力画面
入力画面。
<#import "/spring.ftl" as spring />
<#escape __x as __x?html>
フォーム
<#include "/common/head-css.ftl" />
JSR-303 Bean Validation
<@spring.formInput 'userForm.name' 'id="name" placeholder="Name"' />
<#if spring.status.error>
<@spring.showErrors "
", "color:red" />
<@spring.formInput 'userForm.email' 'id="email" placeholder="Email"' />
<#if spring.status.error>
<@spring.showErrors "
", "color:red" />
@spring.formInput のマクロを使うことでフォームのタグを生成できる。詳細はSpringMVCのドキュメントを参照。
spring.status.error はバリデーションエラーの時にtrueになる変数で、<@spring.showErrors "<br>", "color:red" /> というマクロで、エラーがあった場合にエラーメッセージを表示させる。バリデーションエラーが複数ある場合もあるので、複数のエラーメッセージがここに表示される。1つ目の引数はエラーメッセージを区切る文字列。<br>タグなので改行で区切っている。2つ目の引数はエラーメッセージにつける span タグのCSSスタイル。":"があると<span style="...">となり、":"がない場合は<span class="..."> というクラス指定になるみたい。
↓は実際にバリデーションエラーが起きた時の入力画面のキャプチャ。
フォーム完了画面
これはただの完了画面なので説明不要。
エラーメッセージを変えたい
デフォルトだとHibernate Validator付属の英語のメッセージが表示されるので、ValidationMessages_ja.propertiesという名前のプロパティファイルを用意してクラスパスの通ったところに置いておけばいいらしい。参考。
軽く使ってみた感想や疑問など
いくつか使いづらいところや疑問があった。
- @Sizeのエラーメッセージが "size must be between 0 and 10" となっていて不自然なので、@MaxSizeや@MinSizeとかあった方がいいんじゃないか
- FTLの中で「バリデーションエラーが起こっているか」を判定する方法がよくわからなかった。(@springのマクロにはそれっぽいものはなかった)
- FTLの中で「フォームの全ての入力項目のエラーをまとめて表示させたい」んだけど、どうやるのがいいのかわからない。ControllerでModelに全エラーメッセージをつっこんだListをセットすればいいのかなぁ
でも、全体的に見るといい仕組みとしてはだと思う。ちょっと使いづらいところは自分で拡張もできるっぽいので。というわけで仕事でもこれを使っていこうと思う。