Subscribed unsubscribe Subscribe Subscribe

oinume journal

Scratchpad of what I learned

SpringMVC + Bean Validation + FreeMarkerでFormのバリデーション

最近Javaの面倒臭さに耐性ができてきて何も感じなくなってきた oinume です。こんにちは。今日はSpringMVC + JSR-303 Bean Validation + FreeMarkerでいわゆるフォームのバリデーション+エラーメッセージ表示を試してみたので、そのまとめをば。サンプルコードはGitHubにあげてある。

 

使ったソフトウェアのバージョン

 

 

  • Spring MVC 3.2.3

 

 

 

 

 

 

 

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

│   │   ├── logback.xml

│   └── webapp

│   ├── WEB-INF

│   │   ├── spring-context.xml

│   │   ├── view

│   │   │   ├── common

│   │   │   │   └── head-css.ftl

│   │   │   ├── form

│   │   │   │   ├── complete.ftl

│   │   │   │   └── input.ftl

│   │   │   ├── index.ftl

│   │   │   └── spring.ftl

│   │   ├── web.xml

│   │   └── webapp-context.xml

│   └── static

│   └── bootstrap

 

 

 

画面遷移

画面遷移はこんな感じ。普通のWebアプリだとバリデーションがOKだったらデータベース更新したりするだしょう。処理が完了したら完了画面へHTTPリダイレクトする。なるべく説明をシンプルにするために、今回はCSRF対策は省いてる。

 

jsr-303_validation

 

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)

@Email

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;

}

}

 

 

入力画面

入力画面。

input.ftl

 

 

 

<#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="..."> というクラス指定になるみたい。

 

↓は実際にバリデーションエラーが起きた時の入力画面のキャプチャ。

jsr-303-validation-error

 

フォーム完了画面

これはただの完了画面なので説明不要。

complete.ftl

 

エラーメッセージを変えたい

デフォルトだとHibernate Validator付属の英語のメッセージが表示されるので、ValidationMessages_ja.propertiesという名前のプロパティファイルを用意してクラスパスの通ったところに置いておけばいいらしい。参考

 

軽く使ってみた感想や疑問など

いくつか使いづらいところや疑問があった。

 

 

  • @Sizeのエラーメッセージが "size must be between 0 and 10" となっていて不自然なので、@MaxSizeや@MinSizeとかあった方がいいんじゃないか

 

 

  • FTLの中で「バリデーションエラーが起こっているか」を判定する方法がよくわからなかった。(@springのマクロにはそれっぽいものはなかった)

 

 

  • FTLの中で「フォームの全ての入力項目のエラーをまとめて表示させたい」んだけど、どうやるのがいいのかわからない。ControllerでModelに全エラーメッセージをつっこんだListをセットすればいいのかなぁ

 

 

 

でも、全体的に見るといい仕組みとしてはだと思う。ちょっと使いづらいところは自分で拡張もできるっぽいので。というわけで仕事でもこれを使っていこうと思う。

 

[tmkm-amazon]4798123366[/tmkm-amazon]