• SpringBoot如何优雅的进行参数校验


    写在前面

    上一篇文章中我们学会了如何优雅的接收前端参数,传送门

    SpringBoot如何优雅的接收前端参数

    接收到参数后,接下来要做的就是校验参数的合法性。这一步的重要性就不用多说了。

    即使前端已经对数据进行了校验,我们后端还是要再对接收到的数据进行一遍彻底的校验。

    这样可以避免张三等人利用Http工具,绕过浏览器非法请求数据。

    废话不多说,看完这篇文章,你将从繁琐的校验逻辑中解脱出来

    一、传统参数校验

    虽然往事不堪回首,但还是得回忆一下我们传统参数校验的痛点。

    下面是我们传统校验用户名和邮箱是否合法的代码

    if (username == null || username.isEmpty()) {
    throw new IllegalArgumentException("用户名不能为空");
    }
    if (isValidEmail(email)) {
    throw new IllegalArgumentException("邮箱格式不正确");
    }
    public boolean isValidEmail(String email) {
    String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
    Pattern pattern = Pattern.compile(emailRegex);
    Matcher matcher = pattern.matcher(email);
    return matcher.matches();
    }

    这样的代码不仅冗长,而且难以维护,尤其是在多个地方重复使用时,容易出错。

    面对上面的痛点,我们就得解放双手,利用框架来完成校验。

    它只需要通过简单的注解来定义校验规则,让框架来帮助我们处理校验逻辑,让我们代码变得更加的优雅。

    二、几个名词

    问题①:JSR是什么?

    JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解。

    我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!

    问题②:Bean Validation是什么?

    Bean Validation是一个抽象的框架,它定义了验证规则,而不会涉及具体的业务逻辑

    问题③:Hibernate Validator是什么?

    Bean Validation的实现,目前最新版的 Hibernate Validator 6.xBean Validation 2.0(JSR 380)的参考实现

    三、所需依赖

    Spring boot 2.3以前版本,Springbootspring-boot-starter-web默认内置了Hibernate-Validator

    这些版本直接引入spring-boot-starter-web即可,后面的版本需要单独引入

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-validationartifactId>
    dependency>

    在后面的测试中会用到lombokSpringBootwebtest等基础依赖,这里就不一一给出

    四、注解及作用

    注解 作用类型 作用
    @NotBlank(message='') 字符串 被注释字符串非null,且长度必须大于0
    @NotEmpty 字符串 被注释的字符串必须非空
    @NotNull 任意 被注释的元素不能为null
    @Null 任意 被注释的元素必须为null
    @Email 字符串 被注释的元素必须是电子邮箱地址
    @AssertTrue 布尔值 被注释的元素必须为true
    @AssertFalse 布尔值 被注释的元素必须为false
    @Max(value = , message = "") 数字 被注释的元素必须是一个数字,且小于或等于最大值
    @Min(value = , message = "") 数字 被注释的元素必须是一个数字,且大于或等于最小值
    @DecimalMax(value = "", message = "") 数字 被注释的元素必须是一个数字,且小于或等于最大值
    @DecimalMin(value = "", message = "") 数字 被注释的元素必须是一个数字,且大于或等于最小值
    @Pattern(regex=,flag=) 字符串 被注释的元素是否符合正则表达式规则
    @Size(max=, min=) 数字 被注释的元素的大小必须在指定范围内,min 表示最小,max表示最大
    @Digits (integer, fraction) 数字 被注释的元素必须是一个数字,且在可接收范围内
    @Positive 数字 被注释的元素必须是正数
    @PositiveOrZero 数字 被注释的元素必须是0或正数
    @Negative 数字 被注释的元素必须是负数
    @NegativeOrZero 数字 被注释的元素必须是0或者负数
    @Past 日期 被注释的元素必须是一个过去的日期
    @PastOrPresent 日期 被注释的元素必须是一个过去或当前日期
    @Future 日期 被注释的元素必须是一个过去的日期
    @FutureOrPresent 日期 被注释的元素必须是一个将来或当期的日期

    看到这些注解后,大家可能会对【@NotNul@NotEmpty@NotBlank】这三个注解有点不理解,这里稍作解释

    • @NotNull:任何对象的value不能为null。
    • @NotEmpty:集合对象的元素不为0,即集合不为空,也可以用于字符串不为null。
    • @NotBlank:只能用于字符串不为null,并且字符串trim()以后length要大于0。

    五、快速入门

    5.1 新增加一个一个User 实体类

    @Data
    public class User {
    //姓名
    @NotBlank(message = "用户名不能为空") //注解确保姓名不为空
    private String name;
    //性别
    @NotBlank(message = "性别不能为空") //注解确保性别不为空
    private String sex;
    //年龄
    @NotNull(message = "年龄不能为空") //注解确保年龄不为空
    @Max(value = 120,message = "年龄不能大于120") //注解确保年龄必须小于等于120
    @Min(value = 18,message = "年龄不能小于18") //注解确保年龄必须大于等于18
    private Integer age;
    //邮箱
    @Email(message = "邮箱格式不正确") //注解确保邮箱格式正确
    @NotBlank(message = "邮箱不能为空")
    private String email;
    }

    上述代码说明:

    • @NotBlank: 此注解确保字符串不为空并且不能为空字符串,且去掉前后空格后的长度必须大于 0。它常用于字符串字段验证。message 属性用于指定提示信息;
    • @NotNull: 此注解确保整数类型不能为 null
    • @Min@Max: 这两个注解用于验证数字值是否在指定的范围内。例如,在上面的示例中,我们想要确保 age 的值在 18 到 120 之间;
    • @Email: 此注解用于验证字符串值是否是有效的电子邮件地址格式。

    5.2 Controller层参数校验

    下图是controller层校验流程

    Controller层校验流程

    @RestController
    public class ValidatorController {
    //测试参数校验
    @RequestMapping("/testValidator")
    public ResponseEntity testValidator(@Valid @RequestBody User user, BindingResult bindingResult){
    // 是否存在校验错误
    if (bindingResult.hasErrors()) {
    // 获取校验不通过字段的提示信息
    String errorMsg = bindingResult.getFieldErrors()
    .stream()
    .map(FieldError::getDefaultMessage)
    .collect(Collectors.joining(", "));
    return ResponseEntity.badRequest().body(errorMsg);
    }
    return ResponseEntity.ok("参数校验成功");
    }
    }

    解释一下上面代码:

    • @Validated: 告诉 Spring 需要对 User 对象执行校验; 这个一定不要忘记加上
    • BindingResult : 该类包含校验不通过时的异常信息,校验不通过时,我们通过这个对象来获取注解中message="xxx"中的内容

    注意:当注解校验不通过时,直接将异常信息返回给前端其实并不友好,我们可以将异常包装一下再丢给前端

    5.3 测试校验结果

    这里我们使用postman工具测试一下参数校验是否成功

    入参正确情况

    {
    "name":"小凡",
    "sex":"男",
    "age":18,
    "email":"xiezhr@qq.com"
    }

    入参正确

    入参不正确的情况

    {
    "name":null,
    "sex":"",
    "age":17,
    "email":"xiezhrqq.com"
    }

    入参不正确

    通过上面的入门小案例,你学会了么?

    上面的返回结果看起来可能不是那么优雅,那么怎么封装统一返回结果呢,

    传送门在此优雅的封装返回结果

    六、单个参数校验

    上面快速入门中我们说了实体参数校验,这小节,我们来看看单个参数的校验

    6.1 controller层校验代码

    @RequestMapping("/testSingleParmaValidator")
    public ResponseEntity testSingleParmaValidator(@NotBlank(message = "姓名不能为空") String name,
    @Min(value = 18,message = "年龄不能小于18")
    @Max(value = 120,message = "年龄不能大于120") Integer age
    ){
    // 参数校验
    return ResponseEntity.ok("参数校验成功");
    }

    6.2 全局异常捕获

    当参数校验不通过会发生如下异常信息

    异常信息

    这里我们不能像上面一样通过BindingResult 来获取异常信息,需要添加全局异常捕获校验失败异常,具体代码如下

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    //处理ValidationException异常
    @ExceptionHandler(ValidationException.class)
    //返回状态码为400
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity handleValidationExceptions(ValidationException ex) {
    String message = "";
    //判断异常类型
    if(ex instanceof ConstraintViolationException){
    ConstraintViolationException exs = (ConstraintViolationException) ex;
    //获取验证不通过的信息
    Set> violations = exs.getConstraintViolations();
    //遍历验证不通过的信息
    for (ConstraintViolation item : violations) {
    //将验证不通过的信息拼接到message中
    message+=item.getMessage()+",";
    }
    }
    //返回错误信息
    return ResponseEntity.badRequest().body(message);
    }
    }

    6.3 测试校验结果

    入参正确情况

    http://localhost:8080/testSingleParmaValidator?name=小凡&age=18

    入参正确的情况

    入参不正确情况

    http://localhost:8080/testSingleParmaValidator?name=&age=17

    入参不正确情况

    八、参数校验分组

    在实际开发中,我们会遇到这样的情况:同一个实体类可能会在多个接口中使用,但每次的校验场景又不一样。

    例如:新增用户和修改用户接口,参数都是User 实体,在新增用户的时候ID字段 可以为空,但name字段 不能为空

    在修改用户的是由ID字段不能为空,这种时候就可以使用参数分组来实现。

    8.1 定义验证分组接口

    定义两个分组接口CreateUserGroup(用户创建组),UpdateUserGroup(用户更新组),

    分别继承javax.validation.groups.Default,标识不同的业务场景

    public interface CreateUserGroup extends Default {
    }
    public interface UpdateUserGroup extends Default {
    }

    注:继承Default并不是必须的。只是说,如果继承了Default,那么@Validated(value = Create.class)的校验范畴就
    为【Create】和【Default】;如果没继承Default,那么@Validated(value = Create.class)的校验范畴只
    为【Create】,而@Validated(value = {Create.class, Default.class})的校验范畴才为【Create】和【Default】

    8.2 分组校验的使用

    ① 在实体中添加groups 属性

    @Data
    public class User {
    //用户ID
    @NotNull(message = "用户ID不能为空",groups = UpdateUserGroup.class) //用户更新接口必须传递用户ID
    private Integer id;
    //姓名
    @NotBlank(message = "用户名不能为空",groups = CreateUserGroup.class) //用户创建接口必须传递用户名
    private String name;
    //性别
    @NotBlank(message = "性别不能为空") //注解确保性别不为空
    private String sex;
    //年龄
    @NotNull(message = "年龄不能为空") //注解确保年龄不为空
    @Max(value = 120,message = "年龄不能大于120") //注解确保年龄必须小于等于120
    @Min(value = 18,message = "年龄不能小于18") //注解确保年龄必须大于等于18
    private Integer age;
    //邮箱
    @Email(message = "邮箱格式不正确") //注解确保邮箱格式正确
    @NotBlank(message = "邮箱不能为空")
    private String email;
    }

    ②在接口中使用分组

    使用 @Validated 注解,并指定要执行的验证组。

    //添加用户
    @PostMapping("/addUser")
    public ResponseEntity addUser(@Validated(value= CreateUserGroup.class) @RequestBody User user){
    return ResponseEntity.ok(user);
    }
    //更新用户
    @PutMapping("/updateUser")
    public ResponseEntity updateUserUser(@Validated(value= UpdateUserGroup.class) @RequestBody User user){
    return ResponseEntity.ok(user);
    }

    我们指定create接口指定CreateUserGroup分组,update接口指定UpdateUserGroup

    8.3 测试一下接口

    接口入参

    {
    "name":"小凡",
    "sex":"男",
    "age":18,
    "email":"xiezhr@qq.com"
    }

    addUser接口添加用户,不需要id,验证通过

    添加用户

    updateUser接口修改用户,需要传入id,校验不通过

    修改用户

    九、嵌套对象校验

    9.1 构造一个员工信息表

    @Data
    public class Emp {
    @NotBlank(message = "员工编号不能为空")
    private String empNo;
    @NotBlank(message = "员工姓名不能为空")
    private String empName;
    @NotBlank(message = "员工职位不能为空")
    private String job;
    @Valid //这里必须使用@Valid注解
    private Dept dept;
    }
    @Data
    public class Dept {
    @NotBlank(message = "部门编号不能为空")
    private String deptNo;
    @NotBlank(message = "部门名称不能为空")
    private String deptName;
    }

    在这个示例中, Dept 类包含三个字段需要校验: deptNo 和``deptName字段,通过在 Dept类中的每个字段上添加相应的校验注解,然后在Emp类中的dept字段上添加@Valid` 注解,可以实现对嵌套对象中多个字段进行参数校验。

    9.2 嵌套对象的使用

    @PostMapping("/emp")
    public ResponseEntity createOrder(@Valid @RequestBody Emp emp) {
    return ResponseEntity.ok("参数校验成功");
    }

    9.3 测试一下

    ① 正确入参情况

    {
    "empNo":"10001",
    "empName":"小凡",
    "job":"程序员",
    "dept":{
    "deptNo":"20001",
    "deptName":"研发部111"
    }
    }

    正确入参情况

    ② 不正确入参情况

    {
    "empNo":"10001",
    "empName":"",
    "job":"程序员",
    "dept":{
    "deptNo":"20001",
    "deptName":""
    }
    }

    不正确入参情况

    十、自定义参数校验

    SpringBoot 提供的注解校验功能可以满足大多数的验证需求,但如果在系统中需要实现一些特殊的校验功能时,

    我们可以根据规则自定义校验

    下面我们来手把手教你自定义一个字符串校验,校验字符串必须为大写或小写

    10.1 自定义注解类

    我们要自定义验证功能,需要首先自定义注解,以便我们在实体类中使用它,代码如下

    ①定义一个枚举类 CaseMode

    public enum CaseMode {
    UPPER,
    LOWER;
    }

    ②创建一个自定义的校验注解 @CheckCase

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = CheckCaseValidator.class)
    public @interface CheckCase {
    String message() default "字符串必须是大写或小写";
    Class[] groups() default {};
    Classextends Payload>[] payload() default {};
    CaseMode value();
    }

    10.2 自定义验证业务逻辑类

    public class CheckCaseValidator implements ConstraintValidator {
    private CaseMode caseMode;
    @Override
    public void initialize(CheckCase constraintAnnotation) {
    // 获取约束注解的值
    this.caseMode = constraintAnnotation.value();
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
    // 如果值为空,则返回true
    if (value == null) {
    return true;
    }
    // 根据caseMode的值,判断value是否需要转换大小写
    if (caseMode == CaseMode.UPPER) {
    return value.equals(value.toUpperCase());
    } else {
    return value.equals(value.toLowerCase());
    }
    }
    }

    10.3 自定义校验注解使用

    ①在Car实体类上添加注解

    @Data
    public class Car {
    //车牌号
    @CheckCase(value = CaseMode.UPPER,message = "车牌号必须为大写")
    private String brand;
    //颜色
    @CheckCase(value = CaseMode.LOWER,message = "颜色必须为小写")
    private String color;
    }

    ②在controller 中校验参数

    @GetMapping ("/car")
    public ResponseEntity validatorCar(@Valid @RequestBody Car car) {
    return ResponseEntity.ok("参数校验成功");
    }

    10.4 测试一下

    ①入参正确情况

    {
    "brand":"云A.888888",
    "color":"red"
    }

    入参正确情况

    ②入参错误情况

    {
    "brand":"云a.888888",
    "color":"RED"
    }

    入参错误情况

    以上就是本期的全部内容,希望对你有所帮助,我们下期再见 (●'◡'●)

  • 相关阅读:
    开源版小剧场短剧影视小程序源码+支付收益等模式+付费短剧小程序源码+完整源码包和搭建教程
    个人信息保护视域下知情同意框架的应用困境与对策探析
    Python实现SSA智能麻雀搜索算法优化支持向量机分类模型(SVC算法)项目实战
    asp毕业设计——基于C#+asp.net+sqlserver作业审阅系统设计与实现(毕业论文+程序源码)——作业审阅系统
    LeetCode_优先级队列_回溯_659.分割数组为连续子序列
    TEE威胁评分与评级
    RabbitMQ之消息可靠性投递解读
    报错 | Cannot find module ‘@better-scroll/core/dist/types/BScroll‘
    Linux中的DNS服务搭建与管理
    一种电磁兼容半电波暗室设计和实现
  • 原文地址:https://www.cnblogs.com/xiezhr/p/18093602