SpringBoot 优雅式数据校验
Spring Boot 优雅式数据校验
----javax.validation
0X01 需求描述
在实际项目开发中,总会需要对于由客户端用户传递过来的数据进行校验工作,如果需要校验的字段比较多,此时可以选择进行统一的异常处理并返回异常信息,如果需要进行精准返回错误信息则需要进行大量的条件判断,那么这样则编码工作是非常痛苦的,而且代码还相当冗长,所以这时我们可以使用Spring的javax.validation注解式参数校验来简化这部分的代码开发,从而写出更优雅式、清晰阅读、便于维护的代码。
0X02 快速实现
下面就简单写个Demo来体验一把使用javax.validation来简化业务开发的快感!
由于SpringBoot2.3.0之后就不在集成Validation组件了,那么此时就需要手动导入Spring Boot Starter Validation。或者可以降低SpringBoot的版本。我这里使用SpringBoot2.6.6。
1.导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2.编写实体类
package cn.imyjs.pojo;
import lombok.Data;
// 注意以下注解均是由javax.validation提供!
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* @Classname user
* @Description TODO
* @Date 2022/4/22 19:03
* @Created by YJS
* @WebSite www.imyjs.cn
*/
@Data // 此处是lombok提供相关注解,用于简化实体类开发
public class User {
@NotNull(message = "用户ID不得为空!")
private Integer id;
@Size(min = 3, max = 8, message = "用户名长度在3~8个字符!")
private String username;
@Size(min = 6, max = 16, message = "用户名长度在6~16个字符!")
private String password;
@Min(value = 18, message = "年龄最低限制18!")
@Max(value = 60, message = "年龄最高限制18!")
private Integer age;
}
3.编写控制器类
@RestController
@RequestMapping("user")
public class UserController {
/**
* 演示GET请求方式的使用
* @param user
* @return
*/
@GetMapping("register")
public String login( @Valid User user){ // 此处使用@Valid 注解开启校验
System.out.println(user);
return "注册成功!";
}
/**
* 演示POST请求方式的使用
* @param user
* @param bindingResult
* @return
*/
@PostMapping("register")
public String register(@Valid @RequestBody User user, BindingResult bindingResult){ // 此处使用@Valid 注解开启校验
List<ObjectError> allErrors = bindingResult.getAllErrors();
if ( allErrors.size() > 0){
return allErrors.get(0).getDefaultMessage();
}
System.out.println(user);
return "注册成功!";
}
}
此时,对于校验非法的数据相关提示信息并不会返回到前端。需要进行如下的处理!
4.编写异常处理类
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常
@ExceptionHandler(BindException.class)
@ResponseBody
public String BindExceptionHandler(BindException e) {
String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return message;
}
}
经过测试,此时只能处理GET请求的异常信息,那么对于其他方式请求可以在对应的控制层方法中,进行如下校验:
@PostMapping("register")
public String register(@Valid @RequestBody User user, BindingResult bindingResult){ // 此处使用@Valid 注解开启校验
List<ObjectError> allErrors = bindingResult.getAllErrors();
if ( allErrors.size() > 0){
return allErrors.get(0).getDefaultMessage();
}
System.out.println(user);
return "注册成功!";
}
- 在方法参数列表中添加
BindingResult bindingResult
- 然后通过
getAllErrors()
方法获取可能产生的校验失败信息 - 最后将失败信息返回即可!
0X03 常用注解
验证注解 | 验证的数据类型 | 说明 |
---|---|---|
@AssertFalse | Boolean,boolean | 验证注解的元素值是false |
@AssertTrue | Boolean,boolean | 验证注解的元素值是true |
@NotNull | 任意类型 | 验证注解的元素值不是null |
@Null | 任意类型 | 验证注解的元素值是null |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) | 和@Min要求一样 | 验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) | CharSequence子类型(如String) | 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
0X04 方法级别验证
在能够对传入的实体进行验证之后,希望能够对通过 @RequestParam 或者是 @PathVariable 传入的参数通过注解校验,那就很方便了。Spring对此也进行了扩展,使用方式如下:
- 增加配置 MethodValidationPostProcessor 参数校验处理器
package cn.imyjs.config; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; /** * @Classname MethodValidationPostProcessor * @Description TODO * @Date 2022/4/22 21:21 * @Created by YJS * @WebSite www.imyjs.cn */ @Configuration @EnableAutoConfiguration public class MethodValidationConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } }
- 在对应的 controller 或者 service 或者其他想要校验方法参数的类增加 @Validated
@RestController("hello") @Validated public class HelloController { @RequestMapping("/{username}") public String hello(@PathVariable @Size(min = 6, max = 16, message = "用户名长度在6~16位!") String username){ return "Hello" + username; } }
- 此时,需要进行重新编写参数异常处理类
@RestControllerAdvice public class GlobalExceptionHandler { // 处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常 // 用于处理类似http://localhost/user/register?age=10&username=aa请求中age和username的校验引发的异常 // BindException --- 用于处理请求参数为实体类时校验引发的异常 --- Content-Type为application/x-www-form-urlencoded @ExceptionHandler(BindException.class) @ResponseBody public String BindExceptionHandler(BindException e) { String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining()); return message; } //处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException @ExceptionHandler(ConstraintViolationException.class) @ResponseBody public String ConstraintViolationExceptionHandler(ConstraintViolationException e) { String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining()); return message; } //处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。 // MethodArgumentNotValidException --- 用于处理请求参数为实体类时校验引发的异常 --- Content-Type为application/json @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining()); return message; } // 可能出现的未知异常 @ExceptionHandler(value = Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String handle(Exception e) { return "系统未知异常"; } }
0X05 @Valid与@Validated
以下内容参考自网络:@Validated和@Valid区别
在检验 Controller 的入参是否符合规范时,使用 @Validated 或者 @Valid 在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
1. 分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。将不同的校验规则分给不同的组,在使用时,指定不同的校验规则:
- 创建接口类
/** * 校验分组1 */ public interface Group1 { } // ------------------------------ // /** * 校验分组2 */ public interface Group2 { }
- 实体类
@Data public class User { /** * 用户名 */ @NotBlank(message = "用户名不能为空!", groups = {Group1.class}) private String username; /** * 性别 */ @NotBlank(message = "性别不能为空!") private String gender; /** * 年龄 */ @Min(value = 1, message = "年龄有误!", groups = {Group1.class}) @Max(value = 120, message = "年龄有误!", groups = {Group2.class}) private int age; /** * 地址 */ @NotBlank(message = "地址不能为空!") private String address; /** * 邮箱 */ @Email(message = "邮箱有误!", groups = {Group2.class}) private String email; /** * 手机号码 */ @Pattern(regexp = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$",message = "手机号码有误!", groups = {Group2.class}) private String mobile; }
- 控制类
@RestController @RequestMapping("/api") public class Demo1Controller { @PostMapping("/insert1") public Result validatedDemo3(@Validated @RequestBody User user){ return ResultUtil.success(user); } @PostMapping("/insert2") public Result validatedDemo4(@Validated(Group1.class) @RequestBody User user){ return ResultUtil.success(user); } @PostMapping("/insert3") public Result validatedDemo5(@Validated(Group2.class) @RequestBody User user){ return ResultUtil.success(user); } }
@Valid:作为标准JSR-303规范,还没有分组的功能。
2. 注解地方
@Validated:可以用在类、方法和方法参数上。但是不能用在成员属性(字段)上。
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上。
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
3. 嵌套验证
在比较两者嵌套验证时,先说明下什么叫做嵌套验证。比如我们现在有个实体叫做Item:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Prop> props;
}
props带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。
现在我们有个 ItemController 接受一个Item的入参,想要对Item进行验证,如下所示:
@RestController
public class ItemController {
@RequestMapping("/item/add")
public void addItem(@Validated Item item, BindingResult bindingResult) {
doSomething();
}
}
在以上代码中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
4.小结
@Validated | @Valid | |
分组 | 提供分组功能,可在入参验证时,根据不同的分组采用不同的验证机制。 | 无分组功能 |
可注解位置 | 可以用在类型、方法和方法参数上。但是不能用在成员属性上 | 可以用在方法、构造函数、方法参数和成员属性上(两者是否能用于成员属性上直接影响能否提供嵌套验证的功能) |
嵌套验证 | 用在方法入参上无法单独提供嵌套验证功能。
不能用在成员属性上。 也无法提供框架进行嵌套验证。 能配合嵌套验证注解@Valid进行嵌套验证。 |
用在方法入参上无法单独提供嵌套验证功能。
能够用在成员属性上,提示验证框架进行嵌套验证。 能配合嵌套验证注解@Valid进行嵌套验证。 |
总体来说@validated 相当于 @Valid 验证的升级版,功能更加强大。
0X06 微信关注