欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

SpringBoot如何使用validator框架优雅地校验参数,

来源: javaer 分享于  点击 17938 次 点评:46

SpringBoot如何使用validator框架优雅地校验参数,


目录
  • 1、为什么要校验参数?
  • 2、引入依赖
  • 3、@requestBody 参数校验
  • 4、@requestParam、@PathVariable 参数校验
  • 5、统一异常处理
  • 6、分组校验
    • 6.1、定义分组接口
    • 6.2、给需要校验的字段分配分组
    • 6.3、给需要校验的参数指定分组
    • 6.4、默认分组
  • 7、嵌套校验
    • 8、自定义校验
      • 8.1、案例一、自定义校验 加密id
      • 8.2、案例二、自定义校验 性别只允许两个值
    • 9、实现校验业务规则
      • 9.1、自定义约束注解
      • 9.2、编写约束校验器
      • 9.3、测试
    • 10、@Valid 和 @Validated 的区别
      • 11、常用注解
        • 总结

          1、为什么要校验参数?

          在日常的开发中,为了防止非法参数对业务造成影响,需要对接口的参数进行校验,以便正确性地入库。

          例如:登录时,就需要判断用户名、密码等信息是否为空。虽然前端也有校验,但为了接口的安全性,后端接口还是有必要进行参数校验的。

          同时,为了校验参数更加优雅,这里就介绍了 Spring Validation 方式。

          • Java API 规范(JSR303:JAVA EE 6 中的一项子规范,叫做 Bean Validation)定义了 Bean 校验的标准 validation-api,但没有提供实现。
          • hibernate validation 是对这个规范的实现,并增加了校验注解。如:@Email、@Length。

          JSR 官网

          Hibernate Validator 官网

          Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

          2、引入依赖

          如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于等于 2.3.x,则需要手动引入依赖。

          <dependency>
              <groupId>org.hibernate.validator</groupId>
              <artifactId>hibernate-validator</artifactId>
              <version>8.0.0.Final</version>
          </dependency>

          对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

          • POST、PUT 请求,使用 @requestBody 接收参数
          • GET 请求,使用 @requestParam、@PathVariable 接收参数

          3、@requestBody 参数校验

          对于 POST、PUT 请求,后端一般会使用 @requestBody + 对象 接收参数。此时,只需要给对象添加 @Validated 或 @Valid 注解,即可轻松实现自动校验参数。如果校验失败,会抛出 MethodArgumentNotValidException 异常。

          UserVo :添加校验注解

          @Data
          public class UserVo {
          
          	private Long id;
          
              @NotNull
              @Length(min = 2, max = 10)
              private String userName;
          
              @NotNull
              @Length(min = 6, max = 20)
              private String account;
          
              @NotNull
              @Length(min = 6, max = 20)
              private String password;
          }

          UserController :

          @RestController
          @RequestMapping("/user")
          public class UserController {
          
              @PostMapping("/addUser")
              public String addUser(@RequestBody @Valid UserVo userVo) {
                  return "addUser";
              }
          }

          或者使用 @Validated 注解:

          @PostMapping("/addUser")
          public String addUser(@RequestBody @Validated UserVo userVo) {
              return "addUser";
          }

          4、@requestParam、@PathVariable 参数校验

          GET 请求一般会使用 @requestParam、@PathVariable 注解接收参数。如果参数比较多(比如超过 5 个),还是推荐使用对象接收。否则,推荐将一个个参数平铺到方法入参中。

          在这种情况下,必须在 Controller 类上标注 @Validated 注解,并在入参上声明约束注解(如:@Min )。如果校验失败,会抛出 ConstraintViolationException 异常

          @RestController
          @RequestMapping("/user")
          @Validated
          public class UserController {
          
              @GetMapping("/getUser")
              public String getUser(@Min(1L) Long id) {
                  return "getUser";
              }
          }

          5、统一异常处理

          如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示

          @RestControllerAdvice
          public class ExceptionControllerAdvice {
          
              @ExceptionHandler({MethodArgumentNotValidException.class})
              @ResponseStatus(HttpStatus.OK)
              public String handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
                  BindingResult bindingResult = ex.getBindingResult();
                  StringBuilder sb = new StringBuilder("校验失败:");
                  for (FieldError fieldError : bindingResult.getFieldErrors()) {
                      sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
                  }
                  String msg = sb.toString();
                  return "参数校验失败" + msg;
              }
          
              @ExceptionHandler({ConstraintViolationException.class})
              public String handleConstraintViolationException(ConstraintViolationException ex) {
                  return "参数校验失败" + ex;
              }
          
          }

          6、分组校验

          在实际项目中,可能多个方法需要使用同一个类对象来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。

          如:保存 User 的时候,userId 是可空的,但是更新 User 的时候,userId 的值必须 >= 1L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:

          约束注解上声明适用的分组信息 groups

          6.1、定义分组接口

          public interface ValidGroup extends Default {
          	// 添加操作
              interface Save extends ValidGroup {}
              // 更新操作
              interface Update extends ValidGroup {}
          	// ...
          }

          为什么要继承 Default ?下文有。

          6.2、给需要校验的字段分配分组

          @Data
          public class UserVo {
          	
              @Null(groups = ValidGroup.Save.class, message = "id要为空")
              @NotNull(groups = ValidGroup.Update.class, message = "id不能为空")
              private Long id;
          
              @NotBlank(groups = ValidGroup.Save.class, message = "用户名不能为空")
              @Length(min = 2, max = 10)
              private String userName;
          
              @Email
              @NotNull
              private String email;
          }

          根据校验字段看:

          • id:分配分组:Save、Update。添加时,一定为 null;更新时,一定不为 null
          • userName:分配分组:Save。添加时,一定不能为空
          • email:分配分组:无。即:使用默认的分组

          6.3、给需要校验的参数指定分组

          @RestController
          @RequestMapping("/user")
          public class UserController {
          
              @PostMapping("/addUser")
              public String addUser(@RequestBody @Validated(ValidGroup.Save.class) UserVo userVo) {
                  return "addUser";
              }
          
              @PostMapping("/updateUser")
              public String updateUser(@RequestBody @Validated(ValidGroup.Update.class) UserVo userVo) {
                  return "updateUser";
              }
          }

          测试校验。

          6.4、默认分组

          如果 ValidGroup 接口 不继承 Default 接口,那么,将无法校验 email 字段(未分配分组);

          继承后,ValidGroup 就属于 Default 类型,即:默认分组/所以,可以对 email 校验

          7、嵌套校验

          必须要用 @Valid 注解

          @Data
          public class UserVo {
          
          	@NotNull(groups = {ValidGroup.Save.class, ValidGroup.Update.class})
              @Valid
              private Address address;
          }

          8、自定义校验

          8.1、案例一、自定义校验 加密id

          假设我们自定义加密 id(由数字或者 a-f 的字母组成,32-256 长度)校验,主要分为两步:

          8.1.1、自定义约束注解

          @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
          @Retention(RUNTIME)
          @Documented
          @Constraint(validatedBy = {EncryptIdValidator.class}) // 自定义验证器
          public @interface EncryptId {
          
              // 默认错误消息
              String message() default "加密id格式错误";
              // 分组
              Class<?>[] groups() default {};
              // 负载
              Class<? extends Payload>[] payload() default {};
          }

          8.1.2、编写约束校验器

          public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
          
              private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");
          
              @Override
              public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
                  if (value != null) {
                      Matcher matcher = PATTERN.matcher(value);
                      return matcher.find();
                  }
                  return true;
              }
          }

          8.1.3、使用

          @Data
          public class UserVo {
          
              @EncryptId
              private String id;
          }

          8.2、案例二、自定义校验 性别只允许两个值

          UserVo 类中的 sex 性别属性,只允许前端传递传 M,F 这2个枚举值,如何实现呢?

          8.2.1、自定义约束注解

          @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
          @Retention(RUNTIME)
          @Documented
          @Constraint(validatedBy = {SexValidator.class})
          public @interface SexValid {
          
          
              // 默认错误消息
              String message() default "value not in enum values";
          
              // 分组
              Class<?>[] groups() default {};
          
              // 负载
              Class<? extends Payload>[] payload() default {};
          
              String[] value();
          }

          8.2.2、编写约束校验器

          public class SexValidator implements ConstraintValidator<SexValid, String> {
          
              private List<String> sexs;
          
              @Override
              public void initialize(SexValid constraintAnnotation) {
                  sexs = Arrays.asList(constraintAnnotation.value());
              }
          
              @Override
              public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
                  if (StringUtils.isEmpty(value)) {
                      return true;
                  }
                  return sexs.contains(value);
              }
          
          }

          8.2.3、使用

          @Data
          public class UserVo {
          
              @SexValid(value = {"F", "M"}, message = "性别只允许为F或M")
              private String sex;
          
          }
          ```### 8.2.4、测试
          ```java
          @GetMapping("/get")
          private String get(@RequestBody @Validated UserVo userVo) {
              return "get";
          }

          9、实现校验业务规则

          业务规则校验 指 接口需要满足某些特定的业务规则。举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。 这就要求在创建用户时需要校验用户名称、手机号码、邮箱是否被注册;编辑用户时不能将信息修改成已有用户的属性。

          最优雅的实现方法应该是参考 Bean Validation 的标准方式,借助自定义校验注解完成业务规则校验。

          9.1、自定义约束注解

          首先我们需要创建两个自定义注解,用于业务规则校验:

          • UniqueUser:表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱
          • NotConflictUser:表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合
          @Documented
          @Retention(RUNTIME)
          @Target({FIELD, METHOD, PARAMETER, TYPE})
          @Constraint(validatedBy = UserValidator.UniqueUserValidator.class)
          public @interface UniqueUser {
          
              String message() default "用户名、手机号码、邮箱不允许与现存用户重复";
              Class<?>[] groups() default {};
              Class<? extends Payload>[] payload() default {};
          
          }
          @Documented
          @Retention(RUNTIME)
          @Target({FIELD, METHOD, PARAMETER, TYPE})
          @Constraint(validatedBy = UserValidator.NotConflictUserValidator.class)
          public @interface NotConflictUser {
          
              String message() default "用户名称、邮箱、手机号码与现存用户产生重复";
              Class<?>[] groups() default {};
              Class<? extends Payload>[] payload() default {};
          
          }

          9.2、编写约束校验器

          想让自定义验证注解生效,需要实现 ConstraintValidator 接口。接口的第一个参数是 自定义注解类型,第二个参数是 被注解字段的类,因为需要校验多个参数,我们直接传入用户对象。 需要提到的一点是 ConstraintValidator 接口的实现类无需添加 @Component 它在启动的时候就已经被加载到容器中了。

          public class UserValidator<T extends Annotation> implements ConstraintValidator<T, UserVo> {
          
              protected Predicate<UserVo> predicate = c -> true;
          
              @Override
              public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
                  return predicate.test(userVo);
              }
          
              public static class UniqueUserValidator extends UserValidator<UniqueUser>{
                  @Override
                  public void initialize(UniqueUser uniqueUser) {
                      UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
                      predicate = c -> !userDao.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
                  }
              }
          
              public static class NotConflictUserValidator extends UserValidator<NotConflictUser>{
                  @Override
                  public void initialize(NotConflictUser notConflictUser) {
                      predicate = c -> {
                          UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
                          Collection<UserVo> collection = userDao.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
                          // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突
                          return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
                      };
                  }
              }
          
          }
          @Component
          public class ApplicationContextHolder implements ApplicationContextAware {
          
              private static ApplicationContext context;
          
              @Override
              public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
                  context = applicationContext;
              }
          
              public static ApplicationContext getContext() {
                  return context;
              }
          
              public static Object getBean(String name) {
                  return context != null ? context.getBean(name) : null;
              }
          
              public static <T> T getBean(Class<T> clz) {
                  return context != null ? context.getBean(clz) : null;
              }
          
              public static <T> T getBean(String name, Class<T> clz) {
                  return context != null ? context.getBean(name, clz) : null;
              }
          
              public static void addApplicationListenerBean(String listenerBeanName) {
                  if (context != null) {
                      ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
                      applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
                  }
              }
          
          }

          9.3、测试

          @RestController
          @RequestMapping("/user")
          public class UserController {
          
              @PostMapping("/addUser")
              public String addUser(@RequestBody @UniqueUser UserVo userVo) {
                  return "addUser";
              }
          
              @PostMapping("/updateUser")
              public String updateUser(@RequestBody @NotConflictUser UserVo userVo) {
                  return "updateUser";
              }
          }

          10、@Valid 和 @Validated 的区别

          区别如下:

          11、常用注解

          Bean Validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:

          注解详细信息
          @Null任意类型。被注释的元素必须为 null
          @NotNull任意类型。被注释的元素不为 null
          @Min(value)数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
          @Max(value)数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
          @DecimalMin(value)数值类型(double、float 会有精度丢失)。其值必须大于等于指定的最小值
          @DecimalMax(value)数值类型(double、float 会有精度丢失)。其值必须小于等于指定的最大值
          @Size(max, min)字符串、集合、Map、数组类型。被注释的元素的大小(长度)必须在指定的范围内
          @Digits (integer, fraction)数值类型、数值型字符串类型。其值必须在可接受的范围内。 integer:整数精度;fraction:小数精度
          @Past日期类型。被注释的元素必须是一个过去的日期
          @Future日期类型。被注释的元素必须是一个将来的日期
          @Pattern(value)字符串类型。被注释的元素必须符合指定的正则表达式

          Hibernate Validator 在原有的基础上也内嵌了几个注解,如下:

          注解详细信息
          @Email字符串类型。被注释的元素必须是电子邮箱地址
          @Length字符串类型。被注释的字符串的长度必须在指定的范围内
          @NotEmpty字符串、集合、Map、数组类型。 被注释的元素的长度必须非空
          @Range数值类型、字符串类型。 被注释的元素必须在合适的范围内

          总结

          以上为个人经验,希望能给大家一个参考,也希望大家多多支持3672js教程。

          您可能感兴趣的文章:
          • SpringSecurity安全框架的使用
          • 在Spring框架下配置Quartz集群的详细步骤(MySQL数据源)
          • Spring boot框架下的RabbitMQ消息中间件详解
          • SpringBoot同时集成Mybatis和Mybatis-plus框架
          • 深入解析Spring AI框架如何在Java应用中实现智能化交互的关键
          • Spring框架概述及核心设计思想分享
          相关栏目:

          用户点评