在实际工作中,得到数据后的第一步就是校验数据的正确性,如果存在录入上的问题,一般先经过前端js进行验证,但是前端有多种方式可以绕过前端js验证(不是安全有效的),为了数据安全性一般还需要在服务器端做数据校验。这个时候就可以使用spring所提供的验证器(Validator)规则去验证。
所有的验证都是要先注册验证器,不过验证器也是springmvc自动加载的。我们使用JSR 303校验规范,hibernate提供的数据校验器进行数据校验。
使用JSR 303注解验证输入内容
Spring 提供了对Bean的功能校验,通过注解@Valid标明哪个Bean需要启用注解式的验证。在javax.validation.constrains.*中定义了一系列的JSR 303规范给出的注解:
限制 | 说明 |
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
| 邮箱类型 |
@NotEmpty | 集合,不为空 |
@NotBlank | 不为空字符串 |
@Positive | 数字,正数 |
@PositiveOrZero | 数字,正数或0 |
@Negative | 数字,负数 |
@NegativeOrZero | 数字,负数或0 |
@PastOrPresent | 过去或者现在日期 |
@FutrueOrPresent | 将来或者现在日期 |
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>添加员工</title>
</head>
<body>
<form action="${pageContext.request.contextPath }/control/emp/save" method="post" >
用户名:<input type="text" name="username" placeholder="请输入用户名,最小4位,最大12位" required/>
<span></span>
<br/>
密码:<input type="password" name="password" placeholder="请输入登录密码"/><br/>
电子邮箱:<input type="email" name="email" placeholder="请输入电子邮箱"/><br/>
员工生日:<input type="date" name="bornDate" /><br/>
薪水:<input type="number" name="salary" placeholder="请输入员工薪水" /><br/>
联系电话:<input type="text" name="phone" placeholder="请输入员工联系电话" pattern="^1[358]\d{9}$"/><br/>
员工简介:
<textarea placeholder="请输入员工简介" name="intro"></textarea>
<br/>
<input type="submit" value="提交" /> <input type="reset">
</form>
</body>
</html>
- 户名,密码,电子邮箱,入职日期不能为空
- 入职日期格式为yyyy-MM-dd,且只能大于今日
- 生日日期格式为yyyy-MM-dd,且只能是一个过去日期
- 邮箱需要符合格式:
- 简介内容不得多于256个字符
- 薪水最小值为2000,最大值为5万
- 联系方式需为正确的手机号码(使用正则表达式匹配)
建立pojo,确定校验规则:
public class Employee {
private Integer id;
/**
* 用户名,不允许为空,最小4位,最大12位
*/
@NotBlank(message = "用户名不能为空")
@Size(min = 4,max = 12,message = "{employee.username.valid.size.message}")
private String username;
/**
* 密码:不能为空,最小6位
*/
@NotBlank(message = "密码不能为空")
@Size(min = 6,max = 12,message = "密码最低6位")
private String password;
/**
* 电子邮箱,不能为空,要满足邮箱基本格式
*/
@NotNull(message = "邮箱不能为空")
@Email(message="邮箱必须满足基本格式")
private String email;
/**
* 员工生日,格式为:yyyy-MM-dd
* 必须是一个过去的日期
*/
@DateTimeFormat(iso = ISO.DATE)
@Past(message = "你不能录用还未出生的员工")
private LocalDate bornDate;
/**
* 入职日期,格式为:yyyy-MM-dd
* 必须是一个现在或者将来的日期
*/
@DateTimeFormat(iso = ISO.DATE)
@FutureOrPresent
private LocalDate entryDate = LocalDate.now();
/**
* 员工联系(手机)电话
* 必须符合手机号码格式(正则表达式)
*/
@Pattern(regexp = "^1[358]\\d{9}$",message = "请输入正确的手机号码")
private String phone;
/**
* 员工薪水,最低2000,最高5万
*/
@Min(value = 2000,message="工资不能低于2000,否则麻烦了")
@Max(value=50000,message="工资不能大于50000,否则交奢侈税")
private float salary;
/**
* 员工简介,最大不超过200
*/
@Size(min = 6,max = 12,message = "员工简介不能超过200字")
private String intro;
//***************setter and getter*************/
}
这样就定义了一个pojo,用于接收表单的信息。字段上面的注解反映了对每一个字段的验证要求,这样就可以加入对应校验,如果没有指定message属性,会生成默认的错误信息。message配置项用来定义当校验失败后的错误信息,这样就能启动Spring的检验规则来校验表单了。
用控制器验证表单:
@Controller
@RequestMapping("/control/emp")
public class EmployeeController {
@RequestMapping("/save")
public String save(@Validated Employee emp,BindingResult errors) {
if(errors.hasErrors()) {
return "emp/emp_save";
}
return "redirect:./list";
}
}
使用了注解@Validated
标明这个Bean将会被校验,另外一个类型为BindingResult的参数(或者为Errors)是用于保存校验是否存在错误信息的,也就是当采用JSR 303规范进行校验后,它会将这个错误信息保存到这个参数中,在方法中判断是否有错误信息,如果有错误信息,跳转到填写表单页面告诉用户错误信息。
如何在jsp页面中显示错误信息呢?这时可以使用jsp提供给我们的标签库
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<span style="background-color: #fafafa; font-family: monospace;"><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%></span>
<!DOCTYPE html>
<html>
<head>
<title>添加员工</title>
</head>
<body>
<form:form action="${pageContext.request.contextPath }/control/emp/save" method="post" modelAttribute="employee">
用户名:<input type="text" name="username" placeholder="请输入用户名,最小4位,最大12位" required/>
<span><span style="background-color: #fafafa; font-family: monospace;"><form:errors path="username" cssStyle="color: red"/></span></span>
<br/>
密码:<input type="password" name="password" placeholder="请输入登录密码"/><br/>
电子邮箱:<input type="email" name="email" placeholder="请输入电子邮箱"/><br/>
员工生日:<input type="date" name="bornDate" /><br/>
薪水:<input type="number" name="salary" placeholder="请输入员工薪水" /><br/>
联系电话:<input type="text" name="phone" placeholder="请输入员工联系电话" pattern="^1[358]\d{9}$"/><br/>
员工简介:
<textarea placeholder="请输入员工简介" name="intro"></textarea>
<br/>
<input type="submit" value="提交" /> <input type="reset">
</form:form>
</body>
</html>
看起来似乎非常不错,但是我们的message错误提示信息是硬编码在pojo身上,为了避免其硬编码而实现可配置,我们在src/main/resource下新建messageSource.properties文件:
employee.username.valid.notnull.message=用户名不能为空
employee.username.valid.size.message=用户名不能少于4位且不能超过12位
上面简单罗列了两个错误信息配置,其它道理一样,哪怎么应用(读取)指定的错误信息呢?在pojo验证规则之上:
/**
* 用户名,不允许为空,最小4位,最大12位
*/
@NotBlank(message = "{employee.username.valid.notnull.message}")
@Size(min = 4,max = 12,message = "{employee.username.valid.size.message}")
private String username;
可以采用{}表达式对配置文件的key加以读取其对应的value。其它字段同理,到这里还不能读取指定配置文件,需要告诉spring的检验器加载什么样的配置文件,配置代码如下:
@Configuration
@ComponentScan(basePackages = "com.wise.tiger.web")
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer{
@Bean
public InternalResourceViewResolver viewResolver() {
var viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/pages/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override //静态资源不被前端控制器拦截
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")//添加静态资源的url-pattern
.addResourceLocations("/static/");
}
@Bean
public ReloadableResourceBundleMessageSource messageResource() {
var messageResource = new ReloadableResourceBundleMessageSource();
//如果不指定,默认会去classpath下的messages.properties配置文件
messageResource.setBasename("classpath:resourceMessage");
return messageResource;
}
/**
* 添加表单校验器
*/
@Override
public Validator getValidator() {
var validator = new LocalValidatorFactoryBean();
validator.setProviderClass(HibernateValidator.class);
validator.setValidationMessageSource(messageResource());
return validator;
}
}
分组校验
1.定义分组:可以采用标识接口来进行分组区分
//分组校验1
public interface ValidationGroup1 {
}
//分组校验2
public interface ValidationGroup2 {
}
2.在检验规则上添加分组
/**
* 用户名,不允许为空,最小4位,最大12位
*/
@NotBlank(message = "{employee.username.valid.notnull.message}",groups = ValidationGroup1.class)
@Size(min = 4,max = 12,message = "{employee.username.valid.size.message}",groups = ValidationGroup2.class)
private String username;</span>
3.在conroller中指定使用的分组校验
@Controller
@RequestMapping("/control/emp")
public class EmployeeController {
@RequestMapping("/save")
public String save( @Validated(ValidationGroup2.class) @ModelAttribute Employee emp,BindingResult errors,Model model) {
if(errors.hasErrors()) {
for (var i = 0; i < errors.getErrorCount();i++) {
System.out.println(errors.getObjectName() + ":" + errors.getFieldError().getDefaultMessage());
}
return "emp/emp_save";
}
return "redirect:./list";
}
}
有时候除了简单的输入格式、非空性等校验,也需要一定的业务校验,Spring提供了Validator接口来实现校验,它将在进入控制器逻辑之前对参数的合法性进行校验。核心代码如下:
public interface Validator {
/**
* 判断当前校验器是否用于检验clazz类型的pojo
* @param clazz -- pojo类型
* @return true 启动校验,false 不校验
*/
@Override
public boolean supports(Class<?> clazz) {
return false;
}
/**
* 检验pojo的合法性
* @param target 请求对象
* @param errors 错误信息
*/
@Override
public void validate(Object target, Errors errors) {
}
}
Validator接口是SpringMvc校验表单的核心接口,它只是一个验证器,在Spring中最终被注册到验证器的列表中,这样就可以提供给各个控制器去定义,然后通过supports方法判定是否会启用验证器去验证数据。对于校验的过程,则是通过validate方法去实现的。
package com.wise.tiger.pojo;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
/**
* @Description: 员工校验器
* @author: <a href="mailto:1020zhaodan@163.com">Adan</a>
* @date: 2019年5月28日 下午7:23:40
* @version:1.0-snapshot
*/
public class EmployeeValidator implements Validator {
/**
* 判断当前校验器是否用于检验clazz类型的pojo
* @param clazz -- pojo类型
* @return true 启动校验,false 不校验
*/
@Override
public boolean supports(Class<?> clazz) {
//判断验证是否为Employee,如果是则进行校验
return Employee.class.equals(clazz);
}
/**
* 检验pojo的合法性:提交过来的薪水不能低于职位的最低薪资
* @param target 请求对象 (employee)
* @param errors 错误信息
*/
@Override
public void validate(Object target, Errors errors) {
var emp = (Employee)target;
if(emp.getSalary() < emp.getJob().getLowSalary()){
errors.rejectValue("job",null,String.format("薪水%.2f低于%s的最低薪资水平:%.2f",emp.getSalary(), emp.getJob().getValue(),emp.getJob().getLowSalary()));
}
}
}
在Employee实体上添加职位属性字段,该字段为枚举类型,定义如下:
@Getter
@RequiredArgsConstructor
public enum JOB {
CLERK(2000,"业务员"),
SALESMAN(2000,"销售员"),
ANALYST(5000,"分析员"),
SEARCHER(3000,"搜索员"),
MANAGER(6000,"经理"),;
@NonNull
private float lowSalary;
@NonNull
private String value;
}
//================================Employee===========================
@NotNull(message = "{employee.job.notnull.message}")
private JOB job;
//================================jsp================================
职位:<select name="job">
<option value="CLERK">业务员</option>
<option value="SALESMAN">销售员</option>
<option value="ANALYST" selected="selected">分析员</option>
<option value="SEARCHER">搜索员</option>
<option value="MANAGER">经理</option>
</select>
<span>
<spring:errors cssStyle="color: red;font-size: 12px" path="job"/>
</span>
<br/>
需要将该验证器捆绑到对应的控制器中,Spring MVC提供了注解@InitBinder,通过该注解就可以将验证器和控制器捆绑在一起,这样就能对请求表单进行验证了。
@Controller
@RequestMapping("/control/emp")
public class EmployeeController {
@InitBinder
public void initBinder(DataBinder binder){
//数据绑定器加入验证器
binder.setValidator(new EmployeeValidator());
}
@RequestMapping("/save")
public String save( @Validated(ValidationGroup2.class) @ModelAttribute Employee emp,
BindingResult errors,Model model) {
if(errors.hasErrors()) {
for (var i = 0; i < errors.getErrorCount();i++) {
System.out.println(errors.getObjectName() + ":" + errors.getFieldError().getDefaultMessage());
}
return "emp/emp_save";
}
return "redirect:./list";
}
}
这样就能对比较复杂的逻辑关系进行校验了。@InitBinder的使用还有其它内容,后续讲解。。。。。。
附:依赖列表
dependencies {
compile group: 'org.springframework', name: 'spring-context', version: '5.1.7.RELEASE'
compile group: 'org.springframework', name: 'spring-webmvc', version: '5.1.7.RELEASE'
compile group: 'org.springframework', name: 'spring-test', version: '5.1.7.RELEASE'
providedCompile group: 'javax.servlet.jsp', name: 'javax.servlet.jsp-api', version: '2.3.3'
providedCompile group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
compile group: 'taglibs', name: 'standard', version: '1.1.2'
compile group: 'javax.servlet', name: 'jstl', version: '1.2'
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
compile group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.1'
compile group: 'org.mybatis', name: 'mybatis', version: '3.5.1'
compile group: 'com.alibaba', name: 'druid', version: '1.1.16'
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
compile group: 'org.springframework', name: 'spring-jdbc', version: '5.1.7.RELEASE'
compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
compile group: 'org.hibernate', name: 'hibernate-validator', version: '6.0.16.Final'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.9'
}