Spring数据验证及Spring MVC拦截器

Spring数据验证及Spring MVC拦截器

应用程序在执行业务逻辑前,必须通过数据校验保证接收到的输入数据是正确合法的,如代表生日的日期应该是一个过去的时间、工资的数值必须是一个正数等。一般情况下,应用程序的开发是分层的,不同层的代码由不同的开发人员负责。很多时候,同样的数据验证会出现在不同的层中,这样就会导致代码冗余,为了避免这样的情况,最好将验证逻辑和相应的域模型进行绑定,将代码验证的逻辑集中起来管理。



JSR 303

JSR-303是 Java为 Bean 数据合法性校验所提供的标准框架,它已经包含在Java EE 6.0中。JSR-303通过在 Bean属性上标注类似于@NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证。可以通过 http∶/jcp.org/en/jsr/detail?id=303 了解JSR-303的详细内容。
JSR-303定义了一套可标注在成员变量、属性方法上的校验注解,说明如表所示:

注解 说明
@Null 验证对象是否为null
@NotNull 验证对象是否不为null,无法检查长度为0的字符串,用于验证基本数据类型
@NotBlank 检查约束字符串是不是null,被trim的长度是否大于0,值作用于字符串,并且会去除前后空格
@AssertTrue 验证Boolean对象是否为true
@AssertFalse 验证Boolean对象是否为false
@Max(value) 验证Number和String对象是否小于等于指定的值
@Min(value) 验证Number和String对象是否大于等于指定的值
@DecimalMax(value) 被标注的值必须不大于约束中指定的最大值。这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示,小数存在精度
@DecimalMin(value) 被标注的值必须不小于约束中指定的最小值。这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示,小数存在精度
@Digits(integer,fcaction) 验证字符串是否是符合指定格式的数字,integer指定整数精度,fraction指定小数精度
@Size(min,max) 验证对象(Array、Collection、Map、String)长度是否在给定的范围之内
@Past 验证Date和Calender对象是否在当前时间之前
@Future 验证Date和Calender对象是否在当前时间之后
@Pattern 验证String对象是否符合正则表达式的规则

Hibernate Validator是JSR-303的一个参考实现,它除了支持所有标准的校验注解外,还支持如表所示的扩展注解。


@NotBlank 检查约束字符串是不是Null,被Trim的长度是否大于0。只对字符串,且去掉前后空格
@URL 验证是否是合法的url
@Email 验证是否是合法的邮件地址
@CreditCardNumber 验证是否是合法的信用卡号码
@Length(min,max) 验证字符串的长度必须在指定的范围内
@NotEmpty 检查元素是否为NULL或者EMPTY。用于Array、Collection、Map、String
@Range(min,max,message) 验证属性值必须在合适的范围内

JSR-303 的核心接口是 javax.validation.Validator,该接口根据目标对象类中所标注的校验注解进行数据校验,并得到校验结果。


SpringMVC数据验证

<mvc∶annotation-driven/>会默认装配一个LocalValidatorFactoryBean,通过在处理方法的入参上标注@Valid注解,即可让 Spring MVC 在完成数据绑定后执行数据校验工作。
在开发中凡是用户输入的数据都需要验证,例如登录时,用户填写的用户名或者密码;注册时,用户填写的注册信息等等,如果没有SpringMVC的数据验证,那么只能通过String类提供了API或者利用正则表达式进行验证。并且这是一件很繁琐的事情,下面将示例如果使用注解进制验证数据合法性。

使用注解

下面将以用户登录为例进行讲解,首先添加如下依赖:

1
2
3
4
5
6
<!--hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>

接下来新建VO类,使用注解

1
2
3
4
5
6
7
8
@Data
public class UserVO {
@Max(value = 12,message = "用户名不能超过12位")
@Min(value = 6,message = "用户名不能少于6位")
private String username;
@Pattern(regexp = "\\w{6,12}")
private String password;
}

在上例的VO中,使用了@Max,@Min注解用于验证用户名长度必须在6-12位之间。验证密码时则使用了@Pattern注解,该注解可以以正则表达式来验证一个字符串是否符合标准,上例中的正则表达式表明字符串必须由6-12的数字、字母、下划线组成。

新建页面,用于填写用户名和密码:

1
2
3
4
5
6
7
8
9
<body>
<form action="${pageContext.request.contextPath}/login" method="post">
<div class="msg">${usernameMessage}</div>
<input name="username" value=""></br>
<div class="msg">${passwordMessage}</div>
<input name="password" value=""></br>
<input type="submit" value="提交">
</form>
</body>

接下来,新建Controller进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cn.bytecollege.controller;
import cn.bytecollege.vo.UserVO;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.w3c.dom.stylesheets.LinkStyle;

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

@Controller
public class LoginController {
@PostMapping("/login")
public String login(@Valid UserVO userVO, BindingResult result, Model model){
if(result.hasErrors()){
if(result.hasFieldErrors("username")){
List<FieldError> list = result.getFieldErrors("username");
String msg = "";
for (FieldError e:list) {
msg+=e.getDefaultMessage();
}
model.addAttribute("usernameMessage",msg);
}
if(result.hasFieldErrors("password")){
FieldError error = result.getFieldError("password");
model.addAttribute("passwordMessage",error.getDefaultMessage());
}
}
return "login.jsp";
}
}

部署该应用,访问登录页面,在不填写任何内容的情况下提交表单,运行结果如下图:

在上例中可以看出在Controller的方法参数User上使用了@Valid注解,这个注解用于表明对该参数进行验证。而BindingResult对象则是用于获取验证的信息,SpringMVC会将校验结果保存在该对象。该对象主要有如下几个方法:

  • FieldError getFieldError(String field)∶根据属性名获取对应的校验错误。
  • List<FieldError> getFieldErrors()∶获取所有的属性校验错误。
  • Object getFieldValue(String field)∶获取属性值。
  • int getErrorCount()∶获取错误数量。

拦截器

当收到请求时,DispatcherServlet 将请求交给处理器映射(HandlerMapping),让它找出对应该请求的 HandlerExecutionChain 对象。在讲解 HandlerMapping 之前,有必要认识一下这个 HandlerExecutionChain 对象。
HandlerExecutionChain 顾名思义是一个执行链,它包含一个处理该请求的处理器(Handler),同时包括若干个对该请求实施拦截的拦截器(HandlerInterceptor)。当HandlerMapping 返回 HandlerExecutionChain 后,DispatcherServlet 将请求交给定义在HandlerExecutionChain 中的拦截器和处理器一并处理。
HandlerExecutionChain 是负责处理请求并返回 ModelAndView 的处理执行链,其结构如图所示。请求在被 Handler 执行的前后,链中装配的 HandlerInterceptor 会实施拦截操作。

拦截器方法

拦截器到底做了什么事情?我们通过考查拦截器的几个接口方法进行了解。

  • boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object
handler)∶在请求到达 Handler 之前,先执行这个前置处理方法。当该方法返回false 时,请求直接返回,不会传递到链中的下一个拦截器,更不会传递到处理器链末端的 Handler中。只有返回 true 时,请求才向链中的下一个处理节点传递。
  • void postHandle(HttpServletRequest request, HttpServletResponse response, Object
handler,ModelAndView modelAndView)∶在请求被HandlerAdapter执行后,执行这个后置处理方法。

  • void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler,Exception ex)∶在响应已经被渲染后,执行该方法。
位于处理器链末端的是一个 Handler,DispatcherServlet 通过 HandlerAdapter 适配器对 Handler 进行封装,并按统一的适配器接口对 Handler 处理方法进行调用。

拦截器使用

下面的示例将演示拦截器的使用:

首先,定义拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package cn.bytecollege.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
/**
* preHandle方法是进行处理器拦截用的,该方法将在Controller方法
* 被调用前被调用,该方法的返回值为true时拦截器才会继续往下执行,
* 该方法的返回值为false时,整个请求就结束了。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("执行了preHandle方法");
return true;
}
/**
* 该方法将在Controller方法调用后执行,方法可以对ModelAndView进行操作,
* 该方法也只能在当前Interceptor的preHandle方法的返回值为true时才会执行
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("执行了postHandle方法");
}
/**
* 该方法将在整个请求完成之后执行,作用是清理资源,
* 该方法也只能在当前 Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("执行了afterCompletion方法");
}
}

定义完拦截器后,需要在配置文件中进行如下配置,才能使拦截器生效:

1
2
3
4
5
6
7
8
<!--配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--配置拦截所有请求-->
<mvc:mapping path="/*"/>
<bean class="cn.bytecollege.interceptor.MyInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>

运行结果如下图: