前言
为了保证数据的正确性、完整性,前后端都需要进行数据检验。作为一名后端开发工程师,不能仅仅依靠前端来校验数据,我们还需要对接口请求的参数进行后端的校验。最常见的做法就是通过if/else语句来对请求的每一个参数一一校验,当很多参数需要校验的时候,if/else语句就会比较长,写起来也比较麻烦,一点都不简洁、美观。所以,今天来和大家分享一下Spring Boot Validation。
spring-boot-starter-validation
Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上。
如下图所示,spring-boot-starter-web-2.2.6.RELEASE就包含了spring-boot-starter-validation,
而spring-boot-starter-web-2.5.7并没有spring-boot-starter-validation,需要自己手动加入依赖。
如下图所示,手动加入依赖spring-boot-starter-validation,实际上依赖了hibernate-validator。
@Valid和@Validated
@Valid所属包为javax.validation,不具备分组校验功能;可以用在方法、构造函数、方法参数和成员属性(field)上
说明:Java的JSR-303声明了@Valid这类接口,而hibernate-validator对其进行了实现。
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。
@Validated所属包为org.springframework.validation.annotation,属于spring的校验机制,具有分组校验功能;用在类型、方法和方法参数上。但不能用于成员属性(field)。
@Valid和@Validated注解可以结合使用,来实现嵌套验证。
常用的注解如下:
简单使用
添加依赖
本文使用的是Spring Boot 2.5.7,具体依赖如下:
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
实体类
@Data
@ToString
public class User {
private Integer id;
@NotBlank(message = "姓名不能为空")
private String name;
@Max(value = 100, message = "最大不超过100")
private Integer age;
}
Controller
@RestController
public class TestController {
/**
* post请求 实体类User 加上注解@Valid
*/
@PostMapping("/test")
public String test(@Valid @RequestBody User user) {
System.out.println(user);
return "Hello World";
}
/**
* get请求 实体类User 加上注解@Valid
* 测试:http://localhost:8080/query2?name=&age=101
*/
@GetMapping("/query2")
public String queryUserInfo(@Valid User user) {
System.out.println("query2");
return user.getName();
}
}
/**
* 【反面教材】验证不生效
*/
@RestController
public class Test3Controller {
@GetMapping("/test3")
public String test3(@NotEmpty(message = "name不能为空") String name,
@Max(value = 100, message = "请输入数字") Integer age) {
System.out.println("是啥啊3");
return name;
}
@GetMapping("/test4")
@Validated
public String test4(@NotEmpty(message = "name不能为空") String name,
@Max(value = 100, message = "请输入数字") Integer age) {
System.out.println("是啥啊4");
return name;
}
@GetMapping("/test5")
public String test5(@Validated @NotEmpty(message = "name不能为空") String name,
@Valid @Max(value = 100, message = "请输入数字") Integer age) {
System.out.println("是啥啊5");
return name;
}
}
/**
* 【正面教材】类上加注解@Validated,下面的验证生效
*/
@RestController
@Validated
public class Test2Controller {
@GetMapping("/query")
public String queryUserInfo(@NotEmpty(message = "name不能为空") String name,
@Max(value = 100, message = "请输入数字") Integer age) {
System.out.println("是啥啊");
return name;
}
}
全局异常处理类
/**
* 参数校验异常包括MethodArgumentNotValidException、BindException、ConstraintViolationException
*/
@RestControllerAdvice
public class ExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler(MethodArgumentNotValidException.class)
public String handle(MethodArgumentNotValidException e) {
e.printStackTrace();
return e.getBindingResult().getFieldError().getDefaultMessage();
}
@org.springframework.web.bind.annotation.ExceptionHandler(BindException.class)
public String handle(BindException e) {
e.printStackTrace();
return e.getBindingResult().getFieldError().getDefaultMessage();
}
@org.springframework.web.bind.annotation.ExceptionHandler(ConstraintViolationException.class)
public String handle(ConstraintViolationException e) {
e.printStackTrace();
StringBuffer sb = new StringBuffer();
for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
sb.append(violation.getMessage());
}
return sb.toString();
}
}
进阶使用
分组校验
- 约束注解上声明适用的分组信息
groups
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
// 对于添加、修改都需要操作的公共属性,也可以不加groups标签,这时候,controller中入参需要写成@Validated({UserDTO.Update.class, Default.class}) 即可
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
@Validated
注解上指定校验分组
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
嵌套校验
前面的示例中,
DTO
类里面的字段都是
基本数据类型
和
String
类型。但是实际场景中,有可能某个字段也是一个对象,这种情况下,可以使用
嵌套校验
。
比如,上面保存
User
信息的时候同时还带有
Job
信息。需要注意的是,**此时
DTO
类的对应字段必须标记
@Valid
注解**。
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
// job属性上添加@Valid注解
@NotNull(groups = {Save.class, Update.class})
@Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}
public interface Save {
}
public interface Update {
}
}
自定义校验
- 自定义注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
// 指明使用那个校验器(类) 去校验使用了此标注的元素。
@Constraint(validatedBy = CaseUpperValidator.class)
@Documented
public @interface CaseUpper {
String message() default "必须大写";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 实现
ConstraintValidator
接口编写约束校验器
public class CaseUpperValidator implements ConstraintValidator<CaseUpper, String> {
// 提示信息
private String message;
// 实现校验逻辑的方法。value为当前需要进行校验的值,context可以给约束验证器时提供上下文数据和操作,比如设置错误信息等。
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 为空,返回失败
boolean isValid = false;
// value为空不校验;另外,value都是大写校验通过
if (Objects.isNull(value) || value.equals(value.toUpperCase())) {
isValid = true;
}
// 校验不通过,实现自定义错误信息
if (!isValid) {
//禁止默认消息返回
context.disableDefaultConstraintViolation();
//自定义返回消息
context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
}
return isValid;
}
// caseUpper为当前校验注解的实例,可以获取当前注解的属性值
@Override
public void initialize(CaseUpper caseUpper) {
this.message = caseUpper.message();
}
}
- 使用注解@CaseUpper
@Data
@ToString
public class User {
private Integer id;
@NotBlank(message = "姓名不能为空")
private String name;
@Max(value = 100, message = "最大不超过100")
private Integer age;
@NotBlank(message = "englishName不能为空")
@CaseUpper(message = "englishName必须大写")
private String englishName;
}
参考链接:
SpringBoot 实现各种参数校验,非常实用!_枫哥和java的博客-CSDN博客
Spring Validation最佳实践及其实现原理,参数校验没那么简单! - 掘金
版权归原作者 咸鱼爱吃青椒 所有, 如有侵权,请联系我们删除。