technology-note/java/springboot系列/spring-boot参数校验.md
2022-01-18 16:11:28 +08:00

345 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: "20211009"
date: "2021-10-09 15:42:00"
title: "SpringBoot参数校验看这篇就够了"
tags: ["java", "SpringBoot","exception",“validation”]
categories:
- "java"
- "spring boot学习"
---
**本文所用到的全部代码见文末**
## 前言
任何一个项目都需要对接口做参数校验最简单粗暴的校验方式就是在代码中硬编码来一个个检查参数这种方式显然是很不优雅的Spring 已经为我们设计了一套比较优雅的校验方式,本篇文章将进行详细说明。
## 两个注解`@Valid`和`@Validated`
标题中的两个注解,想必是大家经常看到的,但是大部分人应该都不清楚这两个注解有什么区别(比如我),本节将解开你的疑惑。
首先来看看下这两个注解的来源:
`@Valid`:`javax.validation.Valid`,来自于 javax,属于标准 JSR-380 规范
`@Validated``org.springframework.validation.annotation.Validated`,来自于 spring validation,属于 Spring 的 JSR-380 规范,是标准 JSR-380 的一个变种,有一些增强功能。
<!-- more -->
由此可以看出这两个注解大部分功能相似,但是存在一些区别。
**网上大部分文章说的是 JSR-303,JSR-303 是很老的标准了Bean Validation 1.0JSR-380 是目前最新的标准Bean Validation 2.0,具体可在[此网站](https://beanvalidation.org/)中查看相关文档**
### 作用域区别
这两个注解的作用域定义如下:
```java
//@Valid
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
//@Validated
@Target({TYPE, METHOD, PARAMETER})
```
`@Valid`可作用于方法、**成员属性**、构造函数、方法参数、使用类型的任意语句中(这个是 Java8 中新增的)
`@Validated`可作用于类型、方法、方法参数上
可以看到只有`@Valid`能够作用在成员属性上,也就是只能通过它能够实现嵌套验证(什么是嵌套验证见后文),单单`@Validated`不能实现。
### 功能区别
对比两个注解的定义可以发现,@Validated 多了一个参数`Class<?>[] value() default {}`,可以传入 Class 参数,此参数用于分组校验逻辑的实现。`@Valid`目前为止还不支持分组。
## 校验内容
**本节只说明如何进行校验,校验异常全局处理见下一节**
当我们引入校验依赖后,可以发现有很多的校验注解可以使用,比如@NotNull,@Range,@Email 等等,我们仔细查看这些注解的来源,可以发现部分注解是来自于`javax.validation`,部分注解是来自于`org.hibernate.validator`,为啥呢
javax 是 JSR-380 的标准实现hibernate 是这个规范的参考实现并扩充了一些功能。如果标准实现将 hibernate 中扩充的某些校验加入到标准后hibernate 就会将自身的校验实现标记为过期(比如 6.2 版本中的@Email 注解)
具体有哪些校验注解可以[看看这篇文章](https://juejin.cn/post/6844903976270299149)
## 使用
### 依赖引入
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.5.5</version>
</dependency>
```
引入这个依赖会一起引入`javax.validation``org.hibernate.validator`
每个校验注解中都有一个 message 参数,此参数可传入国际化的 key,或者自定义的字符串,用于自定义错误提示。
### url 参数校验
首先需要在类上加一个`@Validated`注解,否则无法对 url 参数进行校验
```java
@RestController
@Validated
public class TestController {
}
```
然后在对具体的方法参数进行校验,比如:
```java
/**
* url参数校验
*/
@GetMapping("/test1")
public Result test1(@NotBlank @RequestParam String param1, @Range(max = 100, min = 10) @RequestParam int param2) {
return null;
}
```
### post 表单校验
首先定义入参类 TestBody.java
```java
@Data
public class TestBody {
@NotBlank
@Email(message = "请输入一个邮箱")
private String param1;
@NotNull
private String param2;
}
```
然后在入口方法中对要校验的参数加上@Valid 或者@Validated
```java
@PostMapping("/test2")
public Result test2(@Validated @RequestBody TestBody body) {
return null;
}
```
### 嵌套校验
何谓嵌套?就是一个对象中包含另外的对象。默认情况下是不会对潜逃对象中的属性进行校验的。比如新建一个 TestBody2 类,其中包含 TestBody
```java
@Data
public class TestBody2 {
@NotBlank
@Email(message = "请输入一个邮箱")
private String param1;
@NotNull
private String param2;
@NotNull
private TestBody testBody;
}
```
上述情况在校验时只会校验 testBody 是否为 null,并不会校验其中的属性。必须要用@Valid@Validated 不能修饰属性,所以只能用@valid)来注解 testBody 属性,才会对其属性进行校验。
入口方法如下:
```java
/**
* 嵌套校验
*/
@PostMapping("/test3")
public Result test3(@Valid @RequestBody TestBody2 testBody2) {
throw new RuntimeException("我是test2");
}
```
### 分组校验
在实际的业务需求中,可能一个入参类,会被多个接口使用,每个接口有不同的校验逻辑,这是就需要对校验逻辑进行分组,以在不同的接口中调用不同的校验组和。例子如下:
建立 TestBody3
```java
@Data
public class TestBody3 {
@NotBlank(groups = {Insert.class})
private String param1;
@NotBlank(groups = {Update.class})
private String param2;
}
```
通过**groups**属性来说明本注解是在哪些情况下生效,如 param1 只在 Insert 模式下才会进行校验param2 只在 Update 模式下生效,默认在入口函数中定义,如下:
```java
/**
* 分组校验1
*/
@PostMapping("/test4")
public Result test4(@Validated({Update.class}) @RequestBody TestBody3 testBody) {
return null;
}
/**
* 分组校验2
*/
@PostMapping("/test5")
public Result test5(@Validated({Insert.class}) @RequestBody TestBody3 testBody) {
return null;
}
```
**注意分组校验必须使用`@validated`注解**
### 集合类校验
集合类校验分两种
一种是对象内集合的校验,直接使用@valid 注解该属性即可,例如:
```java
@Data
public class TestBody5 {
@Valid
private List<TestBody4> list;
}
```
使用示例见:`com.fanxb.exceptiontest.controller.TestController#test7`
另外一直直接接收集合入参,如:
```java
/**
* 集合校验2对象集合
*/
@PostMapping("/test8")
public Result test8(@RequestBody List<@Valid TestBody4> list) {
return null;
}
```
这种需要使用`@Valid`来注解泛型类,使用示例见`com.fanxb.exceptiontest.controller.TestController#test8`
### 自定义校验
最后,校验依赖提供的校验逻辑是有限的,有时我们需要进行一些特殊的校验,这就需要实现自定义校验注解,实现流程如下:
首先定义注解
```java
@Target({ElementType.FIELD}) //作用于字段
@Retention(RetentionPolicy.RUNTIME)//生命周期
@Constraint(validatedBy = CustomCheckValidator.class)//校验逻辑实现类
public @interface CustomCheck {
String message() default "自定义校验默认错误提示";
/**
* 自定义参数可传递到校验实现类CustomCheckValidator中
*/
String param1() default "";
Class<?>[] groups() default {}; //用于分组校验
Class<? extends Payload>[] payload() default {};
}
```
然后编写校验逻辑类
```java
public class CustomCheckValidator implements ConstraintValidator<CustomCheck, String> {
private String param1;
@Override
public void initialize(CustomCheck constraintAnnotation) {
//在此获取校验参数
param1 = constraintAnnotation.param1();
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
return param1 != null && param1.equals(s);
}
}
```
最后即可向正常校验注解一样使用。详情见代码`TestController#test6`
## 校验异常处理
### `BindingResult`处理
前一节讲了如何进行参数校验,那么参数校验的结果怎么处理呢?一种方式是将校验结果放在`BindingResult`中,如下:
```java
/**
* 自定义校验
*/
@PostMapping("/test6")
public Result test6(@Validated @RequestBody TestBody4 testBody,BindingResult result) {
if(result.hasErrors()){
log.info("asdf");
}
return null;
}
```
显而易见这种处理方式很繁琐,需要对每个接口都做一样的处理,不推荐使用。
**推荐使用全局异常处理来对校验结果进行统一处理**
### 全局异常处理
[上一篇:blog.fleyx.com/blog/detail/20210927/](https://blog.fleyx.com/blog/detail/20210927/)中详细说明了springboot的全局异常处理,不了解的可以去看看。
在全局异常处理类`Exceptionhandle`中能对全部的异常进行捕获处理,那么我们只需要找到参数校验抛出的异常,然后针对这些异常进行处理就可以了。代码如下:
```java
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
BaseException be;
if (e instanceof BaseException) {
be = (BaseException) e;
//手动抛出的异常仅记录message
log.info("baseException:{}", be.getMessage());
if (be instanceof CustomBusinessException) {
//可在这里进行一些针对特定异常的处理逻辑
log.info("customBusinessException:{}", be.getMessage());
}
} else if (e instanceof ConstraintViolationException) {
//url参数、数组类参数校验报错类
ConstraintViolationException ce = (ConstraintViolationException) e;
//针对参数校验异常,建立了一个异常类
be = new CustomValidException(ce.getMessage());
} else if (e instanceof MethodArgumentNotValidException) {
//json对象类参数校验报错类
MethodArgumentNotValidException ce = (MethodArgumentNotValidException) e;
be = new CustomValidException(Objects.requireNonNull(ce.getFieldError()).getDefaultMessage());
} else {
//其它异常,非自动抛出的,无需给前端返回具体错误内容(用户不需要看见空指针之类的异常信息)
log.error("other exception:{}", e.getMessage(), e);
be = new BaseException("系统异常,请稍后重试", e);
}
return new Result(be.getCode(), be.getMessage(), null);
}
}
```
核心是对`ConstraintViolationException``MethodArgumentNotValidException`两种异常的处理。
## 结尾
本篇收集了大量的材料,对参数校验的相关内容覆盖应该比较全面了,码字不已,望点赞收藏。
**本篇用到的全部代码见:**[github](https://github.com/FleyX/demo-project/tree/master/spring-boot/paramsCheck)
**本文原创发布于:[https://blog.fleyx.com/blog/detail/20211009](https://blog.fleyx.com/blog/detail/20211009)**