Spring MVC 之 自定义参数校验(JSR 303 - Bean Validation)
系统提供对外接口或公用方法时,通常需要对入参校验,即直接在接口入口层就进行校验,不让非法参数流入到业务层导致异常,确保系统的健壮性。。
数据校验分客户端校验和服务端校验,客户端校验主要在页面通过JavaScript来实现,过滤正常用户的误操作,仅做初步过滤;服务端校验是整个应用阻止非法数据的最后防线,客户端校验绝不能替代服务端的校验,客户端校验可以降低服务器的负载。
Java EE 6 开始定义了一项为校验 Bean 数据合法性的规范 JSR-303,叫做 Bean Validation,该标准目标有两个实现:Hibernate Validator和Apache bval,使用较多的是前者。
Hibernate Validator (与 Hibernate ORM 没有关系)是 Bean Validation 的参考实现, 提供了 JSR-303 规范中所有内置 constraint 的实现,并扩展了一些常需要使用的 constraint,提供了一套比较完善,便捷的验证实现方式。
Validation
JSR-303
Bean Validation为 Java Bean 定义了相应的数据类型和 API,在应用中通过在Bean属性上标注类似于@NotNull, @Max等标准的注解指定校验规则,并通过验证接口对 Bean 进行验证。
该校验框架是一个运行时框架,在验证之后验证的错误信息被马上返回;核心接口是javax.validation.Validator,该接口根据目标对象类中所标注的校验注解进和地数据校验,并得到校验结果。
内置校验注解
JSR-303(Bean Validation)内置的 constraint(约束):
| Constraint | 描述 | 备注 | 
|---|---|---|
| @Null | 被注释的元素必须为 null | |
| @NotNull | 被注释的元素必须不为 null | 通常作用在对象属性上, 作用在 String 类型上无法校验空格 | 
| @NotEmpty | 被注释的字符串的必须非空 | |
| @NotBlank | 被注释的字符串必须不为 null,且必须至少包含一个非空白字符 | |
| @AssertTrue | 被注释的元素必须为 true | |
| @AssertFalse | 被注释的元素必须为 false | |
| @Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | |
| @Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | |
| @DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | |
| @DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | |
| @Size(max, min) | 被注释的元素的大小必须在指定的范围内 | |
| @Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 | |
| @Past | 被注释的元素必须是一个过去的日期或时间 | |
| @PastOrPresent | 被注释的元素必须是当前或一个过去的日期或时间 | |
| @Future | 被注释的元素必须是一个将来的日期或时间 | |
| @FutureOrPresent | 被注释的元素必须是当前或一个将来的日期或时间 | |
| @Pattern(value) | 被注释的元素必须符合指定的正则表达式 | |
| @Positive | 被注释的元素必须是一个严格的正数 | 0 为非法值 | 
| @PositiveOrZero | 被注释的元素必须是一个正数或为 0 | |
| @Negative | 被注释的元素必须是一个严格的负数 | 0 为非法值 | 
| @NegativeOrZero | 被注释的元素必须是一个负数或为 0 | |
| @Length | 被注释的字符串的大小必须在指定的范围内 | |
| 被注释的元素必须是电子邮箱地址 | ||
| @Range | 被注释的元素必须在合适的范围内 | 
Hibernate Validator
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。更多信息,请查看:Hibernate Validator - The Bean Validation reference implementation.,Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide
扩展校验注解
| Constraint | 描述 | 备注 | 
|---|---|---|
| @Length | 被注释的字符串的长度必须在 min与max之间 | |
| @URL | 被注释的字符串必须一个 URL | |
| @Range | 被注释的元素必须在合适的范围内 | 应用于数值或数值的字符串表示形式 | 
| @CreditCardNumber | 被注释的字符串必须表示有效的信用卡号码 | 这是Luhn算法的实现,目的是检查用户的错误, 而不是信用卡的有效性! | 
| @LuhnCheck | Luhn 算法检查约束 | 允许验证一系列数字是否通过Luhn模10校验和算法, Luhn Mod10的计算方法是将数字相加, 每个奇数位(从右到左)的值乘以2,如果值大于9, 则结果数字a在总和之前求和。 | 
| @Mod10Check | 允许验证一系列数字是否通过Mod10校验和算法 | 经典的Mod10是通过将数字相加来计算的, 每一个奇数(从右到左)值乘以一个乘数 。 例如,ISBN-13是模10校验和(乘数是 3)。 | 
| @Mod11Check | 允许验证一系列数字是否通过Mod11校验和算法。 | 对于最常见的Mod11变量,求和是通过将最右边的数字(不包括校验位)乘以最左边的一个权重来完成的。 权重从2开始,每位数增加1。然后使用(sum%11)计算校验位。 | 
| @ISBN | 被注释的字符串必须是一个 ISBN号 | ISBN:指国际标准书号, 专为识别图书等文南而设计的国际编号 | 
| @UniqueElements | 验证所提供的 Collection中的每个对象是否唯一 | 即在集合中找不到2个相等的元素。 | 
| @ConstraintComposition | 布尔合成运算约束 | 值为 CompositionType 枚举(OR,AND,ALL_FALSE) | 
| 已弃用 | ||
| 已弃用 | ||
| 已弃用 | 
校验返回策略
Hibernate Validator 提供了两种校验策略模式:
- **普通模式:**默认,会校验完所有的属性,然后返回所有的验证失败信息。
- 快速失败返回模式:只要有一个验证失败,就立即返回失败信息。
配置方式:addProperty true 为快速失败返回模式,false 为普通模式。
| 1 | 
 | 
手动使用 Validator 进行校验:
| 1 | // Lombok | 
Spring Validator
Spring 提供了自己的数据校验框架。Spring 在进行数据绑定时,可同时调用校验框架来完成数据校验的工作。
Spring 的校验框架在org.springframework.validation包中,重要的接口和类如下:
- Validator:最重要的接口,里面有两个方法。 
- Errors:存放错误信息的接口。 
 校验的结果必须是- Errors或是- BindingResult类型。
- ValidatorUtils:校验的工具类,提供了多个 - Errors对象保存错误的方法。
- LocalValidatorFactoryBean:该类实现了Spring的 - Validator接口,也实现了- JSR 303的- Validator接口,只要在 Spring 容器中定义一个- LocalValidatorFactoryBean,即可将其注入到需要数据校验的- Bean中。- 1 - <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" /> - <mvc:annotation-driven/>会默认装配好一个LocalValidatorFactoryBean,实现开发中不需要手动配置。
Spring MVC 提供的数据校验是通过硬编码完成的,实现开发推荐使用JSR 303来进行数据校验。
Spring boot
如果是 Spring Boot  开发 Web 项目,spring-boot-starter-web 中已经含了 spring-boot-starter-validation包,所以不需要重复添加依赖。
如果没有用 Web 依赖想要使用 Validator 实现 Bean 验证,则添加 spring-boot-starter-validation 依赖即可,该依赖内部引入的是 hibernate-validator 实现校验。
spring-boot-starter-validation 包的依赖关系:
| 1 | <dependencies> | 
添加依赖
Spring Boot 项目添加 spring-boot-starter-validation 依赖,该依赖使用 Hibernate Validator 对 Java Bean 进行校验操作。
| 1 | <!--校验 --> | 
Java Bean
| 1 | // Lombok | 
开启校验
在 Java Bean 类上 或 属性上添加验证注解,还需要在 Spring MVC Controller 方法入参使用 @Validated 或 @Valid 注解开启校验。
- @Valid:javax.validation 提供,作用在方法,构造方法,参数,成员属性上。 - @Valid 可做嵌套校验,作用在属性上,属性是一个 Java Bean,需要对属性 Bean 的内部属性进行校验。 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- // Lombok 
 public class Father {
 
 private String name;
 
 private List<Son> sonList;
 }
 public class Son {
 
 private Integer age;
 }
- @Validated:Spring 提供(扩展了 @Valid),作用在类,方法,参数上。 - @Validated 支持分组,在接口形参上加上 @Validated({A.class}),只对该分组有效。例如,(@NotBlank(group={A.class},message = “name不能为空”) - 注:A.class 可以是个空类,仅仅用于分组标识。 
入参是 Java Bean
接口入参是 Java Bean 方式,@Valid 和 @Validated 都能起效。
| 1 | 
 | 
入参非 Java Bean
此方式在参数前使用 @Valid 或 @Validated 都不能起效,只能使用 @Validated 并注释在此方法所在的 Controller 类上才能起效。
| 1 | 
 | 
此方式验证抛出的异常为:ConstraintViolationException,可使用全局异常统一处理。见下面 异常统一处理 章节。
验证失败处理
返回验证失败时的错误信息(例如,通过统一异常处理捕获验证异常,取出验证的错误信息)。
绑定验证结果
在验证入参的方法中,可绑定入参校验的结果对象,判断结果对象是否存在错误信息。
| 1 | 
 | 
异常统一处理
Spring Boot 使用 @ControllerAdvice 进行统一异常处理,Spring MVC 框架抛出方法参数校验异常:MethodArgumentNotValidException,取出异常中的错误信息返回给客户端。
全局异常统一处理参考:Spring Boot 2实践系列(三十八):全局异常统一处理
| 1 | if (e instanceof MethodArgumentNotValidException) { | 
Spring 默认提供了一个配置校验器工厂 Bean,具体参考 SpringMVC-Validation数据校验。
自定义校验注解
框架提供的校验注解(constraint)可能并不能完全满足需求, 所以需要自定义校验规则。例如对 IP 地址进行校验,对密码复杂度进行校验等。
Java 的注解使用,可参考 Java基础:Java 注解(Annotation)及使用。
校验注解由两部分组成:
- 校验注解:就一个普通的 Java 注解,但多个了元注解 - @Constraint用于指定该注解的 校验器。见下面章节的示例。- 1 - validatedBy 的值指定为校验逻辑的实现类,即校验器。 - 自定义校验注解必须包含 message,groups,payload 属性。 
- 校验器:对校验注解进行解析,实现 - ConstraintValidator接口,接口使用了泛型,需要指定两个参数,第一个是自定义注解类,第二个是需要校验的数据类型,重写- isValid(T value, ConstraintValidatorContext context)方法。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15- public interface ConstraintValidator<A extends Annotation, T> { 
 
 /**
 * 在调用 isValid 方法之前初始化
 * 该方法的参数类型为自定义注解,可调用注解的方法获取注解信息
 * 重写该方法, 给自定义校验器中定义的类变量赋值
 */
 default void initialize(A constraintAnnotation) {
 }
 
 /**
 * 重写该方法,实现校验逻辑,返回 布尔值
 */
 boolean isValid(T value, ConstraintValidatorContext context);
 }- 自定义注解校验器类实现了 ConstraintValidator 接口,默认会被 Spring 注册成 bean,所以可以在这个校验器类里面使用@Autowired 或者 @Resource 注入别的服务,不需要在类上添加 @Component 注解声明为 Spring 的 bean。 
**备注:**自定义注解只能作用在接口 或 Controller 方法上才能启效,若写在实现类上是不启效的,只支持 Java Bean 验证。
自定义校验示例
金额精确2位小数
校验注解
| 1 | /** | 
注解解析器
| 1 | /** | 
保留小数位校验
校验注解
| 1 | /** | 
注解解析器
| 1 | /** | 
字符串是否在数组中
校验注解
| 1 | /** | 
注解解析器
| 1 | import org.apache.commons.lang3.StringUtils; | 
字符串是否在枚举中
校验注解
| 1 | /** | 
注解解析器
| 1 | import org.apache.commons.lang3.StringUtils; | 
数字是否在枚举中
校验注解
| 1 | 
 | 
注解解析器
| 1 | public class IntegerEnumValidator implements ConstraintValidator<IntegerEnum, Integer> { | 
注解使用
在实例类的属性上使用 @IntegerEnum 注解,指定枚举值。Controller 方法的入参增加 @Validated 注解使校验起效。
| 1 | 
 | 
校验使用示例
- pom.xml文件引入- Hibernate Validator jar包和依赖包。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17- <!-- JSR 303数据校验实现:hibernate-validator --> 
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-validator</artifactId>
 <version>6.0.7.Final</version>
 </dependency>
 <!-- javax.el -->
 <dependency>
 <groupId>javax.el</groupId>
 <artifactId>javax.el-api</artifactId>
 <version>2.2.5</version>
 </dependency>
 <dependency>
 <groupId>org.glassfish.web</groupId>
 <artifactId>javax.el</artifactId>
 <version>2.2.5</version>
 </dependency>
- 实体类 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22- public class People { 
 
 private String name;
 
 private String loginName;
 
 private String password;
 
 private String userName;
 
 private double age;
 
 
 private String email;
 
 
 private Date birthDay;
 
 private String phone;
 //-------set/get方法------------
 }
- Controller代码 
 方法中接收参数的对象添加- @Valid注解- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 public class LoginController {
 
 public String registerForm() {
 return "registerForm";
 }
 
 
 public String login( People people, Errors errors, Model model) {
 System.err.println(people);
 System.err.println(errors);
 if(errors.hasErrors()) {
 String defaultMessage = errors.getFieldError("age").getDefaultMessage();
 System.out.println(defaultMessage);
 return "registerForm";
 }
 model.addAttribute("people", people);
 return "success";
 }
 }
- JSP表单代码 - 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
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53- <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 
 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 <html>
 <head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <script type="text/javascript"
 src="${pageContext.request.contextPath}/static/js/jquery-1.11.0.js"></script>
 <title>JSR 303 Validation</title>
 </head>
 <body>
 <h3>注册页面</h3>
 <form action="${pageContext.request.contextPath}/user/login"
 method="post">
 <table>
 <tr>
 <td>登录名:</td>
 <td><input name="loginName"></td>
 </tr>
 <tr>
 <td>密码:</td>
 <td><input name="password"></td>
 </tr>
 <tr>
 <td>用户名:</td>
 <td><input name="userName"></td>
 </tr>
 <tr>
 <td>年龄:</td>
 <td><input name="age"></td>
 </tr>
 <tr>
 <td>邮箱:</td>
 <td><input name="email"></td>
 </tr>
 <tr>
 <td>生日:</td>
 <td><input name="birthDay"></td>
 </tr>
 <tr>
 <td>电话:</td>
 <td><input name="phone"></td>
 </tr>
 <tr>
 <td><input type="submit" value="提交"></td>
 </tr>
 </table>
 </form>
 </body>
 </html>
相关参考
- JSR 303 - Bean Validation 介绍及最佳实践
- Hibernate Validator:Bean Validation 的参考实现
- Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide
- JSR 303: Bean Validation
- JSR 380: Bean Validation 2.0
- Jakarta Bean Validation
- Jakarta-bean-validation 校验示例
- Validator 自动化校验
- Spring/Spring boot JSR-303验证框架 之 hibernate-validator
- Hibernate Validator 使用介绍
- 参数校验工具之Validator自定义校验
Spring MVC 之 自定义参数校验(JSR 303 - Bean Validation)
http://blog.gxitsky.com/2020/07/02/SpringMVC-37-JSR-303-BeanValidation-Hibernate-Validator/

