SpringMVC[5]--Validation校验

Web系统中,校验是必不可少的环节,校验一般分为前端校验和后端校验,前端校验一般使用脚本语言,对即将要提交的数据进行校验,不符合业务要求的将给予提示。后端校验一般是逻辑性校验,例如校验用户的某种凭证是否过期,某种参数是否在合法请求范围内等在前端不方便的校验。
设涉知识点:

  • Bean Validation数据校验
  • 分组校验
  • Spring Validation接口校验

相关jar包:

  • hibernate-validator-4.3.0.Final.jar
  • jobs-logging-3.1.0.CR2.jar
  • validation-api-1.0.0.GA.jar

1. Bean Validation数据校验

特性: 使用简洁的注释语法来对Bean中的某个属性进行校验。
如:看一下前台传来的水果商品参数中的水果名称是否长度超限,产地信息是否为空。
内容接着:SpringMVC[2]--框架搭建

1.1 搭建validation校验框架
  • 在annotation-driven的注解驱动配置上添加一个validator属性,为其指定一个“validator”值,该值为“校验器”的名称,配置如下:
 <mvc:annotation-driven validator="validator"/>
  • 在核心配置文件springmvc.xml中添加名为“validator”的校验器配置,其具体配置如下:
<!--校验器-->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <property name="validationMessageSource" ref="messageSource"/>
</bean>

 定义了一个为“validator”的校验器,指定其中的校验器提供类是“HibernateValidator”,即添加的Hibernate校验器。
 而下面的validationMessageSource指的是校验使用的资源文件,在该文件中配置校验的错误信息。若不配置默认使用classpath下的ValidationMessages.properties。
在springmvc.xml中添加id为messageSource的资源属性文件配置:

<!--校验错误信息配置文件-->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basenames">
        <list>
            <value>classpath:ProductValidationMessages.properties</value>
        </list>
    </property>
    <!--文件格式设置为“utf-8”-->
    <property name="fileEncodings" value="utf-8"/>
    <!--内容缓存时间设置为120s-->
    <property name="cacheSeconds" value="120"/>
</bean>

然后需要在config或者resources文件夹中新建ProductValidationMessages.properties配置文件,用来配置校验错误信息。

  • 由于该校验机制是给处理器Controlelr使用的,而加载和调用处理器的是处理器适配器HandlerAdapter,所以要为处理器适配器的配置添加校验器:
<mvc:annotation-driven conversion-service="conversionService" validator="validator"/>

【下面的内容<mvc:annotation-driven/>会自动默认注册,所以不用写。】

  • validator还需要检测前台传来的日期、数字类型数据是否正确,所以在其conversion-service属性中配置一个可以将字符串转换为Data类型或数字类型的Java类,配置如下:
<!--检测前台传来的日期、数字类型数据是否正确-->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"/>
1.2 添加校验注解信息
package cn.com.mvc.model;

import org.hibernate.validator.constraints.NotEmpty;

import javax.validation.constraints.Size;

public class Fruits {
    @Size(min=1, max=20, message="{fruits.name.length.error}")
    private String name;
    private double price;
    @NotEmpty(message="{fruits.producing_area.isEmpty}")
    private String producing_area;//产地

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    public String getProducing_area() {
        return producing_area;
    }
    public void setProducing_area(String producing_area) {
        this.producing_area = producing_area;
    }
}

 在name属性上添加@Size注解,并且指定了其最小(min)和最大(max)字符限制,其中message用来提示校验出错误时显示的错误信息。
 校验非空使用的注解为@NotEmpty,其中也指定了message错误信息。

  • 编写ProductValidationMessages.properties配置文件
#添加校验错误提示信息
fruits.name.length.error=请输入1到20个字符的商品名称
fruits.producing_area.isEmpty=请输入商品的生产地
  • 在Controller方法中捕获校验信息
@Controller
@RequestMapping("query")
public class FindControllerTest4 {
    private FruitsService fruitsService = new FruitsServiceImpl();

    @RequestMapping("queryFruitsByCondition")
    public String queryFruitsByCondition(Model model, @Validated Fruits fruits, BindingResult bindingResult){
//        获取校验错误信息
        List<ObjectError> allErrors = null;
        if (bindingResult.hasErrors()){
            allErrors = bindingResult.getAllErrors();
            for(ObjectError objectError:allErrors){
//                输出错误信息
                System.out.println(objectError.getDefaultMessage());
            }
        }
        List<Fruits> findList = null;
        if(fruits==null || (fruits.getName()==null && fruits.getProducing_area()==null)){
            //如果fruits或查询条件为空,默认查询所有数据
            findList = fruitsService.queryFruitsList();
        } else {
//           如果fruits查询条件不为空,按条件查询
            findList = fruitsService.queryFruitsByCondition(fruits);
        }
        model.addAttribute("fruitsList", findList);
        return "findFruits";
    }
}

在Controller方法的形参fruits前面添加了@Validated注解,在后面添加了BindingResult类。一般会在需要校验的Bean形参前面加@Validated注解,标注该参数需要执行Validated校验,而在需要校验的Bean形参后面添加BindingResult参数接收校验的出错信息。

@Validated和BindingResult注解时成对出现的,并且在形参中出现的顺序是固定的(一前一后)。

  • 补充Service层代码
package cn.com.mvc.service;

import cn.com.mvc.model.Fruits;

import java.util.ArrayList;
import java.util.List;

public class FruitsServiceImpl implements FruitsService {
    public List<Fruits> fruitsList = null;
    public List<Fruits> init(){
        if (fruitsList == null){
            fruitsList = new ArrayList<Fruits>();

            Fruits apple = new Fruits();
            apple.setId(1);
            apple.setName("红富士苹果");
            apple.setPrice(2.3);
            apple.setProducing_area("山东");

            Fruits banana = new Fruits();
            banana.setId(2);
            banana.setName("香蕉");
            banana.setPrice(1.5);
            banana.setProducing_area("上海");

            fruitsList.add(apple);
            fruitsList.add(banana);

            return fruitsList;
        }else {
            return fruitsList;
        }

    }
    @Override
    public List<Fruits> queryFruitsList() {
        return init();
    }

    @Override
    public Fruits queryFruitById(Integer id) {
        init();
        Fruits f;
        for(int i = 0; i < fruitsList.size(); i++){
            f = fruitsList.get(i);
            if (f.getId() == id)
                return f;
        }
        return null;
    }

    @Override
    public List<Fruits> queryFruitsByCondition(Fruits fruits) {
        init();
        String name = fruits.getName();
        String area = fruits.getProducing_area();
        List<Fruits> queryList = new ArrayList<Fruits>();
        Fruits f;
        for (int i = 0; i < fruitsList.size(); i++){
            f = fruitsList.get(i);
            if ((!name.equals("")&&f.getName().contains(name)) ||
                    (!area.equals("")&&f.getProducing_area().contains(area))){
                queryList.add(f);
            }
        }
        return queryList.size()>0?queryList:null;
    }
}
1.3 测试校验结果
  • controller层添加
//将错误传到页面
model.addAttribute("allErrors", allErrors);
  • 前端页面定义一个div,专门用来显示错误。
<%--显示错误信息--%>
<c:if test="${allErrors != null}">
    <c:forEach items="${allErrors}" var="error">
        <font color="red">${error.defaultMessage}</font><br/>
    </c:forEach>
</c:if>

2. 分组校验

当使用Bean Validation校验框架的时候,一般都会将校验信息在对应的实体JavaBean中,上面代码中有。
问题:所有使用该实体类的Controller类对应的方法都要进习惯一次校验,但有些Controller仅仅将Fruits实体类作为查询条件(如里面只有一个id),这样的Fruits实体类再进行Bean Validation校验时就会出现问题,导致该方法抛出不该抛的异常。

  SpringMVC提供“分组校验”,将不同校验规则分给不同的组,当在Controller方法中校验相关的实体类Bean时,可以指定不同的组使用不同的校验规则。
首先创建两个组接口,称为FruitGroup1、FruitGroup2

package cn.com.mvc.validator.group;
//校验分组1
public interface FruitsGroup1 {
}
package cn.com.mvc.validator.group;
//校验分组2
public interface FruitsGroup2 {
}

在Fruits实体类中的两个校验分配给不同的组:

public class Fruits {
    private int id;
    @Size(min=0, max=10, message="{fruits.name.length.error}", groups = {FruitsGroup1.class})
    private String name;
    private double price;
    @NotEmpty(message="{fruits.producing_area.isEmpty}", groups = {FruitsGroup2.class})
    private String producing_area;//产地
    //其余代码省略
}

之后就在Controller中的@Validation注解中添加一个value值即可,如:

@RequestMapping("queryFruitsByCondition")
    public String queryFruitsByCondition(Model model, @Validated(value=FruitsGroup1.class) Fruits fruits, BindingResult bindingResult){
    //代码省略
}

3. Spring Validator接口校验

Validator接口校验是SpringMVC自己的校验机制。SpringMVC为其提供了接口,可以用它来验证自己定义的实体对象。
原理:使用了一个Errors对象工作,当验证器验证失败的时候,会向Errors对象填充验证失败的信息。
区别:Bean Validation是在需要校验的JavaBean中进行约束指定,而Spring的Validator接口校验是实现Validator接口,并编写指定类型的校验规则。

接口使用:

3.1 定义一个User实体类
package cn.com.mvc.model;

public class User {
    private String username;
    private String password;
    //get和set方法省略
}
3.2 编写一个Validator接口的实现类,并实现其supports方法和validate方法。

supports方法的作用:判断当前的Validator实现类是否支持校验当前需要校验的实体类,如果支持,该方法返回true,此时才可以调用valida方法来对需要校验的实体类进行校验。
validate方法的作用:编写具体的校验逻辑,并根据不同的校验结果,将错误放入错误对象Errors中。其中Errors是存储和暴露数据绑定错误和验证错误相关信息的接口,其提供了存储和获取错误消息的方法。
编写一个Validator接口的实现类:

package cn.com.mvc.validator;

import cn.com.mvc.model.User;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class UserValidator implements Validator {
    @Override
    public boolean supports(Class<?> aClass) {
        return User.class.equals(aClass);
    }
    @Override
    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "username", "Username.is.empty", "用户名不能为空");
        User user = (User)o;
        if (null == user.getPassword() || "".equals(user.getPassword())){
            //指定验证失败的字段名、错误名、默认错误信息
            errors.rejectValue("password", "Password.is.empty", "密码不能为空。");
        } else if (user.getPassword().length() < 6){
            //指定验证失败的字段名、错误吗、默认错误信息
            errors.rejectValue("password","length.too.short","密码长度不小于6位。");
        }
    }
}

关于Errors的两个方法,第一个rejectValue方法设置了错误字段名为“password”,注册全局错误码“Password.is.empty”。第二个rejectValue方法除了设置错误字段名和全局错误码外,还设置默认消息“密码长度不得小于6位”,当校验器从messageSource没有找到错误码“Password.is.empty.”对应的错误消息时,则显示默认消息“密码长度不得小于6位”。

3.3 在Controller中的initBinder方法中位DataBinder设置一个Validator(即UserValidator),然后在相关方法中添加BindingResult对象。
package cn.com.mvc.controller;

import cn.com.mvc.model.User;
import cn.com.mvc.validator.UserValidator;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;
import java.util.List;

@Controller
@RequestMapping("user")
public class UserControllerTest {
    @InitBinder
    public void initBinder(DataBinder binder){
        binder.setValidator(new UserValidator());
    }
    @RequestMapping("toLogin")
    public String toLoginPage(){
        //跳转至登录页面
        return "/user/login";
    }
    @RequestMapping("login")
    public String login(Model model, @Valid User user, BindingResult result){
        //登录检测
        List<ObjectError> allErrors = null;
        if(result.hasErrors()){
            allErrors = result.getAllErrors();
            for (ObjectError objectError : allErrors){
                //输出错误信息
                System.out.println("code="+objectError.getCode()+" DefaultMessage="+objectError.getDefaultMessage());
                //将错误信息传到页面
                model.addAttribute("allErrors",allErrors);
            }
            return "/user/login";
        } else {
            //其他的业务逻辑
        }
        return "/user/loginSuccess";
    }
}
3.4 校验测试

jsp页面代码,分别为login.jsp和loginSuccess.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>登录页面</title>
</head>
<body>
    <form action="/user/login.action" method="post">
        账号:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/><br/>
        <input type="submit" value="Login"/>
        <%--错误信息展示--%>
        <c:if test="${allErrors != null}">
            <c:forEach items="${allErrors}" var="error">
                <br/><font color="red">${error.defaultMessage}</font>
            </c:forEach>
        </c:if>
    </form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录成功页面</title>
</head>
<body>
    <font color="green">登录成功!</font>
</body>
</html>

下面选取了一张测试结果图:


测试结果.png

注:在UserControllerTest中,login首先会接收到客户端发送的一个User对象(这是因为包装类型参数绑定),利用前面定义的UserValidator对接收到的User对象进行校验。而在login方法的User形参前,使用@Valid注解对其进行标注,这是因为只有当使用@Valid标注需要校验的参数时,Spring才会对其进行校验。而在校验的参数后面,必须给定一个包含Errors的参数,可以是Errors本身,也可以是其子类BindingResult。如果不设置包含Errors的参数,Spring会直接抛出异常,而设置后Spring会将异常的处理权交给开发人员,有开发人员来处理形参中包含Error参数的对象。注意,这个参数必须紧挨着@Valid注解标注的参数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容