@Validated 使用介绍
说明:在项目开发中,请求进入系统的第一步就是校验,在前后端分离的项目中,有前端校验、后端校验。对于后端开发程序员来说,完全依靠前端校验是不合理的,因为只需要用户知道一点计算机知识,就能使用诸如apifox、postman,附带token调用后端接口,绕过前端校验。
后端校验,一般有以下几类校验:
-
非空校验:校验对象、数值是否为空,包括不能等于null、或者空字符串;
-
非法校验:校验数值是否在合法的数值范围内,如自增ID不能为负数,年龄不能为负数,生日不能是未来时间等;
-
不符合业务逻辑校验:校验数值是否符合业务逻辑,如传入ID,查完数据库发现记录不存在,那后面的逻辑可能就不需要继续了;
-
……
本文介绍如何使用 @Validated
注解实现对请求参数的校验
搭建环境
首先,搭一个简单的Spring Boot项目,pom如下
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/>
</parent>
<groupId>com.hezy</groupId>
<artifactId>validated_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>validated_demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>
后面这个依赖,是配合 @Validated 使用的,一些校验数值的注解。
@Validated 校验DTO
如果参数是一个Java Bean对象,里面封装了很多参数,对应Controlled接口如下:
@PostMapping
public String demo1(@RequestBody @Validated ParamDTO paramDTO) {
return "success";
}
ParamDTO,里面写了很多属性和校验
import com.hezy.annotation.AllUpperCase;
import org.hibernate.validator.constraints.UniqueElements;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 参数DTO
* @author hezy
* @version 1.0.0
* @create 2025/3/22 16:16
*/
public class ParamDTO implements Serializable {
// 不能为空
@NotBlank(message = "name不能为空")
@NotEmpty(message = "name不能为空")
public String name;
// 不能为null,并且在指定范围内
@NotNull
@Min(value = 1, message = "age不能小于1")
@Max(value = 100, message = "age不能大于100")
public Integer age;
// 长度在指定范围内
@Size(min = 2, max = 5, message = "username长度不能小于2或者大于5")
public String username;
// 符合正则表达式
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "请输入有效的中国大陆 11 位手机号码")
public String phone;
// 整数部分最多 3 位,小数部分最多 2 位
@Digits(integer = 3, fraction = 2, message = "amount 的整数部分不能超过 3 位,小数部分不能超过 2 位")
public Double amount;
// 必须位正数
@Positive(message = "score 必须为正数")
public Integer score;
// 必须为正数或零
@PositiveOrZero(message = "balance 必须为正数或零")
public Double balance;
// 必须为负数
@Negative(message = "debt 必须为负数")
public Double debt;
// 必须为负数或者零
@NegativeOrZero(message = "overdraft 必须为负数或零")
public Double overdraft;
// 必须为未来时间
@Future(message = "dueDate 必须是未来的日期")
public Date dueDate;
// 必须为过去时间
@Past(message = "birthDate 必须是过去的日期")
public Date birthDate;
// 集合内元素必须唯一
@UniqueElements(message = "ids 中的元素必须唯一")
public List<Integer> ids;
// 自定义,字符串必须全为大写
@AllUpperCase(message = "字符串必须全为大写")
public String uppercaseString;
}
其中:
-
@NotNull:不能为null;
-
@NotBlankL:不能是空格组成的字符串;
-
@NotEmpty:不能为空字符串;
-
@Min(value = 1):长度不能小于1;
-
@Max(value = 100):长度不能大于100;
-
@Size(min = 2, max = 5):长度需要在[2, 5]区间内;
-
@Pattern(regexp = “^1[3-9]\d{9}$”):数值需要符合该正则表达式;
-
@Digits(integer = 3, fraction = 2):浮点型数值,整数部分不能超过3位,小数部分不能超过2位;
-
@Positive():必须是正数;
-
@PositiveOrZero():必须是正数或者零;
-
@Negative():必须是负数;
-
@NegativeOrZero():必须为负数或零;
-
@Future():必须是未来的日期;
-
@Past():必须是过去的日期;
-
@UniqueElements():集合内元素必须唯一;
注解内的message,表示不符合注解规则时,抛出的异常信息。另外 @AllUpperCase 注解是自定义校验规则,校验数值字符串必须全为大写,实现如下:
(先创建一个自定义注解)
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 自定义注解
* @author hezy
* @version 1.0.0
* @create 2025/3/22
*/
@Documented
@Constraint(validatedBy = AllUpperCaseValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AllUpperCase {
String message() ;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
(实现框架的 ConstraintValidator 接口,isValid()方法里面写自己需要校验的逻辑)
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 自定义验证器
* @author hezy
* @version 1.0.0
* @create 2025/3/22
*/
public class AllUpperCaseValidator implements ConstraintValidator<AllUpperCase, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || value.equals(value.toUpperCase());
}
}
@Validated 校验单个参数、路径参数
如果接口传入的参数不是用DTO封装的,而是用路径参数,或者直接传递进来的,如下:
@GetMapping("/{id}")
public String demo2(@PathVariable @Min(value = 1) Integer id) {
return "success";
}
@GetMapping
public String demo3(@NotEmpty(message = "name不能为空") String name) {
return "success";
}
那么,@Validated 注解需要加到类上,像下面这样
import com.hezy.pojo.ParamDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
/**
* @author hezy
* @version 1.0.0
* @create 2025/3/22
*/
@RestController
@RequestMapping("/validated")
@Validated
public class ValidatedController {
@PostMapping
public String demo1(@RequestBody @Validated ParamDTO paramDTO) {
return "success";
}
@GetMapping("/{id}")
public String demo2(@PathVariable @Min(value = 1) Integer id) {
return "success";
}
@GetMapping
public String demo3(@NotEmpty(message = "name不能为空") String name) {
return "success";
}
}
创建全局异常处理器
需要另外创建一个全局异常处理器,用于处理参数校验不通过时,直接将注解内的message信息作为请求结果返回,如下:
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
/**
* Validated 全局异常处理器
* @author hezy
* @version 1.0.0
* @create 2025/3/22
*/
@RestControllerAdvice
public class ValidatedExceptionHandler {
/**
* DTO中的校验
* 处理 @RequestBody 参数校验异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
/**
* 路径参数或者请求参数中的校验
* 处理方法参数校验异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public Map<String, String> handleConstraintViolationException(ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach((violation) -> {
String propertyPath = violation.getPropertyPath().toString();
String errorMessage = violation.getMessage();
errors.put(propertyPath, errorMessage);
});
return errors;
}
}
如果没有这个处理器,校验不通过的请求会直接返回400状态码,对前端不友好。
启动测试
工作完成了,启动项目,跑两步,调用DTO参数接口,如下:
可见ids集合中,有元素重复,不符合ids字段的校验,故返回注解中的message信息。如果有多个参数不符合要求,都会返回提示,如下:
再试下路径参数
路径参数,参数值最小是1(@Min(value = 1)
),我传入0,返回提示,可见提示指明了是哪个方法的哪个参数名不符合要求。再试下直接传入的参数,name要i去不能为空(@NotEmpty(message = "name不能为空")
),下面没传,调用返回提示。
到这里,@Validated 注解的使用基本能覆盖我们大多数场景的参数校验。
另外
另外,抛开参数校验。博主认为,校验是要讲成本的,无休止地校验不能说明你作为一个程序员的成熟,只能说明你对项目业务不熟悉。所以尽量减去不必要的校验,那么,哪些校验是不必要的,这就需要去熟悉业务,把握整个项目,当然也包括项目中使用的框架代码。
总结
本文介绍了在Spring Boot项目中使用@Validated 注解校验接口参数