我使用 idea 开发工具的 Spring Initilizer 搭建了一个 grandle 的项目,在这个项目中针对实体类的 Entity 希望可以使用已经比较方便的 javax.validation.constraints 进行属性的校验,结果本来看似很容易的注解,搞了一下午才能够正常使用,令人有些崩溃,特把此次踩到的坑记录如下,方便以后查看。
实体对象级别验证
- 在实体类中对有校验需求的字段提价注解及配置错误提示信息
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注解即可级联验证 |
- 在 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 这两个注解的先后顺序的问题,我这里测试的是不存在的,谁在前后都可以正常校验。
- 然后这么简单的觉得就可以正常生效了吧?呵呵,并不是这样的,一直没有生效。直到搞了一下午,查很多资料,各种实验,直到看到一篇文章中说: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 (亲试可以)
- 增加全局异常捕获
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 对此也进行了扩展,使用方式如下:
- 增加配置 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();
}
}
- 在对应的 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 等验证注解