Spring的验证有两种方式:
使用JSR-303注解
很多博客、文章,都是说的怎么使用JSR-303注解,配合Hibernate Validator来进行验证,少有看到使用Spring的Validator接口的,偶尔有看到也是一带而过,可能注解方便,大家比较注重应用吧。使用Spring自带的Validator是相对比较基础底层的验证方式,但比较灵活一点。实现的过程中,其中有些细节让人比较磕碰,吾辈草民浅淡一下如何使用Spring自带的Validator接口来实现验证逻辑,这其中应该注意哪些细节,最后给出一个Demo,希望能给那些想了解这方面的童鞋一点帮助。
首先说一下,最后想达到的效果是这样的:
首先看一下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" /> <form:errors path="name" cssStyle="color:red"/><br/><br/>
密码:<form:input path="password" id="password" name="password" type="password"/> <form:errors path="password" cssStyle="color:red"/><br/><br/>
国家:<form:input path="address.country" id="country" name="country" type="text"/> <form:errors path="address.country" cssStyle="color:red"/><br/><br/>
城市:<form:input path="address.city" id="city" name="city" type="text"/> <form:errors path="address.city" cssStyle="color:red"/><br/><br/>
<input type="submit" value="提交"/>
<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;
}
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>
注意,关于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:
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