在平常的项目开发中,我们常需要校验前端传递的参数是否合法
对于是新增、修改的数据,校验参数是否合法的意义在于
防止异常的数据落到数据库层导致数据库异常
对于是查询的数据,校验参数是否合法的意义在于
过滤掉一些明显不合法的查询条件,防止恶意查询,减少数据库的查询压力
@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
校验表单实体
校验List实体
@Validated
校验失败的异常
拦截处理异常
JSON实体类
表单实体类
校验单个参数
数组与嵌套
@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);
gorge_yong:
谢谢,解决了我的问题
与SpringBoot整合
在SpringBootv2.3之前的版本只需要引入 web 依赖就可以了,他包含了validation校验包
而在此之后SpringBoot版本就独立出来了需要单独引入依赖
Fri Oct 13 09:28:34 CST 2023