应用程序在执行业务逻辑之前, 需要验证从请求接收到的数据是否正确, 数据验证其实大原则上来说也算是业务的一部分. Spring MVC只要能从请求中将数据类型转换到目标对象, 就算完成了.
然而, 很多可以转换成目标对象的数据, 未必就是在业务层面来说的数据. 如果不同的人员都来编写各自业务部分的验证代码, 就会导致同样的数据可能在不同的地方被拦截.
还一个问题是, 数据输入错误, 必须要让用户知道哪里出错, 需要将出错信息返回到视图, 因此也需要在模型上设置错误数据. 视图上还必须要留出渲染错误信息的地方.
为了解决这个问题, Java EE有一个JSR-303标准, 专门验证一个Bean的数据合法性. 实现了JSR-303的就有Hibernate validator, 所以学会Hibernate真的是方便.
- JSR-303标准和Hibernate的扩展
- Spring配置验证框架
- 如何进行验证
- 展示错误信息
- 自定义错误信息
JSR-303标准和Hibernate的扩展
JSR-303是通过将验证逻辑直接绑定到数据对象的域上进行验证的方法, 具体来说, 就是将注解加到要验证的数据对象的各个域上.
JSR-303标准提供了如下注解:
JSR-303
注解 |
说明 |
@Null |
被注解的域必须是null |
@NotNull |
被注解的域不能为null |
@AssertTrue |
被注解的域必须是true |
@AssertFalse |
必须是false |
@Min(value) |
被注解的必须是一个number(基本类型), 其值必须大于等于注解的参数value |
@Max(value) |
被注解的必须是一个number(基本类型), 其值必须小于等于注解的参数value |
@DecimalMin(value) |
BigDecimal类型的最小值 |
@DecimalMax(value) |
BigDecimal类型的最大值 |
@Size(max, min) |
被注解的域大小(不是数值, 而是那个数据类型的size/长度/length)必须在max和min之间 |
@Digits(integer, fraction) |
被注释的必须是数字, 而且值必须在可接受的范围内 |
@Past |
被注解的是时间对象, 必须是一个过去的日期 |
@Future |
被注解的是时间对象, 必须是一个将来的日期 |
@Pattern |
使用正则表达式进行验证 |
Hibernate还有四个扩展的验证:
@Email
, 被注解的格式符合电子邮件地址的格式
@Length
, 被注解的字符串长度
@NotEmpty
, 字符串不能是空串, 这是为了有效区分null与空串
@Range
, 表示范围.
这里经过我实际检验, @Email
和@NotEmpty
已经也是最新JSR-303的标准, 而Hibernate特有的只剩下@Length和@Range, 所以基本上不需要使用Hibernate特有的注解也可以完成验证.
Spring配置验证框架
想也不用想, Spring配置验证框架, 肯定也是需要创建一个Bean, 创建的过程中使用外部的实现, 就和JPA很类似.
Spring的包是org.springframework.validation
,其中重要的接口是Validator
, 有两个方法:
boolean supports(Class<?> clazz)
, 表示可以验证什么类.
void validate(Object target, Errors errors)
, 对目标进行校验, 并将校验错误记录在errors内, 如果没有错误, errors内没有数据.
这个Bean的具体实现类是LocalValidatorFactoryBean
, XML方式就是一个Bean标签, 直接指定类是org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
即可.
不过更方便的是, 如果配置了<mvc:annotation-driven/>
或者以Java配置类方式写上了@ComponentScan("xxx")
, Spring会在当前的classpath下寻找实现了JSR-303的供应商程序, 然后用其装配LocalValidatorFactoryBean
在Maven中加入Hibernate Validator的配置:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>
然后直接启动项目, 可以观察到一行日志:
17-Feb-2020 08:54:27.630 INFO [RMI TCP Connection(3)-127.0.0.1] org.hibernate.validator.internal.util.Version.<clinit> HV000001: Hibernate Validator 6.0.7.Final
通过IDEA的Spring支持, 可以查看当前Spring中的Bean, 可以看到有一个MVCValidator名称的Bean, 其实现类是org.springframework.validation.Validator, 说明确实自动创建成功了.
如何进行验证
前边已经说过, 验证是采用注解与要验证的类进行绑定的方式, 因此验证的第一步, 就是给被验证的类加上合适的注解.
添加验证规则
来新创建一个类Velkhana, 用于表示怪物猎人中的怪物冰呪龙, 为其加上注解:
public class Velkhana {
@Pattern(regexp = "\\w{7,20}")
private String monsterName;
@DecimalMax("3245.06")
@DecimalMin("2284.52")
private BigDecimal size;
@Past
private Date releaseDate;
......
}
这里我查了一下, 怪物猎人里怪物的名称不会超过20个英文字母. 然后查了一下冰呪龙的大小金冠体积, 给Size标记了大小. 最后由于冰呪龙是冰原的首发怪物, 因此加入到本作的时间一定是过去时间, 也可以将这个日期看做击杀时间, 总之一定是过去的日期.
然后来编写一个控制器, 用来输入一个Velkhana的对象:
@Controller
@RequestMapping("/mhwi")
public class MHWI {
@PostMapping("/add")
public String getVelkhana(@ModelAttribute("vel") Velkhana velkhana) {
System.out.println("传入的冰呪龙对象是: " + velkhana);
return "index";
}
}
在index.jsp中加上:
<p>当前的冰呪龙是: ${vel}</p>
之后可以用Postman来发一个Post请求到/mhwi/add, 要注意时间的字符串格式需要使用像06 Sep 2019
这样的字符串, 才能被转换到Date对象.
随便发送一个超过限制的数据, index.jsp会显示如下:
当前的冰呪龙是: Monster{monsterName='aaaa', size=4000, releaseDate=Sat Feb 29 00:00:00 CST 2020}
很显然, 我们发送了超过不符合要求的数据, 不过此时依然正常显示, 是因为我们只是配置好了验证规则, 但没有实际进行验证, 所以接下来就是第二步, 开启验证.
开启验证
要开启验证, 需要给控制器方法做一些修改, 在传入的Velkhana对象之前加上@Valid注解. 注意, 从请求中获取数据和模型的注解只能同时在方法入参上加一个, 而@Valid不受此限制, 可以额外再加在参数上.
这里还需要注意的是, 只要加上了@Valid注解, 之后的参数马上就要传入一个BindingResult类型的参数, 用于存放验证结果. 不能随心所欲的设置参数. 有几个被验证的参数, 就要在每个参数之后都紧接着加上BindingResult参数.
修改控制器如下:
@Controller
@RequestMapping("/mhwi")
public class MHWI {
@PostMapping("/add")
public String getVelkhana(@Valid @ModelAttribute("vel") Velkhana velkhana, BindingResult bindingResult, Map<String, Object> map) {
if (bindingResult.hasErrors()) {
for (FieldError e : bindingResult.getFieldErrors()) {
System.out.println(e.getField() + e.getDefaultMessage() + " 错误的值是: " + e.getRejectedValue());
}
map.put("vel", "出错了, 请重新输入");
return "index";
}
System.out.println("传入的冰呪龙对象是: " + velkhana);
return "index";
}
}
再一次Post刚才的数据, 就会发现控制台打印出了错误信息:
releaseDate需要是一个过去的时间 错误的值是: Sat Feb 29 00:00:00 CST 2020
monsterName需要匹配正则表达式"w{7,20}" 错误的值是: a
size必须小于或等于3245.06 错误的值是: 4000
通过BindingResult对象, 可以通过FieldErrors得到所有验证错误的错误对象, 从每一个错误对象中, 可以取得属性名称, 验证错误的信息, 错误的值等信息.
这样在后台, 我们就可以知道哪些属性发生了错误, 从而可以进行相应的业务处理. 不过还有一个问题, 就是如何将错误信息展示给用户.
展示错误信息
其实从上边可以看出来, 既然我们可以获取错误对象的所有内容, 比如属性名称和原来错误的值, 就可以将其都放入到模型中, 用户提交表单之后如果发生错误, 可以依然返回表单页, 将错误信息都展示出来.
没错, Spring也是这么做的, 在方法参数前只要注解了@Valid
, 哪怕没有那个@ModelAttribute
注解, Spring也会将验证错误的信息全部都放入到隐含的模型中. 可以用Spring的form标签在JSP中获取:
我们来添加一个form.jsp, 用来输入怪物信息和显示验证结果:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>输入怪物信息</title>
<style>
.errorClass {
color: red;
}
</style>
</head>
<body>
<h1>输入怪物名称</h1>
<form:form modelAttribute="vel" action="${pageContext.request.contextPath}/mhwi/add">
<div>
请输入怪物名称
<form:errors path="monsterName" cssClass="errorClass"/>
<form:input path="monsterName" type="text"/>
</div>
<div>
请输入怪物大小
<form:errors path="size" cssClass="errorClass"/>
<form:input path="size" type="number"/>
</div>
<div>
请输入怪物参战时间
<form:errors path="releaseDate" cssClass="errorClass"/>
<form:input path="releaseDate" type="text"/>
</div>
<button type="submit">提交</button>
</form:form>
</body>
</html>
然后修改一下控制器, 因为表单绑定了vel对象, 需要每次创建一个空的, 然后表单可以自动的拿到用户上次提交的数据用于展示:
@Controller
@RequestMapping("/mhwi")
public class MHWI {
@GetMapping("/form")
public String form() {
return "form";
}
//#1
@ModelAttribute("vel")
public Velkhana getVel() {
return new Velkhana();
}
//#2
@PostMapping("/add")
public String getVelkhana(@Valid @ModelAttribute("vel") Velkhana velkhana, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
for (FieldError e : bindingResult.getFieldErrors()) {
System.out.println(e.getField() + e.getDefaultMessage() + " 错误的值是: " + e.getRejectedValue());
}
return "form";
}
System.out.println("传入的冰呪龙对象是: " + velkhana);
return "index";
}
}
1号方法是新增的, 用于每次第一次访问表单的时候创建一个空白的对象用于表单绑定. 在2号方法里取消了设置vel到map上, 交给框架自行控制.
现在访问/mhwi/form, 填入一些不符合要求的结果, 就可以发现显示出了错误信息:
输入怪物名称
请输入怪物名称 需要匹配正则表达式"\w{7,20}" jar
请输入怪物大小 必须小于或等于3245.06 4000
请输入怪物参战时间 需要是一个过去的时间 Sat Feb 29 14:00:00 CST 2020
提交
而且页面依然会展示表单和用户已经提交的数据, 方便用户修改, 直到全部都正确为止, 就会转到index的JSP页面, 展示出正确的结果.
当然这个例子比较简陋, 页面顶部的地址栏都一直是/add结尾的控制器页面, 其实应该进行更详细的规划, 不过用来说明错误是足够了.
如果想自己操作页面,而不使用Spring提供的方式, 那么就可以自己将错误信息和字段放入到模型中来展示, 比如自己编写一个新表单form2.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>输入怪物信息2</title>
<style>
.errorClass {
color: red;
}
</style>
</head>
<body>
<h1>输入怪物名称</h1>
<form action="/mhwi/add2" method="post">
<div>
<c:if test="${monsterNameError!=null}">
<p class="errorClass">${monsterNameError}</p>
</c:if>
<label for="">输入怪物名称 <input name="monsterName" type="text" value="${vel.monsterName}"></label>
</div>
<div>
<c:if test="${sizeError!=null}">
<p class="errorClass">${sizeError}</p>
</c:if>
<label for="">输入怪物大小 <input name="size" type="number" value="${vel.size}"></label>
</div>
<div>
<c:if test="${releaseDateError!=null}">
<p class="errorClass">${releaseDateError}</p>
</c:if>
<label for="">输入参战日期 <input name="releaseDate" type="text" value="${vel.releaseDate}"></label>
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
这其中绑定了input的value到对象中, 然后通过判断是否存在错误信息来显示错误信息. 在控制器中, 用户输入的数据以绑定到模型中, 剩下的就是如果出现验证错误, 就在模型中放入对应的错误和信息即可:
@Controller
@RequestMapping("/mhwi")
public class MHWI {
@GetMapping("/form2")
public String form2() {
return "form2";
}
@ModelAttribute("vel")
public Velkhana getVel() {
return new Velkhana();
}
@PostMapping("/add2")
public String getVelkhana2(@Valid @ModelAttribute("vel") Velkhana velkhana, BindingResult bindingResult, Map<String, Object> map) {
System.out.println("传入的冰呪龙对象是: " + velkhana);
if (bindingResult.hasErrors()) {
for (FieldError e : bindingResult.getFieldErrors()) {
System.out.println(e.getField() + e.getDefaultMessage() + " 错误的值是: " + e.getRejectedValue());
//向模型中放入验证错误的数据
map.put(e.getField() + "Error", e.getDefaultMessage());
}
return "form2";
}
return "index";
}
}
这样就完成了自定义的表单, 其功能和前边Spring的表单完全一致, 反复会让用户输入到正确结果为止.
自定义错误信息
错误信息也是可以自定义的, 预先定义的信息比较生硬, 可以通过注解的message属性来设置自定义的信息.
@Pattern(regexp = "\\w{7,20}", message = "怪物名称在7-20个字符之间")
private String monsterName;
@DecimalMax(value = "3245.06", message = "不能超过大金冠长度3245.06")
@DecimalMin(value = "2284.52", message = "不能小于小金冠长度2284.52")
private BigDecimal size;
@Past(message = "必须是过去的日期")
private Date releaseDate;
这就可以覆盖系统默认的错误消息.
还可以通过国际化来配置错误消息, 还可以自定义验证器, 这些高级内容就在需要的时候来看了. 这次重新看Spring, 发现写这种绑定数据的代码, 和渲染视图的代码都更加老练了, 还有@ModelAttribute的使用, 理解更加深刻了. 继续加油.