Spring WebMVCをやってみる (13) - @ValidとBindingResult -

2013-12-23T00:00:00+00:00 Java Spring Framework

簡単に入力検証なのを実現したいのであれば@ValidとBindingResultを利用する事で出来るらしい。色々突っかかる所があるのであくまで暫定的ではあるけどメモっておく

必要なライブラリ

Spring Frameworkには@Validではなく、@Validatedっていうアノテーションがある。今回やるのは前者な方で後者に関しては今後やる可能性もあるのでとりあえずは「アノテーション「@Validated」と「@Valid」」を参考されたしという事で(ry

で本題の必要なライブラリに関して、javax.validationとその実装であるライブラリが必要。んまぁ後者のその実装に関してはHibernate Validatorがサポートされているのでそれを使う。という事でそのライブラリ参照を追加する

repositories {
    mavenCentral()
}

dependencies {
    compile "javax.servlet:servlet-api:2.5"
    compile "org.springframework:spring-webmvc:3.2.5.RELEASE"
    // 追加
    compile "org.hibernate:hibernate-validator:5.0.2.Final"

    testRuntime "javax.servlet:jstl:1.2"
    testCompile "junit:junit:4.11"
    testCompile "org.hamcrest:hamcrest-all:1.3"
    testCompile "org.springframework:spring-test:3.2.5.RELEASE"
}

SampleForm.java

package sample;

import java.io.Serializable;
import javax.validation.constraints.NotNull;

import org.hibernate.validator.constraints.NotEmpty;

public class SampleForm implements Serializable {

    private static final long serialVersionUID = 1L;

    private int id;

    @NotNull
    @NotEmpty(message = "{error.empty.sample.name}")
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

@NotEmptyで引数を指定しているが、引数を指定しない場合にはorg.hibernate.validator.constraints.NotEmpty.messageというメッセージリソースなプロパティが参照される。これに関しては後術するので(ry

ValidationMessages_ja.properties

#org.hibernate.validator.constraints.NotEmpty.message={sample_form.name}が未入力です
error.empty.sample_form.name="{sample_form.name}"が入力されてません
sample_form.name=名前

SampleController.java

package sample;

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.AbstractView;

@Controller
@RequestMapping("/sample")
public class SampleController {

    @ModelAttribute("sample_form")
    public SampleForm setupForm() {
        SampleForm sample = new SampleForm();
        return sample;
    }

    @RequestMapping
    public String index() {
        return "index";
    }

    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public ModelAndView save(
        @Valid @ModelAttribute("sample_form") final SampleForm sample,
        BindingResult bindingResult) {

        if (bindingResult.hasErrors())
            return new ModelAndView("index");

        return new ModelAndView(new AbstractView() {
            @Override
            protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
                response.getWriter().print(sample.getName());
            }
        });
    }
}

というようにフォーム検証を行うアノテーションを付与して@ModelAttributeで送信したデータとのオブジェクトのマッピングを行い、そのバインディング上でエラーがあるかをチェックすれば良い模様。んで正常な場合だと普通に画面に入力したデータが表示されるだけだけど、エラーな場合にはindexなビューなレスポンスをレンダリングする的な感じかと

でJSPでは

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

<spring:hasBindErrors name="sample_form">
    <c:forEach items="${errors.fieldErrors}" var="error">
        <spring:message message="${error}" />
    </c:forEach>
</spring:hasBindErrors>

<form:form action="/swmvc/sample/save.action" method="post" modelAttribute="sample_form">
    <form:input path="name" />
    <form:errors path="name" />
    <input type="submit" value="save" />
</form:form>

というようにフォームエラーがある場合には表示させるのは

  • <spring:hasBindErrors>でチェックして<spring:message>で表示させる
  • <form:errors path=""/>で表示させる

というような方法がある模様。まぁテストも書きましょうって事で

SampleControllerTest.java

package swmvc;

import org.junit.Test;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

public class SampleControllerTest extends AbstractTestCase {

    @Test
    public void test_save() throws Exception {
        mock.perform(post("/sample/save").param("name", "hoge"))
            .andExpect(status().isOk())
            .andExpect(model().hasNoErrors())
            .andExpect(content().string(is("hoge")));
    }

    @Test
    public void test_save_fail() throws Exception {
        mock.perform(post("/sample/save"))
            .andExpect(status().isOk())
            .andExpect(view().name(is("index")))
            .andExpect(model().hasErrors())
            .andExpect(model().attributeHasFieldErrors(
                "sample_form", // model attribute name?
                "name" // property
            ));
    }
}

終わり。ただ書いたやり方だと、フォーム検証でエラーが発生したらindexビューをレンダリングするようになっているのでリロードするとPOSTなデータを再送信するような方式になってしまうのでそこら辺の対処法は後日書く

余談: javax.validationが読み込むメッセージリソースとSpringが利用するメッセージリソースを共通化する

上記でも書いたように検証エラーメッセージ等はValidationMessages_[locale].properties等から読まれる訳ですけど、Springが使うMessageSourceと共通化する形で

<bean
    id="messageSource"
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource"
    p:basename="classpath:applicationMessages" />

で指定しても、検証メッセージで使用するプロパティはValidationMessagesから参照して無かったらデフォルトを出すっていう形になる模様。なので上記だけ設定してもapplicationMessages.propertiesからは検証メッセージなプロパティが作用しない状態になる模様。その場合は

<?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:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <context:component-scan base-package="sample" />
    <mvc:annotation-driven validator="validator" />

    <bean
        id="validator"
        class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">

        <property name="validationMessageSource" ref="messageSource" />
    </bean>

    <bean
        id="messageSource"
        class="org.springframework.context.support.ReloadableResourceBundleMessageSource"
        p:basename="classpath:applicationMessages" />

</beans>

っていうようにLocalValidatorFactoryBeanなvalidationMessageSourceな設定が別途で必要な模様。但し、これSpring3.2.x系で動かすとSupport Bean Validation 1.1 (JSR-349)っていうエラーが起きた。

3.2.x系で動かす場合には、 https://jira.springsource.org/browse/SPR-10466 を参考にする事で同様な事を利用する事が出来るようになる模様

一応メモ

Spring WebMVCをやってみる (14) - @Valid+BindingResult+RedirectAttributes - Spring WebMVCをやってみる (12) - RedirectAttributes -