背景
- 在平常的项目开发中,我们常需要校验前端传递的参数是否合法
- 对于是新增、修改的数据,校验参数是否合法的意义在于防止异常的数据落到数据库层导致数据库异常
- 对于是查询的数据,校验参数是否合法的意义在于过滤掉一些明显不合法的查询条件,防止恶意查询,减少数据库的查询压力
@PostMapping("/add")
private ApiResponse<String> add(@RequestBody ApiRequest<PersonDto> apiRequest) {
PersonDto person = apiRequest.getData();
//入参校验的常规做法
String name = person.getName();
if(null == name || "".equals(name) || name.length() > 20) {
return ApiResponse.error(4000, "入参校验不通过: name不合法");
}
Integer age = person.getAge();
if(null == age || age < 0 || age > 100) {
return ApiResponse.error(4000, "入参校验不通过: age不合法");
}
//...
//personService.add(person);
return ApiResponse.success(2000, "成功");
}
本文主要内容
- 介绍两种参数校验方式(valid与validated)的基本用法
- 提供相应的测试案例,供读者深度理解
内容导览
参数校验
@Valid
- @Valid是使用Hibernate validation的时候使用。注意:Java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现。
- 可以用在方法、构造函数、方法参数和成员属性(字段)上
- 支持嵌套检测:在一个beanA中,存在另外一个beanB属性。嵌套检测beanA同时也检测beanB
- 不支持分组
- @Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;
@Validated
- @Validated是只用Spring Validator校验机制使用
- 可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
- 不支持嵌套检测
- 支持分组
- @Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示。
与SpringBoot整合
- 在SpringBootv2.3之前的版本只需要引入 web 依赖就可以了,他包含了validation校验包
- 而在此之后SpringBoot版本就独立出来了需要单独引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注意:这个依赖的本质还是
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
注解类型
字符串空值检查
- @NotBlank(message =) 验证字符串非null,且长度必须大于0
- @NotEmpty 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”
- @NotNull 不能为null,可以为空
- @Null 必须为null
字符串格式检查
- @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
- @Email 被注释的元素必须是电子邮箱地址
- @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
- @URL(protocol=,host=, port=, regexp=, flags=) 被注释的字符串必须是一个有效的url
验证数字
- @DecimalMax(value=x) 被注解的元素值小于等于(<=)指定的十进制value 值
- @DecimalMin(value=x) 被验证注解的元素值大于等于指定的十进制value 值
- @Digits(integer=整数位数, fraction=小数位数) 验证注解的元素值的整数位数和小数位数上限
- @Max(value=x) 验证注解的元素值小于等于指定的 value值
- @Min(value=x) 验证注解的元素值大于等于指定的 value值
- @Size(max=, min=) 被注释的元素的大小必须在指定的范围内,可以是 String、Collection、Map、数组
- @Range 值必需在一个范围内
- @Positive:被注释的元素必须为正数
- @PositiveOrZero:被注释的元素必须为正数或 0
- @Negative:被注释的元素必须为负数
- @NegativeOrZero:被注释的元素必须为负数或 0
验证日期
- @Future 验证日期是否是未来
- @FutureOrPresent:被注释的元素必须是现在或者将来的日期
- @Past 验证日期是否是已经过去了的
- @PastOrPresent:被注释的元素必须是现在或者过去的日期
验证布尔
- @Null 验证元素必须为 null
- @NotNull 验证元素必须不为 null
- @AssertTrue 验证元素必须为 true
- @AssertFalse 验证元素必须为 false
实例
@Data
public class StudentAddDto {
@NotBlank(message = "主键不能为空")
private String id;
@NotBlank(message = "名字不能为空")
@Size(min=2, max = 4, message = "名字字符长度必须为 2~4个")
private String name;
@Pattern(regexp = "/^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/", message = "手机号格式错误")
private String phone;
@Email(message = "邮箱格式错误")
private String email;
@Past(message = "生日必须早于当前时间")
private Date birth;
@Min(value = 0, message = "年龄必须为 0~100")
@Max(value = 100, message = "年龄必须为 0~100")
private Integer age;
@PositiveOrZero
private Double score;
}
@Valid
核心特性
- 在Controller层使用”@Valid”修饰实体类参数
- 在实体类的属性上使用校验注解
- 使用BindingResult接收校验结果,手动控制是否校验通过
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class PersonAddDto {
private Long id;
@NotBlank(message = "请输入名称")
@Size(message = "名称字符长度在{min}到{max}之间", min = 1, max = 10)
private String name;
@NotNull(message = "请输入年龄")
@Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
private Integer age;
@Max(value = 2, message = "性别限定最大值为2")
@Min(value = 1, message = "性别限定最小值为1")
@Positive(message = "性别字段只可能为正数")
private Integer gender;
@PastOrPresent(message = "出生日期一定在当前之间之前")
private Date birthday;
private String address;
@Pattern(regexp = "/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\\d{8}$/", message = "手机号格式错误")
private String tel;
@Email(message = "邮箱格式错误")
private String email;
@DecimalMax(value = "99999", message = "工资上限为99999")
//@DecimalMin(value = "99", message = "工资下限为99")
@PositiveOrZero
private BigDecimal salary;
}
校验表单实体
关键语法
- 在Controller层使用”@Valid”修饰实体类参数
- 在实体类中使用校验注解
- 使用BindingResult接收校验结果
import com.alibaba.fastjson.JSON;
import com.ks.demo.vv.dto.ApiResponse;
import com.ks.demo.vv.dto.PersonAddDto;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RequestMapping("/valid")
@RestController
public class ValidController {
@PostMapping("/addByFrom")
private ApiResponse<String> addByFrom(@Valid PersonAddDto personAddDtoDto, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
//return ApiResponse.error(9999, "请求参数校验不通过", JSON.toJSONString(bindingResult.getAllErrors()));
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(ele -> sb.append(ele.getDefaultMessage()).append(";"));
return ApiResponse.error(9999, "请求参数校验不通过", sb.toString());
}
System.out.println(JSON.toJSONString(personAddDtoDto));
return ApiResponse.success(2000, "成功");
}
}
校验List实体
关键语法
- 在Controller层使用”@Valid List<PersonAddDto> personDtoList”,是无法检测List中的实体属性的
- 最佳解决方案:@RequestBody @Valid ValidList<PersonAddDto> personDtoList,也即将List包一层,并使用@Valid修饰该个List
import com.alibaba.fastjson.JSON;
import com.ks.demo.vv.dto.ApiResponse;
import com.ks.demo.vv.dto.PersonAddDto;
import com.ks.demo.vv.dto.ValidList;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
@RequestMapping("/valid")
@RestController
public class ValidController {
/**
* 校验失效,不支持检验List中的实体
*/
@PostMapping("/add")
private ApiResponse<String> add(@RequestBody @Valid List<PersonAddDto> personDtoList, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
//return ApiResponse.error(9999, "请求参数校验不通过", JSON.toJSONString(bindingResult.getAllErrors()));
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(ele -> sb.append(ele.getDefaultMessage()).append(";"));
return ApiResponse.error(9999, "请求参数校验不通过", sb.toString());
}
System.out.println(JSON.toJSONString(personDtoList));
return ApiResponse.success(2000, "成功");
}
/**
* 解决校验List中的实体:使用一个对象包装一层List,其本质是"嵌套校验"
* 最佳的解决方案是下文将要介绍的将请求体以ApiRequest封装,在"T data"上使用@Valid修饰
*/
@PostMapping("/addListByNest")
private ApiResponse<String> addListByNest(@RequestBody @Valid ValidList<PersonAddDto> personDtoList, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
//return ApiResponse.error(9999, "请求参数校验不通过", JSON.toJSONString(bindingResult.getAllErrors()));
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(ele -> sb.append(ele.getDefaultMessage()).append(";"));
return ApiResponse.error(9999, "请求参数校验不通过", sb.toString());
}
System.out.println(JSON.toJSONString(personDtoList));
return ApiResponse.success(2000, "成功");
}
}
import lombok.Data;
import javax.validation.Valid;
import java.util.List;
@Data
public class ValidList<T> {
@Valid
private List<T> dataList;
}
嵌套校验
嵌套检测:在一个beanA中,存在另外一个beanB属性。嵌套检测beanA同时也检测beanB
/**
* 嵌套检测
* 重点关注ApiRequest中"private T data;"上的"@Valid"
*/
@PostMapping("/addByObjNest")
private ApiResponse<String> addByObjNest(@RequestBody @Valid ApiRequest<PersonAddDto> personDto, BindingResult br) {
if(br.hasErrors()) {
return ApiResponse.error(9999, "请求参数校验不通过", errorInfo(br));
}
System.out.println(JSON.toJSONString(personDto));
return ApiResponse.success(2000, "成功");
}
/**
* 嵌套List
*/
@PostMapping("/addByListNest")
private ApiResponse<String> addByListNest(@RequestBody @Valid ApiRequest<List<PersonAddDto>> personDto, BindingResult br) {
if(br.hasErrors()) {
return ApiResponse.error(9999, "请求参数校验不通过", errorInfo(br));
}
System.out.println(JSON.toJSONString(personDto));
return ApiResponse.success(2000, "成功");
}
@Validated
核心特性
- 在Controller层使用”@Validated”修饰实体类参数
- 在实体类的属性上使用校验注解
- 立即失败:一旦校验失败,自动抛出异常(springframework.validation.BindException),结束正在执行中的流程,本个HTTP请求结束,响应500
基本用法
- 校验的参数是实体,@Validated注解直接放在该模型参数前即可,属性校验放在实体类的属性上。
- 校验的参数是普通参数,@Validated要直接放在类上,在具体的参数前加上校验注解。
校验失败的异常
背景:由于校验不通过则立即失败,抛出异常,本次请求结束,响应500。
异常抛出:
- 校验从@RequestBody来的实体,失败抛出: org.springframework.web.bind.MethodArgumentNotValidException
- 校验普通实体失败抛出:springframework.validation.BindException
- 校验普通参数失败抛出:validation.ConstraintViolationException
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import net.hackyle.boot.entity.Person;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Email;
@Api("ViolationController")
@Validated
@RestController
public class ViolationController {
@ApiOperation(value = "test01", notes = "校验从@RequestBody来的实体")
//校验从@RequestBody来的实体,失败抛出:springframeword.MethodArgumentNotValidException
@PostMapping("/test01")
public String test01(@Validated @RequestBody Person person) {
return "通过校验" + " " + person.toString();
}
@ApiOperation(value = "test02", notes = "校验普通实体")
//校验普通实体失败抛出:org.springframework.validation.BindException
@GetMapping("/test02")
public String test02(@Validated Person person) {
return "通过校验" + " " + person.toString();
}
@ApiOperation(value = "test03", notes = "校验普通参数")
//校验普通参数失败抛出:javax.validation.ConstraintViolationException
@PostMapping("/test03")
public String test03(@RequestBody @Email String email) {
return "通过校验" + " " + email;
}
}
public class Person {
@Max(value = 50)
private Integer age;
其他属性
}
拦截处理异常
背景:
- 由于校验不通过则立即失败,抛出异常,本次请求结束,响应500。
- 为了不让用户对响应的500而产生疑惑,所以我们需要在全局异常捕获器中捕获Validator的异常,并响应给客户看得懂的信息。
解决方案:新建一个配置类,并添加@RestControllerAdvice注解,然后在具体方法中通过 @ExceptionHandler指定需要处理的异常
import com.hackyle.boot.common.pojo.ApiResponse;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.naming.AuthenticationException;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* Validator校验RequestBody实体不通过抛出的异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
//LOGGER.info("全局异常捕获器-捕获到MethodArgumentNotValidException:", e);
return ApiResponse.error(9999, "校验RequestBody的实体不通过"); //这里为了代码方便,就不放于枚举类了
}
/**
* Validator校验单个参数校验失败抛出的异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<String> constraintViolationException(ConstraintViolationException e) {
//LOGGER.info("全局异常捕获器-捕获到ConstraintViolationException:", e);
return ApiResponse.error(9999, "处理单个参数校验失败抛出的异常");
}
/**
* Validator校验普通实体失败抛出的异常
*/
@ExceptionHandler(BindException.class)
public ApiResponse<String> bindException(BindException e) {
//LOGGER.info("全局异常捕获器-捕获到BindException:", e);
return ApiResponse.error(9999, "校验普通实体失败抛出的异常");
}
/**
* 请求方法不被允许异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ApiResponse<String> httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
//LOGGER.info("全局异常捕获器-捕获到HttpRequestMethodNotSupportedException:", e);
return ApiResponse.error(9999, "请求方法不被允许异常");
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ApiResponse<String> httpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
//LOGGER.info("全局异常捕获器-捕获到HttpRequestMethodNotSupportedException:", e);
return ApiResponse.error(9999, "HTTP请求不支持");
}
@ExceptionHandler(NoHandlerFoundException.class)
public ApiResponse<String> noHandlerFoundException(NoHandlerFoundException e) {
//LOGGER.info("全局异常捕获器-捕获到NoHandlerFoundException:", e);
return ApiResponse.error(9999, "接口不存在");
}
@ExceptionHandler(AuthenticationException.class)
public ApiResponse<String> authenticationException(AuthenticationException e) {
//LOGGER.info("全局异常捕获器-捕获到AuthenticationException:", e);
return ApiResponse.error(9999, "认证异常");
}
/**
* 总异常:只要出现异常,总会被这个拦截,因为所以的异常父类为:Exception
*/
@ExceptionHandler(Exception.class)
public ApiResponse<String> exception(Exception e) {
//LOGGER.info("全局异常捕获器-捕获到Exception:", e);
return ApiResponse.error(9999, "总异常");
}
}
测试
主要测试点
- 校验JSON传来的实体类
- 校验表单传来的实体类
- 校验单个参数
- 校验嵌套传来的JSON实体类,注意在嵌套检测处要使用“@Valid”
JSON实体类
//校验从@RequestBody来的实体,失败抛出:springframeword.MethodArgumentNotValidException
@PostMapping("/requestBody")
public String requestBody(@Validated @RequestBody PersonAddDto person) {
return "通过校验" + " " + JSON.toJSONString(person);
}
表单实体类
//校验普通实体失败抛出:org.springframework.validation.BindException
@PostMapping("/entity")
public String entity(@Validated PersonAddDto person) {
return "通过校验" + " " + JSON.toJSONString(person);
}
校验单个参数
//校验普通参数失败抛出:javax.validation.ConstraintViolationException
//注意:在使用属性校验参数前一定要额外加“@Validated”,也可以加在类上
@PostMapping("/param")
public String param(@Validated @Email @RequestParam("email") String email) {
return "通过校验" + " " + email;
}
数组与嵌套
//数组与嵌套
@PostMapping("/listNest")
public String listNest(@Validated @RequestBody ApiRequest<List<PersonAddDto>> apiRequest) {
return "通过校验" + " " + JSON.toJSONString(apiRequest);
}
分组校验
背景:
- 对于同一实体,在不同场景下需要校验的参数也是不同的。
- 例如,在增加操作的时候,需要校验username、password;在删除操作的时候,需要校验ID;在修改操作的时候,也需要校验ID和某些字段;在查询操作的时候需要校验ID。
语法:
- 创建分组校验
- 在实体类上加各个分组标识
- 在Controller层的Validated注解中也加入分组标识
定义分组标识
import javax.validation.groups.Default;
public interface ValidatedGroup extends Default {
interface Create extends ValidatedGroup {
}
interface Update extends ValidatedGroup {
}
interface Query extends ValidatedGroup {
}
interface Delete extends ValidatedGroup {
}
}
属性使用校验注解
import com.ks.demo.vv.config.ValidatedGroup;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class PersonAddDto {
//使用了group的,限定只有在该个标记的情况下才会使得当前校验生效
//在Controller层使用:@Validated(ValidatedGroup.Update.class) @RequestBody PersonAddDto personAddDto
//含义:限定在更新和删除时,必须携带ID
@NotNull(groups = {ValidatedGroup.Update.class, ValidatedGroup.Delete.class}, message = "更新操作时不允许id为空")
private Long id;
@NotBlank(message = "请输入名称")
@Size(message = "名称字符长度在{min}到{max}之间", min = 1, max = 10)
private String name;
@NotNull(message = "请输入年龄")
@Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
private Integer age;
}
在Controller层指定开启那些校验
import com.alibaba.fastjson2.JSON;
import com.ks.demo.vv.config.ValidatedGroup;
import com.ks.demo.vv.dto.ApiRequest;
import com.ks.demo.vv.dto.PersonAddDto;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Email;
import java.util.List;
@RequestMapping("/validated")
@RestController
public class ValidatedGroupController {
/**
* `@Validated`中不指定任何分组
*/
@PostMapping("/groupAdd")
public String groupAdd(@Validated @RequestBody ApiRequest<PersonAddDto> apiRequest) {
return "通过校验" + " " + JSON.toJSONString(apiRequest);
}
/**
* `@Validated`中限定使用'ValidatedGroup.Update.class'分组
* PersonAddDto实体类中的所有加了该个分组标识的校验都生效
*/
@PostMapping("/groupUpdate")
public String groupUpdate(@Validated(ValidatedGroup.Update.class) @RequestBody ApiRequest<PersonAddDto> apiRequest) {
return "通过校验" + " " + JSON.toJSONString(apiRequest);
}
/**
* `@Validated`中限定'ValidatedGroup.Delete.class'分组
* PersonAddDto实体类中的所有加了该个分组标识的校验都生效
*/
@PostMapping("/groupDel")
public String groupDel(@Validated(ValidatedGroup.Delete.class) @RequestBody ApiRequest<PersonAddDto> apiRequest) {
return "通过校验" + " " + JSON.toJSONString(apiRequest);
}
}
Name:
Email:
Link: