跳至主要內容

SpringBootValidataion

soulballad微服务SpringCloud NetfilxSpringCloud约 1337 字大约 4 分钟

Spring Boot Bean Validator

springboot 简单验证

Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

命名规则(Since Spring Boot 1.4):Spring Boot 大多数情况采用 starter(启动器,包含一些自动装配的Spring 组件),官方的命名规则:spring-boot-starter-{name},业界或者民间:{name}-spring-boot-starter

  1. 实体类 User

    public class User {
    
        @Max(value = 10000)
        private long id;
    
        @NotNull
        private String name;
        
        // 卡号 -- GUPAO-123456789
        private String cardNumber;
    
        // get&set
    }
    
  2. 控制器

    @RestController
    public class UserController {
    
        @PostMapping("/user/save")
        public User save(@Valid @RequestBody User user) {
            return user;
        }
    }
    
  3. postman 测试

    1. id<10000 && name==null,400

      1570091348759

    2. id > 10000 && name != null,400

      1570091479052

    3. id < 1000 && name != null,200

      1570091518963

常用验证技术

Spring Assert API

// Spring API 的方式
Assert.hasText(user.getName(), "name can not be null");

Java assert 断言

// JVM 断言
assert user.getId() <= 10000;

SpringMVC Interceptor

public class UserControllerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 校验逻辑,检验通过返回true,否则返回false
        return true;
    }
}

Filter

使用 doFilter 进行参数校验

以上方式的缺点,耦合了业务逻辑,虽然可以通过HandlerInterceptor 或者Filter做拦截,但是也是非常恶心的;

还可以通过 AOP 的方式,也可以提升代码的可读性。

以上方法都有一个问题,不是统一的标准。

自定义 Bean Validation

需求说明

通过员工的卡号来校验,需要通过工号的前缀和后缀来判断

  • 前缀必须是 "GUPAO-"
  • 后缀必须是数字

需要通过 Bean Validator 检验

实现步骤

  1. 复制成熟 Bean Validation Annotation的模式,自定义 @ValidCardNumber 注解

    1570093979476

    @Target(FIELD)
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {})
    public @interface ValidCardNumber {
    }
    
  2. 参考和理解 @Constraint

    是一个注解,里面有一个 validateBy 方法,接收一个 ConstraintValidator 实现类

    1570094010324

  3. 实现ConstraintValidator 接口

    1570094122324

    ConstraintValidator 接口泛型有两个参数,第一个是注解类型,第二个是要校验的参数类型

    public class ValidCardNumberConstraintValidator 
        implements ConstraintValidator<ValidCardNumber, String> {
        @Override
        public void initialize(ValidCardNumber constraintAnnotation) {
    
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
    
            // 前半部分和后半部分
            String[] parts = StringUtils.split(value, "-");
            // 为什么不用 String#split 方法,原因在于该方法使用了正则表达式
            // 其次是 NPE 保护不够
            // 如果在依赖中,没有 StringUtils.delimitedListToStringArray API 的话呢,可以使用
            // Apache commons-lang StringUtils
            // JDK 里面 StringTokenizer(不足类似于枚举 Enumeration API)
    
            if (ArrayUtils.getLength(parts) != 2) {
                return false;
            }
    
            String prefix = parts[0];
            String suffix = parts[1];
    
            boolean isValidPrefix = Objects.equals(prefix, "GUPAO");
            boolean isValidNumber = StringUtils.isNumeric(suffix);
    
            return isValidPrefix && isValidNumber;
        }
    }
    
  4. 将实现ConstraintValidator 接口 定义到@Constraint#validatedBy

    @Target({FIELD})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class})
    public @interface ValidCardNumber {
       
    }
    
  5. 使用 postman 测试

    1570094239309

    提示 constraing does not contain a message partner,所以需要添加

  6. @ValidCardNumber 添加 message 参数

    可以直接添加在注解中给出默认值,也可以在使用时指定

    @Target({FIELD})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class})
    public @interface ValidCardNumber {
        String message() default "{卡号必须以 \"GUPAO\" 开头,以数字结尾}";
    }
    

    然后再次测试,提示 constraing does not contain a groupts partner,所以需要添加

    1570094403758

  7. @ValidCardNumber 添加 groups、payload参数

    都添加后,如下所示

    @Target({FIELD})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class})
    public @interface ValidCardNumber {
        String message() default "{卡号必须以 \"GUPAO\" 开头,以数字结尾}";
    
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    }
    
  8. 使用 postman 再次测试

    • cardNumber=GUPAO,输入无效,校验不通过

      1570094499635

    • cardNumber=GUPAO-1234,校验通过

      1570096110290

    发现自定义校验规则生效了

  9. 国际化配置

    @NotNull 的 message() 中有这样一句描述 ”javax.validation.constraints.NotNull.message“

    1570095528639

    全局搜索后发现,在 hibernate-validator\6.0.17.Final jar包下面有个ValidationMessages_zh_CN.properties 配置文件,所以需要同样配置

    1570095594516

    配置步骤:

    1. 修改 @ValidCardNumber 中 message() 描述内容

      @Target({FIELD})
      @Retention(RUNTIME)
      @Documented
      @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class})
      public @interface ValidCardNumber {
          String message() default "{com.gupao.bean.validation.invalid.card.number.message}";
      
          Class<?>[] groups() default { };
      
          Class<? extends Payload>[] payload() default { };
      }
      
    2. src/main/resources 目录下,新增 ValidationMessages_zh_CN.propertiesValidationMessages.properties 配置文件

      1570095836600

      文件内容:

      • ValidationMessages

        com.gupao.bean.validation.invalid.card.number.message=the card number must start with "GUPAO-" and end with a series of numbers.
        
      • ValidationMessages

        # 卡号必须以 "GUPAO-" 开头,以数字结尾
        com.gupao.bean.validation.invalid.card.number.message=\u5361\u53F7\u5FC5\u987B\u4EE5 "GUPAO-" \u5F00\u5934\uFF0C\u4EE5\u6570\u5B57\u7ED3\u5C3E
        
    3. 使用 postman 测试

      1570095983526

      1570096938372

源码分析

org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree#validateSingleConstraint

1570096409256

问答部分

  1. JSON校验如何办?

    答:尝试变成 Bean 的方式

  2. 实际中 很多参数都要校验 那时候怎么写 这样写会增加很多类

    答:确实会增加部分工作量,大多数场景,不需要自定义,除非很特殊情况。

    Bean Validation 的主要缺点,单元测试不方便

  3. 如果前端固定表单的话,这种校验方式很好。但是灵活性不够,如果表单是动态的话,如何校验呢?

    答: 表单字段与 Form 对象绑定即可,再走 Bean Validation 逻辑

    <form action="" method="POST" command="form">
        <input value="${form.name}" />
        ...
        <input value="${form.age}" />
    
    </form>
    

    一个接一个验证,责任链模式(Pipeline):

    field 1-> field 2 -> field 3 -> compute -> result

  4. 如何自定义,返回格式?如何最佳实现?

    答:可以通过REST来实现,比如 XML 或者 JSON 的格式(视图)

  5. 面试的看法

    答:具备一定的水平

    不该问的不要问,因为面试官的水平可能还不及于你!

上次编辑于:
贡献者: soulballad