浅谈Spring的Validator接口验证

概述

Spring的验证有两种方式:

  • 使用Spring的Validator接口
  • 使用JSR-303注解

      很多博客、文章,都是说的怎么使用JSR-303注解,配合Hibernate Validator来进行验证,少有看到使用Spring的Validator接口的,偶尔有看到也是一带而过,可能注解方便,大家比较注重应用吧。使用Spring自带的Validator是相对比较基础底层的验证方式,但比较灵活一点。实现的过程中,其中有些细节让人比较磕碰,吾辈草民浅淡一下如何使用Spring自带的Validator接口来实现验证逻辑,这其中应该注意哪些细节,最后给出一个Demo,希望能给那些想了解这方面的童鞋一点帮助。

      首先说一下,最后想达到的效果是这样的:

浅谈Spring的Validator接口验证_第1张图片浅谈Spring的Validator接口验证_第2张图片

简单流程

首先看一下web.xml

<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" >

    <web-app>
        <display-name>Archetype Created Web Application</display-name>

        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:spring-config.xml</param-value>
        </context-param>

        <!--Spring Framework-->
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>

        <!--Spring MVC-->
        <servlet>
            <servlet-name>dispatcher</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath*:dispatcher-servlet.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>

        <servlet-mapping>
            <servlet-name>dispatcher</servlet-name>
            <url-pattern>*.action</url-pattern>
        </servlet-mapping>

        <welcome-file-list>
            <welcome-file>index.action</welcome-file>
        </welcome-file-list>
    </web-app>

接下来看下login.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
    <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    <html>
    <head>
        <title>用户登录</title>
    </head>
    <body>
        <form:form commandName="user" id="loginForm" action="login.action" method="post">
            用户名:<form:input id="name" path="name" />&nbsp;&nbsp;<form:errors path="name" cssStyle="color:red"/><br/><br/>
            密码:<form:input path="password" id="password" name="password" type="password"/>&nbsp;&nbsp;<form:errors path="password" cssStyle="color:red"/><br/><br/>
            国家:<form:input path="address.country" id="country" name="country" type="text"/>&nbsp;&nbsp;<form:errors path="address.country" cssStyle="color:red"/><br/><br/>
            城市:<form:input path="address.city" id="city" name="city" type="text"/>&nbsp;&nbsp;<form:errors path="address.city" cssStyle="color:red"/><br/><br/>
            <input type="submit" value="提交"/>&nbsp;&nbsp;&nbsp;&nbsp;
            <input type="reset" value="重置"/>
        </form:form>
    </body>
    </html>

这里用到了Spring的<form>标签,这是为了从user这个域对象中取值并显示错误信息。同时使用<form:errors>来显示验证错误所产生的信息。
<form:input>一个很重要的作用就是,当验证错误后,再回到login.jsp这个页面时,可以显示上次用户的输入,用户可在此基础上进行修改,而不会像html的<input>一样把输入都清空了。

然后是success.jsp,也十分简单:

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
    <title>登录成功</title>
</head>
<body>
    欢迎你,${user.name}
</body>
</html>

login.jsp的表单会提交到login.action这个URL,接下来看LoginController。

package com.thomas.login.controller;

import com.thomas.login.bean.User;
import com.thomas.login.validator.AddressValidator;
import com.thomas.login.validator.UserValidator;
import org.springframework.stereotype.Controller;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;

/** * Created by thomas on 16-1-9. */
@Controller
public class LoginController
{
    @InitBinder
    public void initBinder(DataBinder binder)
    {
        binder.addValidators(new UserValidator(new AddressValidator()));
    }

    @RequestMapping(path = "/login.action", method = RequestMethod.POST)
    public String login(@Valid @ModelAttribute User user, Errors errors) throws Exception
    {
        if(errors.hasErrors())
            return "login";
        return "success";
    }
}

  这里通过@InitBinder注解,让SpringMVC在绑定请求参数之前,先addValidators,添加了一个Validator,即UserValidator实例。其中login()方法中的@Valid注解,是让SpringMVC在执行login()方法之前,先对User入参进行验证,验证器自然就是我们添加的UserValidator实例。

User类的定义如下(省略setters/getters):

public class User
{
    private String name;
    private String password;
    private Address address;
}

Address类的定义如下(省略setters/getters):

public class Address
{
    private String city;
    private String country;
}

自定义及复用Validator

UserValidator的实现如下所示:

package com.thomas.login.validator;

import com.thomas.login.bean.Address;
import com.thomas.login.bean.User;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

/** * Created by thomas on 16-1-9. */
public class UserValidator implements Validator {
    private static final Integer MIN_PASSWD_LEN = 6;
    private final Validator addressValidator;

    public UserValidator(Validator addressValidator)
    {
        if(null == addressValidator)
            throw new IllegalArgumentException("The address validator can't be null");
        if(!addressValidator.supports(Address.class))
            throw new IllegalArgumentException("The address validator must support Address Class");
        this.addressValidator = addressValidator;
    }

    public boolean supports(Class<?> aClass)
    {
        return User.class.equals(aClass);
    }

    public void validate(Object o, Errors errors)
    {
        //名字、密码非空验证
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.empty");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "password.empty");

        User user = (User) o;
        //密码最短长度验证
        if(!errors.hasFieldErrors("password"))
        {
            String password = user.getPassword();
            if(password.length() < MIN_PASSWD_LEN)
                errors.rejectValue("password", "password.too.short", new Object[]{MIN_PASSWD_LEN}, null);
        }

        //地址验证
        try{
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(addressValidator, user.getAddress(), errors);
        }finally {
            errors.popNestedPath();
        }
    }
}

  UserValidator嵌套复用了另一个Validator,即AddressValidator,然后通过ValidationUtils的invokeValidator来实现对地址的验证。顺带看下AddressValidator的实现:

package com.thomas.login.validator;

import com.thomas.login.bean.Address;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

/** * Created by Thomas on 2016/1/16. */
public class AddressValidator implements Validator {
    public boolean supports(Class<?> aClass)
    {
        return Address.class.equals(aClass);
    }

    public void validate(Object o, Errors errors)
    {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "country", "country.empty");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "city", "city.empty");
    }
}

  Spring的Validator接口中,最重要的两个方法是:supports和validate。Spring先调用supports方法确定验证器与域对象是否匹配,然后再通过validate方法实现我们自己的验证逻辑。在validate()方法中,通过Errors参数,将所有的验证错误集合到一起,可以在最开头的login.jsp页面中显示这些错误信息。validate()方法中,最常用到的两个类/接口,分别是:

验证辅助类/接口

  • ValidationUtils

      验证器工具类,提供了诸多静态方法,最常用的如rejectIfEmpty(Errors errors, String field, String errorCode), rejectIfEmptyOrWhitespace(Errors errors, String field, String errorCode), invokeValidator(Validator validator, Object object, Errors erros)。分别用于验证失败时,产生针对于某个成员属性的错误信息,或者复用其他的验证器。

  • Errors

      Errors接口常用的方法是:各种reject*方法,作用同样是产生会对于成员属性的错误信息,或者全局的错误信息。地址验证时,则使用了pushNestedPath(),类似于OGNL的对象导航。这样一来,当address这个复合成员里的city验证错误时,产生的错误信息所对应的path就是address.city。hasErrors()方法则用于判断是否产生了验证错误。

错误信息配置

最后,Validator所产生的errorCode所对应的错误信息从何而来?有两种方式可以设定错误信息:

  • 硬编码

      在Errors或ValidationUtils的各个reject*方法中,使用带有defaultErrorMessage形参的方法,指定默认的错误信息,这种属于硬编码。错误信息写死在Java Code中。无法实现国际化,不易于修改,每次修改后要重编译。

  • MessageSource资源文件

      配置Spring的MessageSource来实现国际化的错误信息处理。配置方法是:在classpath中放置.properties文件,名字随意取,比如叫validationMessage.properties。这个是默认的错误信息资源文件,如果想提供针对于中文的错误信息,则另建一个名为validationMessage_zh_CN.properties的文件,在文件中,定义键值对,注意键就是上述自定义Validator中出现的errorCode,值就是具体的错误详细信息。应当注意的是,validationMessage_zh_CN.properties要通过JDK自带的native2ascii工具,将里面的中文转换成对应的unicode编码。

      Spring通过LocaleResolver来解析用户区域,然后决定采用哪一个版本的validationMessage,而默认的LocaleResolver可在spring-webmvc的jar包中通过查看DispatcherServlet.properties知晓,就是org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver,即通过浏览器的HTTP请求头的Accept-Language这个属性来判断用户的区域,如果没有该属性,或者有该属性,但服务器没有相应的validationMessage文件,则采用默认的validationMessage.properties。

      这种方式易修改,国际化,属于软编码,值得推荐。最后,记得在springMVC的配置文件中配置messagesource。如下:

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename">
        <value>classpath:validationMessage</value>
    </property>
</bean>

可能的异常

NoSuchMessageException

注意,关于MessageSource,如果是使用Hibernate Validator + JSR-303注解,有另外的配置方式,稍微注意一下,一般不会出现如下错误,但使用Spring的Validator接口的话,比较容易抛出如下异常:

    org.springframework.context.NoSuchMessageException: No message found under code 'name.empty.user.name' for locale 'zh_CN'.

解决方法:
1. <bean>的id一定要叫messageSource。试过其他的,都会抛异常。
2. 设置basename时,一定要加上classpath:

IllegalStateException

    java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'user' available as request attribute

  这个异常是由于在login.jsp中采用了Spring的<form>标签库,在<form:form>中,需要设置commandName或modelAttribute属性(就算没有设置,默认也会设置成commandName=”command”),因为其他的<form:input>, <form:error>标签中的path属性,都是从commandName/modelAttribute所指定的对象的成员路径,所以如果ModelMap中没有commandName所指定的key/value的话,就会抛出这个异常。在提交过一次表单后,LoginController中,会把user存到ModelMap中,但首次加载login.jsp时,ModelMap里肯定是没有user这个对象的。那解决方法自然就是自己先创建一个空的User实例放到ModelMap中去呗。思路有多种,我的做法是经由一个IndexController导航到login.jsp,在IndexController中,只要使用@ModelAttribute注解就行了。如下:

package com.thomas.login.controller;

import com.thomas.login.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

/** * Created by Thomas on 2016/1/16. */
@Controller
public class IndexController {
    @RequestMapping("/index.action")
    public String index(@ModelAttribute User user)
    {
        return "login";
    }
}

login.jsp页面显示错误信息,其实也可以通过<spring:bind>标签,如:

<spring:bind path="user.name">
       用户名:<input id="name" name="${status.expression}" value="${status.value}" type="text"/><br/><br/>
   </spring:bind>

但同样的,也要求存在user这个命令对象。

最后附上这个Demo的下载链接(PS:这是一个IntelliJ IDEA下的maven工程)
LoginValidation.zip

你可能感兴趣的:(spring,验证,Validator)