1、概述
SR 303 – Bean Validation 是一个数据验证的规范。
在任何时候,当你要处理一个应用程序的业务逻辑时,必须要考虑数据校验,确保输入进来的数据从语 义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余、不利于维护等问题。使用Bean Validation,将验证逻辑与相应的域模型进行绑定,能够很好的避免发生这样的问题。
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。借助Hibernate Validator,可以很好的进行参数验证。
2、常用注解
validator内置注解:
- @Null:被注释的元素必须为null
- @NotNull:被注释的元素必须不为null
- @AssertTrue:被注释的元素必须为true
- @AssertFalse:被注释的元素必须为false
- @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max, min):被注释的元素的大小必须在指定的范围内
- @Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
- @Past:被注释的元素必须是一个过去的日期
- @Future:被注释的元素必须是一个将来的日期
- @Pattern(value):被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的注解:
@Email:被注释的元素必须是电子邮箱地址
@Length:被注释的字符串的大小必须在指定的范围内
@NotEmpty:被注释的字符串的必须非空
@Range:被注释的元素必须在合适的范围内
@NotBlank:验证字符串非null,且长度必须大于0
@NotNull,@NotEmpty和@NotBlank
关于@NotNull,@NotEmpty和@NotBlank之间的区别如以下
- @NotNull 适用于任何类型,被标注的元素必须不能为null
- @NotEmpty适用于String类型,Map类型或者数组,不能为null,且长度必须大于0
- @NotBlank只能用于String类型,不能为null,且调用trim()后,长度必须大于0
添加完需要的校验注解之后,在请求处理方法中的接收的请求参数上添加@Valid注解,即可开启校验功能 如果是分组校验则需要 @Validated 注解
3、基本使用
3.1 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--json和bean转换依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.66</version>
</dependency>
<!--包含时间,字符串等基本的工具类依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--时间属性接收字符内串转date的依赖-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.3</version>
</dependency>
<!--jsr303参数校验器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
3.2 请求bean(RequestVo )注解标识
package com.st.valid.jsr3.simple.req;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.*;
import java.util.Date;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
@lombok.Data
public class RequestVo {
@NotNull(message = "id 不能为空")
private Long id;
@Min(value = 10,message = "年龄必须大于等于10岁")
private Integer age;
@Length(min = 5,max = 20,message = "名字的长度必须在5-10之间")
private String name;
@Email(message = "email format is error")
private String email;
@NotBlank(message = "地址不能为空")
private String address;
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "请填写正确的手机号")
private String phone;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@Past(message = "必须是一个过去的时间")
private Date begin;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@Future(message = "必须是一个将来的时间")
private Date end;
}
3.3 controller
package com.st.valid.jsr3.simple.controller;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.simple.req.RequestVo;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
@RestController
public class VaildController {
@PostMapping("/check")
public String getData(@Valid @RequestBody RequestVo requestVo, BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
//1、获取校验的错误结果
result.getFieldErrors().forEach((item)->{
//FieldError 获取到错误提示
String message = item.getDefaultMessage();
//获取错误的属性的名字
String field = item.getField();
map.put(field,message);
});
return JSON.toJSONString(map);
}
return "ok";
}
}
3.4 转换请求参数
主要是为了获取json的请求对象,然后发送请求测试
package com.st.valid.jsr3.simple.test;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.simple.req.RequestVo;
import org.junit.Test;
import java.util.Date;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
public class TT {
@Test
public void beanToJson(){
RequestVo rv = new RequestVo();
rv.setId(1L);
rv.setAddress("上海市,普陀区,大渡河路888号");
rv.setAge(12);
// rv.setBegin(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
// rv.setEnd(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
rv.setBegin(new Date());
rv.setEnd(new Date());
rv.setPhone("15707066004");
rv.setEmail("fangshengisli@163.com");
rv.setName("fangsheng");
String rvStr = JSON.toJSONString(rv);
System.out.println(rvStr);
}
}
运行测试方法获取json对象
{"address":"上海市,普陀区,大渡河路888号","age":12,"begin":1648976868584,"email":"fangshengisli@163.com","end":1648976868584,"id":1,"name":"fangsheng","phone":"15707066004"}
将上面的时间参数手动换成字符串的时间格式(最终请求参数)
{
"address":"上海市,普陀区,大渡河路888号",
"age":12,
"begin":"2022-04-02 18:23:58",
"email":"fangshengisli@163.com",
"end":"2022-04-02 18:23:58",
"id":1,
"name":"fangsheng",
"phone":"11707066004"
}
3.5 测试

4、分组校验
4.1 分组场景需求分析
如上面的 基本使用 案列的基础上,如果请求bean RequestVo 既作为新增数据的bean(新增时id不存在) 也作为修改数据的bean(修改时候前端必须带id),我们不可能为了id需要两种转态而定义两个bean,我们这是个时候就可以使用分组校验的,新增是执行新增的校验逻辑,修改时就执行修改的校验逻辑,实现步骤如下
4.2 引入依赖
还是基本使用 案列的依赖
4.3 GroupRequestVo
package com.st.valid.jsr3.groupValid.entity;
import com.st.valid.jsr3.groupValid.mark.AddGroup;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
@lombok.Data
public class GroupRequestVo {
@NotNull(message = "修改必须指定id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
private Long id;
@Min(value = 10, message = "年龄必须大于等于10岁",groups = {UpdateGroup.class,AddGroup.class})
private Integer age;
@Length(min = 5, max = 20, message = "名字的长度必须在5-10之间",groups = {UpdateGroup.class,AddGroup.class})
private String name;
@Email(message = "email format is error",groups = {UpdateGroup.class,AddGroup.class})
private String email;
@NotBlank(message = "地址不能为空",groups = {UpdateGroup.class})
private String address;
}
4.4 定义两个分组
只需要定义两个接口就行,无需其他的操作,只是作为一个标识而已
package com.st.valid.jsr3.groupValid.mark;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
public interface AddGroup {
}
package com.st.valid.jsr3.groupValid.mark;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
public interface UpdateGroup {
}
4.5 controller
package com.st.valid.jsr3.groupValid.controller;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.groupValid.entity.GroupRequestVo;
import com.st.valid.jsr3.groupValid.mark.AddGroup;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述: 这里要是指定了分组,实体类上的注解就是指定了分组的注解才生效,
* 没有指定分组的默认不生效,要是没有指定分组,就是对没有指定分组的注解生效,指定分组的注解就不生效了
* 可以在自定义的异常分组接口中继承Default类。所有没有写明group的都属于Default分组。
*/
@RestController
public class GroupVaildController {
@PostMapping("/save")
public String save(@Validated(AddGroup.class) @RequestBody GroupRequestVo grv, BindingResult result){
String message = getError(result);
return Objects.isNull(message)?"save ok":message;
}
@PostMapping("/update")
public String update(@Validated(UpdateGroup.class) @RequestBody GroupRequestVo grv, BindingResult result){
String message = getError(result);
return Objects.isNull(message)?"update ok":message;
}
public String getError(BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
//1、获取校验的错误结果
result.getFieldErrors().forEach((item)->{
//FieldError 获取到错误提示
String message = item.getDefaultMessage();
//获取错误的属性的名字
String field = item.getField();
map.put(field,message);
});
return JSON.toJSONString(map);
}
return null;
}
}
4.6 测试
4.6.1 新增请求
请求参数
{
"id":1,
"address":"上海市,普陀区,大渡河路888号",
"age":1,"name":"fangsheng",
"email":"fangshengisli@163.com"
}

4.6.2 修改请求
请求参数
{
"address":"上海市,普陀区,大渡河路888号",
"age":12,"name":"fangsheng",
"email":"fangshengisli@163.com"
}

5、自定义校验
5.1 自定义场景需求
在大部分的情况下,以上的校验基本上能满足所有的校验需求,但是在极端的情况下默认提供的校验无法满足,我们就需要自定义的校验,假如我们有这样一种场景,校验的属性是一个int,这个值必须在一个集合规定的数据里面(当然这种场景也可以这则表达式解决)
5.2 CustomRequestVo
package com.st.valid.jsr3.customVaild.entity;
import com.st.valid.jsr3.customVaild.ListValue;
import com.st.valid.jsr3.groupValid.mark.AddGroup;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
@lombok.Data
public class CustomRequestVo {
/**
* 显示状态[0-不显示;1-显示]可能是一个更大的范围 value = {0,1,2,3,4 ...}
* @ListValue 接下来要定义的校验注解
*/
@ListValue(value = {0,1}, groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
}
5.3 自定义校验注解类 ListValue
package com.st.valid.jsr3.customVaild;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class}) // 校验器需要自定义用什么要得校验器 多个以逗号隔开
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) // 哪都可以标注
@Retention(RUNTIME)
public @interface ListValue {
// 使用该属性去Validation.properties中取
String message() default "{com.st.valid.jsr3.customVaild.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
// 数组,需要用户自己指定
int[] value() default {};
}
5.4 自定义校验器 ListValueConstraintValidator
package com.st.valid.jsr3.customVaild;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
* @描述:
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {//<注解,校验值类型>
// 存储所有可能的值
private Set<Integer> set=new HashSet<>();
// 初始化,你可以获取注解上的内容并进行处理
@Override
public void initialize(ListValue constraintAnnotation) {
// 获取后端写好的限制 // 这个value就是ListValue里的value,我们写的注解是@ListValue(value={0,1})
int[] value = constraintAnnotation.value();
for (int i : value) {
set.add(i);
}
}
// 覆写验证逻辑
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
// 看是否在限制的值里
return set.contains(value);
}
}
5.5 自定义报错配置类ValidationMessages.properties
默认回去当前的properties文件获取报错的信息,文件的名字不能变
com.st.valid.jsr3.customVaild.ListValue.message=value must in [0,1]
5.6 controller
package com.st.valid.jsr3.customVaild.controller;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.customVaild.entity.CustomRequestVo;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @创建人: 放生
* @创建时间: 2022/4/2
*/
@RestController
public class CustomVaildController {
@PostMapping("/custom")
public String custom(@Validated(UpdateGroup.class) @RequestBody CustomRequestVo grv, BindingResult result){
String message = getError(result);
return Objects.isNull(message)?"update ok":message;
}
public String getError(BindingResult result){
if(result.hasErrors()){
Map<String,String> map = new HashMap<>();
//1、获取校验的错误结果
result.getFieldErrors().forEach((item)->{
//FieldError 获取到错误提示
String message = item.getDefaultMessage();
//获取错误的属性的名字
String field = item.getField();
map.put(field,message);
});
return JSON.toJSONString(map);
}
return null;
}
}
5.7 测试
请求参数
{
"showStatus":5
}


6、优化-统一异常处理
6.1 异常统一处理
上面所有的异常处理我们都是处理在每一个测试的controller中的,实际生产中是不行的,代码太冗余, @ ExceptionHandler 需要进行异常处理的方法必须与出错的方法在同一个Controller里面。那么当代码加入了 @ControllerAdvice,则不需要必须在同一个 controller 中了。这也是 Spring 3.2 带来的新特性。从名字上可以看出大体意思是控制器增强。 也就是说,@controlleradvice + @ ExceptionHandler 也可以实现全局的异常捕捉
(1)抽取一个异常处理类
@ControllerAdvice标注在类上,通过“basePackages”能够说明处理哪些路径下的异常。
@ExceptionHandler(value = 异常类型.class)标注在方法上
@Slf4j
@RestControllerAdvice(basePackages = "com.st.valid.jsr3.simple.controller")//管理的controller(这个包下的controller都会被管理)
public class ValidExceptionControllerAdvice {
@ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
public R handleValidException(MethodArgumentNotValidException exception){
Map<String,String> map=new HashMap<>();
// 获取数据校验的错误结果
BindingResult bindingResult = exception.getBindingResult();
// 处理错误
bindingResult.getFieldErrors().forEach(fieldError -> {
String message = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field,message);
});
log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
return R.error(400,"数据校验出现问题").put("data",map);
}
}
注意 如果进行统一的异常处理原来的controller中的BindingResult要注释掉,否则会导致@RestControllerAdvice不生效

6.2 异常信息国际化处理
@Slf4j
@RestControllerAdvice(basePackages = "com.st.valid.jsr3.simple.controller")//管理的controller(这个包下的controller都会被管理)
public class ValidExceptionControllerAdvice {
@ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
public Map<String,String> handleValidException(MethodArgumentNotValidException exception){
Map<String,String> map=new HashMap<>();
// 获取数据校验的错误结果
BindingResult bindingResult = exception.getBindingResult();
//获取当前的国际化语言,
ResourceBundle message = ResourceBundle.getBundle("messages", Locale.US);
// 处理错误
bindingResult.getFieldErrors().forEach(fieldError -> {
// String message = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field,message.getString(field));
});
log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
return map;
}
}
7、最终案例的目录结构
