SpringBoot优雅的参数校验-validation

SpringBoot优雅的参数校验-validation

前言

    在后端开发过程中,我们避免不了对前端传过来的参数进行检验。有的人会说,前端检验不就行了吗,为啥还要后端检验一遍?我们前端经常对我说的一句话:”后端不要信任前端,你怎么知道接口不会被拦截,前端就一定不会传错。“。因此,参数校验是非常重要的一个环节,严格参数校验会减少很多出bug的概率,增加接口的安全性,增强程序的健壮性。

    当我们在做参数检验的时候,可能会出现这种情况:参数太多,我们要写很多条件判断语句,这样就会显得代码不够整洁,阅读体验也不是很好。
SKO0yt
    因此我们需要一种优雅的方式来处理上面的问题,进行SpringBoot统一参数校验。那就是今天我们的主角validation啦。

使用方式

    通过@Validated这一注解配合一些参数校验注解(PS:@NotNull,@NotEmpty)。然后对抛出的异常进行全局统一捕获然后返回错误信息。

引入依赖

1
2
3
4
5
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

PostParams

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.spring_boot_validation.entity.params;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

/**
* @Author: Roger
* @description: post参数类
* @date: 2020/7/25 4:31 下午
*/
@Data
public class PostParams {
/**
* ID
*/
@NotNull(message = "ID不能为空")
private Integer id;
/**
* 名称
*/
@NotNull(message = "名称不能为空")
private String name;
/**
* 数组
*/
@NotEmpty(message = "数组里面至少有一个元素")
private List<Integer> array;
/**
* 对象
*/
@Valid
@NotNull(message = "item不能为空")
private Item item;
}

TestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RestController
@Validated
public class TestController {
@GetMapping("/get-test")
public ResponseEntity<Object> getTest(@RequestParam(required = false) @NotNull(message = "offset不能为空") Integer offset,
@RequestParam(required = false) @NotNull(message = "limit不能为空") Integer limit){
return new ResponseEntity<>("ok", HttpStatus.OK);
}

@PostMapping("/post-test")
public ResponseEntity<Object> postTest(@Valid @RequestBody PostParams postParams){
return new ResponseEntity<>(HttpStatus.CREATED);
}

@GetMapping("/get-test2")
public ResponseEntity<Object> getTest2(@RequestParam(required = false) @NotNull(message = "offset不能为空") Integer offset,
@RequestParam(required = false) Integer limit){
return new ResponseEntity<>("ok", HttpStatus.OK);
}

@GetMapping("/get-test3")
public ResponseEntity<Object> getTest3(@Valid Item item){
return new ResponseEntity<>("ok", HttpStatus.OK);
}

@GetMapping("/get-test4")
public ResponseEntity<Object> getTest4(@NotNull(message = "offset不能为空") Integer offset,
Integer limit){
return new ResponseEntity<>("ok", HttpStatus.OK);
}
}

ControllerAdvice

参数检验异常统一拦截处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RestControllerAdvice
public class ControllerAdvice {

//参数检验错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ResultVo<Object> handlerMethodArgumentNotValidException(final MethodArgumentNotValidException e) {
List<ObjectError> objectErrors = e.getBindingResult().getAllErrors();
StringBuilder errorMessages = new StringBuilder();
objectErrors.forEach(objectError -> errorMessages.append(objectError.getDefaultMessage()).append(";"));
return new ResultVo<>(0,String.valueOf(errorMessages),null);
}

//参数检验错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ResultVo<Object> handlerConstraintViolationException (final ConstraintViolationException e) {
String errorMessages = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
return new ResultVo<>(0,String.valueOf(errorMessages),null);
}

//参数检验错误 validate失败后抛出的异常是BindException异常。
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ResultVo<Object> handlerConstraintViolationException (final BindException e) {
String errorMessages = e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(";"));
return new ResultVo<>(0,String.valueOf(errorMessages),null);
}
}

​ 在使用参数检验的过程中,主要会抛出以上三种异常BindException、MethodArgumentNotValidException、ConstraintViolationException。原因主要是因为跟请求发起的数据格式(content-type)有关系,对于不同的传输数据的格式spring采用不同的HttpMessageConverter(http参数转换器)来进行处理。

​ 请求体(@RequestBody)绑定到java bean上失败时抛出MethodArgumentNotValidException;普通参数(@RequestParam)(非 java bean)校验出错时抛出ConstraintViolationException;请求参数绑定到java bean上失败时抛出BindException;

测试

请求POST: http://localhost:8080/post-test ,结果如下

QwBokF

常用的一些注解解析

@Validated 和 @Valid 的异同
注解 范围 嵌套 校验组
@Validated 可以标记类、方法、方法参数,不能用在成员属性(字段)上 不支持 支持
@Valid 可以标记方法、构造函数、方法参数和成员属性(字段)上 支持 不支持

通常在使用过程中,我们把@Validated标记在类上,然后@Valid标记在实体中的属性上。在Controller中使用,把@Validated标记在类上。然后针对java bean的参数就用@Valid注解。如下所示:

0LcqcD

校验注解一览表

注解 作用
@Valid 被注释的元素是一个对象,需要检查此对象的所有字段值
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电子邮箱地址
@Length(min=, max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=, max=) 被注释的元素必须在合适的范围内
@NotBlank 被注释的字符串的必须非空
@URL(protocol=,host=, port=,regexp=, flags=) 被注释的字符串必须是一个有效的url

嵌套验证

我们很多时候会存在这样的业务场景,前端会给后端传递一个list,我们不仅要限制每次请求list内的个数,同时还要对list内基本元素的属性值进行校验。这个时候就需要进行嵌套验证了,实现的方式很简单。在list上添加@Vaild就可以实现了。

UqPx8n