Java -- javax.validation.constraints - @NotNull 注解验证踩坑记录

5,674 阅读6分钟

我使用 idea 开发工具的 Spring Initilizer 搭建了一个 grandle 的项目,在这个项目中针对实体类的 Entity 希望可以使用已经比较方便的 javax.validation.constraints 进行属性的校验,结果本来看似很容易的注解,搞了一下午才能够正常使用,令人有些崩溃,特把此次踩到的坑记录如下,方便以后查看。

实体对象级别验证

  1. 在实体类中对有校验需求的字段提价注解及配置错误提示信息
package com.cockleshell.entity;

import com.cockleshell.common.MongoCommonObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.core.mapping.Document;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Document("staff")
public class Staff extends MongoCommonObject implements Serializable {
    @NotBlank(message = "appId 不能为空或空串")
    private String appId;
    @NotBlank(message = "userCode 不能为空或空串")
    private String userCode;
    @NotBlank(message = "userName 不能为空或空串")
    private String userName;
    @NotBlank(message = "roleId 不能为空或空串")
    private String roleId;

}

验证的对象中如果包含新的对象:则使用 @Valid 放在该对象属性上,然后再该属性对应的类上添加校验注解。

package com.cockleshell.entity;

import com.cockleshell.common.MongoCommonObject;
import lombok.*;
import org.springframework.data.mongodb.core.mapping.Document;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Document("route")
public class Route extends MongoCommonObject implements Serializable {
    public static final String ROOT = "0";
    @NotBlank(message = "appId 不能为空或空串")
    private String appId;
    @NotBlank(message = "name 不能为空或空串")
    private String name;
    @NotBlank(message = "path 不能为空或空串")
    private String path;
    @Valid
    private Meta meta;
    private String parentId = "0"; // 0 标识该菜单为根菜单

    @Data
    @NoArgsConstructor
    class Meta {
        private boolean auth = false;
        @NotBlank(message = "title 不能为空或空串")
        private String title;
        private boolean hide = false;
        private String icon;
    }
}

常用注解说明

验证注解 验证的数据类型 说明
@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注解即可级联验证
  1. 在 controller 中添加 @Valid 注解
package com.cockleshell.controller;

import com.cockleshell.common.ApiResponse;
import com.cockleshell.common.PageHelper;
import com.cockleshell.entity.Staff;
import com.cockleshell.service.StaffService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@RestController
@RequestMapping("/v1/staff")
public class StaffController {
    @Autowired
    private StaffService staffService;

    @PostMapping
    public ApiResponse saveOrUpdate (@RequestBody @Valid Staff staff) {
        Staff insert = staffService.saveOrUpdate(staff);
        return ApiResponse.ok(insert);
    }
}

说明:在百度中查到的一些内容中有强调 @RequestBody @Valid 这两个注解的先后顺序的问题,我这里测试的是不存在的,谁在前后都可以正常校验。

  1. 然后这么简单的觉得就可以正常生效了吧?呵呵,并不是这样的,一直没有生效。直到搞了一下午,查很多资料,各种实验,直到看到一篇文章中说:validation-api 即 javax.validation.constraints 中的注解需要依赖hibernate-validator 才能有效,这才感觉到有希望了,于是手动引入了 hibernate-validator 后,这个注解才真正的开始有效了。@@
plugins {
    id 'org.springframework.boot' version '2.3.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.cockleshell'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    compile 'org.springframework.boot:spring-boot-devtools'
    compile group: 'org.hibernate', name: 'hibernate-validator', version: '6.0.16.Final'
    compile group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.3.2'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}

然而,在创建 maven 项目的时候,并不需要单独引入 hibernate-validator 的依赖,在 org.springframework.boot:spring-boot-starter-web 这个 web 的 starter 下,会将两种包都引入,然而在 grandle 的项目的 web 的 starter 竟然没有引入 hibernate-validator。这真是有点费解,我以为 maven 和 grandle 只是两种不同的依赖的管理方式,依赖的树应该都是一样的,然后事实并非如此。。。

由于不是这种 start 自动引入的 hibernate-validator 的情况,有引起了后面一个问题,虽然注解生效了,但是却抛出了不是我预期的异常信息:

"HV000030: No validator could be found for constraint 'javax.validation.constraints.NotBlank' validating type 'java.lang.String'. Check configuration for 'roleId'"

这个异常明显不对,这是找不到匹配的 validator 了。 再查资料发现是 validation-api 和 hibernate-validator 两个会有一个版本匹配的问题: 下面记录两个匹配的版本:

(1)validation-api 1.1.0.Final + hibernate-validator 5.3.6.Final (未实验) (2)validation-api 2.0.1.Final + hibernate-validator 6.0.16.Final (亲试可以)

  1. 增加全局异常捕获
package com.cockleshell;

import com.cockleshell.common.ApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 方法参数校验
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        logger.error(ex.getMessage());
        return ApiResponse.fail(ex.getBindingResult().getFieldError().getDefaultMessage());
    }


    @ExceptionHandler(Exception.class)
    @ResponseBody
    public final ApiResponse handleAllExceptions(Exception ex) {
        logger.error(ex.getMessage());
        return ApiResponse.fail(ex.getLocalizedMessage());
    }
}


好了,经过上面的踩坑,终于可以正常的使用注解验证了。

方法级别验证

在能够对传入的实体进行验证之后,希望能够对通过 @RequestParam 或者是 @PathVariable 传入的参数通过注解校验,那就很方便了。spring 对此也进行了扩展,使用方式如下:

  1. 增加配置 MethodValidationPostProcessor 参数校验处理器
package com.cockleshell.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;

@Configuration
@EnableAutoConfiguration
public class BaseConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

  1. 在对应的 controller 或者 service 或者其他想要校验方法参数的类增加 @Validated
package com.cockleshell.controller;

import com.cockleshell.common.ApiResponse;
import com.cockleshell.common.PageHelper;
import com.cockleshell.entity.Route;
import com.cockleshell.service.RouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

@RestController
@RequestMapping("/v1/route")
@Validated
public class RouteController {

    @Autowired
    private RouteService routeService;

    @GetMapping("/{appId}")
    public ApiResponse queryByAppId (@PathVariable @NotBlank(message = "appId 不能为空串")String appId) {
        return ApiResponse.ok(routeService.queryByAppId(appId));
    }

}

3.在需要验证的方法的参数增加验证注解 @NotNull @NotBlank 等验证注解