SpringBoot表单参数校验及自定义参数校验注解一、H

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

在日常api开发中,为了程序的安全性和用户体验,我们都离不开api入参校验,我们可以使用Hibernate Validator 对参数进行校验。当内置验证规则不能满足我们的校验需求,可以通过自定义注解配合Hibernate Validator 较为优雅的实现参数校验。

一、Hibernate Validator

1. 常用校验注解介绍

@AssertFalse 注解的元素必须是Boolean类型,且值为false

@AssertTrue 注解的元素必须是Boolean类型,且值为true

@DecimalMax 注解的元素必须是数字,且值小于等于给定的值

@DecimalMin 注解的元素必须是数字,且值大于等于给定的值

@Digits 注解的元素必须是数字,且值必须是指定的位数

@Max 注解的元素必须是数字,且值小于等于给定的值

@Min 注解的元素必须是数字,且值小于等于给定的值

@Range 注解的元素需在指定范围区间内

@Future 注解的元素必须是将来某个日期

@Past 注解的元素必须是某个过去的日期

@PastOrPresent 注解的元素必须是过去某个或现在日期

@NotNull 注解的元素值不能为null

@NotBlank 注解的元素值有内容(不为null、去除首位空格后长度为0),用于字符串

@Null 注解的元素值为null

@Pattern 注解的元素必须满足给定的正则表达式

@Size 注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内

@Email 注解的元素需满足Email格式

2. 验证器使用

a. RequestParam参数校验

注意:当校验 RequestParam 的时候,@Validated 注解必须放在控制器类上,否则校验注解不生效。

  • 错误示范:
  @RestController
  @RequestMapping("test")
  public class TestController {
      @GetMapping("users")
      public void userList(@Validated @Min(value = 1, message = "页码不能小于1") @RequestParam(name = "page", defaultValue = "1") Integer page) {
          System.out.println(page);
      }
  }
复制代码
  • 正确做法:
  @RestController
  @RequestMapping("test")
  @Validated
  public class TestController {
      @GetMapping("users")
      public void userList(@Min(value = 1, message = "页码不能小于1") @RequestParam(name = "page", defaultValue = "1") Integer page) {
          System.out.println(page);
      }
  }
复制代码

当参数校验不通过的时候,我们在控制台就能看到抛出的异常:

javax.validation.ConstraintViolationException: userList.page: 页码不能小于1
复制代码

实际业务中我们希望结果是这样的 {"code": 400, msg: "页码不能小于1", "data": null},有个统一的返回格式。

  • 创建一个统一响应类,代码如下:
  public class UnifyResponse<T> {
    	private int code;
      private String msg;
      private T data;
    
    	public UnifyResponse (int code, String message) {
          this.code = code;
          this.msg = message;
          this.request = request;
          this.data = null;
      }
      public UnifyResponse (int code, String message, T data) {
            this.code = code;
            this.msg = message;
            this.request = request;
            this.data = null;
      }
  }
复制代码
  • 使用@ControllerAdvice 注解实现参数校验异常处理:
  @ControllerAdvice
  public class GlobalExceptionAdvice {
      // ConstraintViolationException参数校验异常处理
      @ExceptionHandler(ConstraintViolationException.class)
    	// 响应http状态码为400
      @ResponseStatus(code = HttpStatus.BAD_REQUEST)
      @ResponseBody
      public UnifyResponse handleConstraintException (HttpServletRequest request, ConstraintViolationException e) {
        	// 获取参数校验的异常信息,注解的message
          String errorMsg = e.getConstraintViolations().iterator().next().getMessage();
          return new UnifyResponse(400, errorMsg);
      }
  }
复制代码
  • 当参数校验失败时候就会响应
  {
    "code": 40009,
    "msg": "页码不能小于1",
    "data": null
  }
复制代码

b. RequestBody校验

复杂请求的时候,我们肯能就会创建bean 来接收这些参数,我们可以在每个需要校验的成员变量上使用校验注解。例如有个需求,新增用户,字段分别为usernamepassword

注意:bean校验必须在参数前添加 @Validated 注解,否则验证器不生效。

  • 创建bean,增加校验规则:username 必填,并且长度2-50字符;password 必填,长度6-20字符。
  @Data
  class User {
    @NotBlank(message = "用户名必填")
    @Length(min = 2, max = 20, message = "用户名2-50字")
    private String username;
    @NotBlank(message = "密码必填")
    @Length(min = 6, max = 20, message = "密码长度6-20")
    private String password;
  }
复制代码
  • 控制器
  @RestController
  @RequestMapping("test")
  @Validated
  public class TestController {
    @PostMapping("users")
      public void createUser (@Validated @RequestBody User user) {
          System.out.println(user);
      }
  }
复制代码
  • 当参数校验不通过的时候会抛出org.springframework.web.bind.MethodArgumentNotValidException 异常,我们继续在 GlobalExceptionAdvice 中增加对这个异常的处理:
  @ControllerAdvice
  public class GlobalExceptionAdvice {
    	...
      @ExceptionHandler(MethodArgumentNotValidException.class)
      @ResponseStatus(code = HttpStatus.BAD_REQUEST)
      @ResponseBody
      public UnifyResponse handleBeanValidation (HttpServletRequest request, MethodArgumentNotValidException e) {
          List<ObjectError> errors = e.getBindingResult().getAllErrors();
          String message = errors.get(0).getDefaultMessage();
          return new UnifyResponse(40009, message);
      }
  }
复制代码

二、自定义校验注解

Hibernate Validator 提供的校验注解不能满足我们的参数校验需求,这时就需要我们自定义校验注解。还是举例上面的新增用户,需要增加一个字段 mobile ,我们自定义一个校验手机号码的注解。

  • 新建注解,通过 @Constraint 告诉验证器需要使用哪个验证器。
  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.FIELD, ElementType.PARAMETER})
  @Constraint(validatedBy = MobileNumberValidator.class)
  public @interface MobileNumber {
    	// 注解参数,校验失败提示信息
      String message() default "手机号码不正确";
    	// 注解参数,是否必填
      boolean require() default false;
  
      Class<?>[] groups() default {};
  
      Class<? extends Payload>[] payload() default {};
  }
复制代码
  • 注解验证器实现类 MobileNumberValidator;需要实现 ConstraintValidator 接口,泛型中第一项是校验注解,第二项是需要校验字段的数据类型;

    实现其中 initializeisValid 方法,在 initialize 中我们可以拿到注解,获取一些注解中参数。isValid 是实际对参数的校验方法,返回 true 则校验通过,反之校验失败。

  public class MobileNumberValidator implements ConstraintValidator<MobileNumber, String> {
    	private boolean require;
  
      @Override
      public void initialize(MobileNumber constraintAnnotation) {
          this.require = constraintAnnotation.require();
      }
    
    	@Override
      public boolean isValid(String mobile, ConstraintValidatorContext constraintValidatorContext) {
          if (this.require) {
              return !StringUtils.isBlank(mobile) && this.isMobileNumber(mobile);
          } else {
              return StringUtils.isBlank(mobile) || this.isMobileNumber(mobile);
          }
      }
  
      private boolean isMobileNumber (String mobile) {
          String regex = "^1[3-9]\\d{9}$";
          if (mobile.length() != 11) {
              return false;
          } else {
              Pattern p = Pattern.compile(regex);
              Matcher matcher = p.matcher(mobile);
              return matcher.matches();
          }
      }
  }
复制代码
  • 使用方式,在字段上增加 @MobileNumber 注解,如果必填项则传入 require = true,可以通过 message 参数定义参数校验失败提示信息。

    • bean
  @Data
  class User {
    @NotBlank(message = "用户名必填")
    @Length(min = 2, max = 20, message = "用户名2-50字")
    private String username;
    @NotBlank(message = "密码必填")
    @Length(min = 6, max = 20, message = "密码长度6-20")
    private String password;
    @MobileNumber(require = true, message = "手机号码校验失败")
    private String mobile;
  }
复制代码
  • controller
  @RestController
  @RequestMapping("test")
  @Validated
  public class TestController {
      @GetMapping("users")
      public void userList(
        @Min(value = 1, message = "页码不能小于1") @RequestParam(name = "page", defaultValue = "1") Integer page, 
        @MobileNumber @RequestParam(name = "mobile", defaultValue = "") String mobile) {
          System.out.println(page);
        	System.out.println(mobile);
      }
  }
复制代码