一、Bean Validation 和 Hibernate Validation 前者是数据校验的一套规范 ,是接口 ,没有实现 。
而后者是此规范的参考实现 ,并对前者进行了扩展。
二、传统方式的参数校验 2.1、创建一个Java Bean 定义各属性的校验规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Data public class Person { private Long id; private String name; private Integer age; private String email; private String phone; private LocalDateTime birthday; }
2.2、创建一个测试类,使用传统方式进行校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static void valid (Person person) { String name = person.getName(); if (name == null || "" .equals(name) || "" .equals(name.trim())) { throw new RuntimeException("name不符合校验规则!" ); } Integer age = person.getAge(); if (age < 1 || age > 800 ) { throw new RuntimeException("年龄属性不符合校验规则!" ); } String email = person.getEmail(); String emailReg = "^[a-z0-9A-Z]+[- | a-z0-9A-Z . _]+@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\\\.)+[a-z]{2,}$" ; if (!email.matches(emailReg)) { throw new RuntimeException("邮箱不符合格式!" ); } ... }
2.3、弊端 代码复用率低,代码的可维护性会随着校验规则的增多而降低,且这种参数校验方法看起来有点笨拙。 对于一些复杂的校验规则,难以实现。 三、常用的校验约束注解 3.1、Bean Validation 中内置的约束 1、@Null
被注释的元素必须为 null
2、@NotNull
被注解的元素必须不为 null
3、@NotEmpty
被注释的 集合 (size > 0)
被注释的字符串 不为空且 不等于空串
4、@AssertTrue
被注释的元素必须为true
5、@AssertFalse
被注释的元素必须为false
6、@Min(value)
被注释的元素必须为一个数字,且值必须大于等于value
7、@Max(value)
被注释的元素必须为一个数字,且值必须小于等于value
8、@DecimalMin(value)
被注释的元素必须是一个数字,其值必须大于等于指定的最小值
9、@DecimalMax(value)
被注释的元素必须是一个数字,其值必须小于等于指定的最大值
10、@Size(max,min)
被注释的元素的大小必须在指定的范围内
11、@Digits(integer,fraction)
被注释的元素的大小必须是一个数字,其值必须在可接受的范围内
12、@Past
被注释的元素必须是一个过去的日期
13、@Future
被注释的元素必须是一个将来的日期
14、@Pattern(value)
被注释的元素必须符合指定的正则表达式
15、@Email
被注释的元素必须为电子邮箱地址
3.2、Hibernate Validator 附加的约束 1、@Length 被注释的字符串大小必须在指定的范围内
2、@Range 被注释的元素必须在合适的范围内
3、@URL 被注释的元素必须为一个 url
四、Spring Boot中配合统一异常处理类进行参数校验 4.1、引入相关依赖 在 Spring Boot 2.2.1.RELEASE 版本中,spring-boot-starter-web 中包含了 spring-boot-starter-validation 的相关依赖,所以无需额外引入
4.2、创建 Bean 在Bean属性上添加参数校验规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Data public class UserDTO { @NotBlank(message = "用户名不能为空!") private String userName; @NotBlank(message = "手机号不能为空!") @Pattern(regexp = "^((13[0-9])|(14[5-9])|(15([0-3]|[5-9]))|(16[6-7])|(17[1-8])|(18[0-9])|(19[1|3])|(19[5|6])|(19[8|9]))\\d{8}$",message = "手机号不合格式") private String mobile; @Email(message = "邮箱不合法") @NotBlank(message = "邮箱不能为空!") private String email; @Min(value = 1,message = "年龄必须在1-800之间") @Max(value = 800,message = "年龄必须在1-800之间") private Integer age; }
4.3、创建统一返回结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Data @ApiModel(value = "全局统一返回结果") public class R { @ApiModelProperty(value = "是否成功") private Boolean success; @ApiModelProperty(value = "返回码") private Integer code; @ApiModelProperty(value = "返回消息") private String message; @ApiModelProperty(value = "返回数据") private Map<String, Object> data = new HashMap<String, Object>(); public R () {} public static R ok () { R r = new R(); r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess()); r.setCode(ResultCodeEnum.SUCCESS.getCode()); r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); return r; } public static R error () { R r = new R(); r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess()); r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode()); r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage()); return r; } public static R setResult (ResultCodeEnum resultCodeEnum) { R r = new R(); r.setSuccess(resultCodeEnum.getSuccess()); r.setCode(resultCodeEnum.getCode()); r.setMessage(resultCodeEnum.getMessage()); return r; } public R success (Boolean success) { this .setSuccess(success); return this ; } public R message (String message) { this .setMessage(message); return this ; } public R code (Integer code) { this .setCode(code); return this ; } public R data (String key, Object value) { this .data.put(key, value); return this ; } public R data (Map<String, Object> map) { this .setData(map); return this ; } }
4.4、创建统一异常处理类,对参数校验结果进行封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public R handleValidationExceptions (MethodArgumentNotValidException methodArgumentNotValidException) { Map<String, Object> errors = new HashMap<>(); methodArgumentNotValidException.getBindingResult().getAllErrors().stream().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); return R.error().data(errors); } }
4.5、创建Controller 在参数列表的对象前使用@Validated注解,开启校验
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/userDto") public class UserDTOController { @PostMapping("/add") public R addUser (@RequestBody @Validated UserDTO userDTO) { System.out.println(userDTO); return R.ok().data("user" ,userDTO); } }
使用Swagger进行测试
点击 execute ,查看结果
五、校验组 校验组可以对需要校验的字段进行分类,即为每个字段提供不同的校验规则。校验组只需定义简单的接口即可。
比如说,在更新UserDTO
时,我们可以让传入的UserDTO
对象的某些属性为空,但ID不为空,此时我们可以使用教研组进行区分
5.1、为UserDTO
添加ID属性,并创建校验组 1、创建两个校验组 1 2 public interface SaveGroup {}
1 2 public interface UpdateGroup {}
2、为UserDTO
添加ID属性 1 2 @NotBlank(message = "传入id不能为空!",groups = UpdateGroup.class) private Long id;
3、为其他属性的校验规则添加校验组 这里需要注意,对于格式的校验(手机号、邮箱)可以不添加校验组,因为无论是添加操作还是更新操作都要求传入的参数必须符合格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data public class UserDTO { @NotNull(message = "传入id不能为空!",groups = UpdateGroup.class) private Long id; @NotBlank(message = "用户名不能为空!",groups = SaveGroup.class) private String userName; @NotBlank(message = "手机号不能为空!",groups = SaveGroup.class) @Pattern(regexp = "^((13[0-9])|(14[5-9])|(15([0-3]|[5-9]))|(16[6-7])|(17[1-8])|(18[0-9])|(19[1|3])|(19[5|6])|(19[8|9]))\\d{8}$",message = "手机号不合格式") private String mobile; @Email(message = "邮箱不合法") @NotBlank(message = "邮箱不能为空!",groups = SaveGroup.class) private String email; @Min(value = 1,message = "年龄必须在1-800之间",groups = SaveGroup.class) @Max(value = 800,message = "年龄必须在1-800之间",groups = SaveGroup.class) private Integer age; }
5.2、在Controller方法体的@Validated注解中指定校验组 1、在添加方法中指定校验组为 SaveGroup
1 2 3 4 5 @PostMapping("/add") public R addUser (@RequestBody @Validated(value = SaveGroup.class) UserDTO userDTO) { System.out.println(userDTO); return R.ok().data("user" ,userDTO); }
打开 Swagger 进行测试
查看结果,发现只对 SaveGroup
校验组的属性进行校验,由于 id 属性不属于 SaveGroup
,所以不会对id属性进行校验
2、新增一个Update方法,指定校验组为 UpdateGroup
1 2 3 4 5 @PutMapping("/edit") public R editUser (@RequestBody @Validated(value = UpdateGroup.class) UserDTO userDTO) { System.out.println(userDTO); return R.ok().data("user" ,userDTO); }
打开Swagger,进行测试
查看结果,可以看到当前方法只对 UpdateGroup
组的属性进行了校验
六、对传入的简单参数进行校验 6.1、编写一个简单的查询方法(根据姓名查询) 对传入的姓名进行校验
1 2 3 4 5 6 7 @GetMapping("/getByName/{name}") public R getByName (@PathVariable("name") @NotBlank(message = "传入的姓名不能为空!") String name) { System.out.println("传入的参数为:" + name); UserDTO userDTO = new UserDTO(); userDTO.setUserName(name); return R.ok().data("user" ,userDTO); }
6.2、在Controller上添加@Validated注解 对传入的简单参数进行校验时,需要将@Validated注解添加在控制器上。
1 2 3 4 5 6 @Validated @RestController @RequestMapping("/userDto") public class UserDTOController { }
6.3、测试 打开Swagger进行测试
这个时候抛出的异常为 ConstraintViolationException
异常
6.4、对 ConstraintViolationException
进行处理 由于上面抛出的异常是我们没有进行捕获的异常,所以我们需要在统一异常处理类中对参数校验结果进行封装
1 2 3 4 5 6 @ExceptionHandler(ConstraintViolationException.class) public R handleConstraintViolationException (ConstraintViolationException exs) { Map<String, Object> errors = new HashMap<>(); exs.getConstraintViolations().forEach(err -> errors.put(err.getPropertyPath().toString(), err.getMessage())); return R.error().data(errors); }
6.5、再次进行测试 使用swagger进行测试
查看结果
七、嵌套校验 当一个bean中存在另外一个Bean属性时,我们可以使用嵌套校验,只需要在bean属性上添加 @Valid 注解即可
7.1、创建一个 Card 实体类 并添加参数校验规则
1 2 3 4 5 6 7 8 @Data public class Card { @NotNull(message = "卡号不能为空!") private Long cardId; @NotBlank(message = "手机号不能为空") private String phone; }
7.2、为UserDTO
创建一个Card属性 并在该属性上添加@Valid注解,进行嵌套校验
1 2 3 @NotNull(message = "卡信息不能为空!") @Valid private Card card;
7.3、测试 这里需要先去掉 添加 方法中的校验组限制
查看结果
八、自定义校验注解 创建一个用于校验手机号是否合法的注解
8.1、创建一个注解 @Phone 这个注解只能加在字段上
其中 @Constraint(validatedBy = PhoneValidator.class)
表明这个注解修饰的属性将被哪个类完成校验
1 2 3 4 5 6 7 8 9 10 11 12 @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneValidator.class) public @interface Phone { String message () default "传入的手机号码格式有误" ; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
8.2、创建 PhoneValidator
类 这个类用于校验 @Phone 注解修饰的属性,需要实现 ConstraintValidator<K,V>
接口
其中泛型 K 表示这个类需要校验的注解,而泛型 V 表示注解修饰属性的类(如果要校验的属性是String类型的手机号,就填入String)
重写 isValid
方法,这个方法用于校验并返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class PhoneValidator implements ConstraintValidator <Phone ,String > { public static final String PHONE_REGEX = "^((13[0-9])|(14[5-9])|(15([0-3]|[5-9]))|(16[6-7])|(17[1-8])|(18[0-9])|(19[1|3])|(19[5|6])|(19[8|9]))\\d{8}$" ; @Override public boolean isValid (String phone, ConstraintValidatorContext constraintValidatorContext) { if (phone == null || "" .equals(phone) || "" .equals(phone.trim())) { return false ; } return Pattern.matches(phone,PHONE_REGEX); } }
8.3、测试自定义的校验注解 1、为UserDTO
创建一个phone属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Data public class UserDTO { @NotNull(message = "传入id不能为空!") private Long id; @NotBlank(message = "用户名不能为空!") private String userName; @Email(message = "邮箱不合法") @NotBlank(message = "邮箱不能为空!") private String email; @Min(value = 1,message = "年龄必须在1-800之间") @Max(value = 800,message = "年龄必须在1-800之间") private Integer age; @Phone(message = "传入的手机号不合法!") private String phone; }
8.2、使用Swagger进行测试 当传入的 phone
不合法时
结果
传入的 phone
为null时
结果
九、@Valid 和 @Validated的区别 9.1、使用位置 @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上 @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上 9.2、是否支持分组校验 使用注解的value属性指定校验组,校验组可以有多个
1 2 3 4 5 6 @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Validated { Class<?>[] value() default {}; }
在controller层的方法的要校验的参数上添加@Valid注解,并且需要传入 BindingResult
对象,用于获取校验失败情况下的反馈信息
9.3、@Validated支持多个模型对象的校验 在方法参数列表中,如果有多个Bean对象需要校验,那么可以在每个Bean对象前添加@Validated注解
9.4、嵌套校验 如果一个Bean中含有另外一个Bean属性,那么可以在内部Bean属性上添加@Valid注解来开启嵌套查询。
而@Validated不能添加在属性上,所以不能开启嵌套校验
9.5、校验方法入参 在使用@NotNull
验证方法入参时,需要在控制器类上添加@Validated注解开启校验
十、谷粒商城使用 validation 进行后端校验 10.1、在 Brand
实体类上的属性上添加校验注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Data @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) @TableName("pms_brand") @ApiModel(value = "Brand对象") @EqualsAndHashCode(callSuper = false) public class Brand implements Serializable { private static final long serialVersionUID = 1L ; @TableId(value = "brand_id",type = IdType.AUTO) @ApiModelProperty(value = "品牌id") private Long brandId; @NotBlank(message = "品牌名不能为空!") @ApiModelProperty(value = "品牌名") private String name; @NotEmpty(message = "Logo不能为空!") @URL(message = "Logo必须是一个合法的 URL 地址") @ApiModelProperty(value = "品牌logo地址") private String logo; @NotBlank(message = "品牌介绍不能为空!") @ApiModelProperty(value = "介绍") private String description; @NotNull(message = "显示状态不能为空!") @Min(value = 0,message = "显示状态必须为0 或 1") @Max(value = 1,message = "显示状态必须为0 或 1") @TableLogic(value = "1",delval = "0") @ApiModelProperty(value = "显示状态[0-不显示;1-显示]") private Integer showStatus; @NotBlank(message = "检索首字母不能为空!") @Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母") @ApiModelProperty(value = "检索首字母") private String firstLetter; @NotNull(message = "排序不能为空!") @Min(value = 0, message = "排序必须大于等于0") @ApiModelProperty(value = "排序") private Integer sort; }
10.2、统一异常处理 需要在 Controller
方法需要校验的对象前使用 @Valid
注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RestControllerAdvice(basePackages = "com.hzx.grain") public class GrainMallExceptionControllerAdvice { @ExceptionHandler(value = MethodArgumentNotValidException.class) public R handleNotValidException (MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); Map<String, String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError) -> { errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); }); return R.error(ResultCodeEnum.NOT_VALID_EXCEPTION.getCode(), ResultCodeEnum.NOT_VALID_EXCEPTION.getMsg()).put("data" ,errorMap); } @ExceptionHandler(value = Exception.class) public R handleException (Exception e) { return R.error(ResultCodeEnum.UN_KNOW_EXCEPTION.getCode(), ResultCodeEnum.UN_KNOW_EXCEPTION.getMsg()); } }
10.3、添加分组校验 在进行新增品牌对象时,不能指定 ID ,在修改时必须携带 ID
1、创建两个校验组,分别为 AddGroup
和 UpdateGroup
只需要创建两个空接口即可
1 2 3 public interface AddGroup /UpdateGroup { }
2、修改实体类 由于新增、修改都需要指定品牌名,所以为品牌 name 分组时需要指定两个分组,即修改校验组和新增校验组。
没有指定校验组的注解不会生效,所以我们需要为所有属性添加校验组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @Data @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) @TableName("pms_brand") @ApiModel(value = "Brand对象") @EqualsAndHashCode(callSuper = false) public class Brand implements Serializable { private static final long serialVersionUID = 1L ; @NotNull(message = "修改时必须指定ID",groups = UpdateGroup.class) @Null(message = "新增时不能指定 ID",groups = AddGroup.class) @TableId(value = "brand_id",type = IdType.AUTO) @ApiModelProperty(value = "品牌id") private Long brandId; @NotBlank(message = "品牌名不能为空!",groups = {UpdateGroup.class, AddGroup.class}) @ApiModelProperty(value = "品牌名") private String name; @NotEmpty(message = "Logo不能为空!",groups = {AddGroup.class}) @URL(message = "Logo必须是一个合法的 URL 地址",groups = {UpdateGroup.class, AddGroup.class}) @ApiModelProperty(value = "品牌logo地址") private String logo; @NotBlank(message = "品牌介绍不能为空!",groups = {AddGroup.class}) @ApiModelProperty(value = "介绍") private String description; @NotNull(message = "显示状态不能为空!",groups = {AddGroup.class}) @Min(value = 0,message = "显示状态必须为0 或 1",groups = {UpdateGroup.class, AddGroup.class}) @Max(value = 1,message = "显示状态必须为0 或 1",groups = {UpdateGroup.class, AddGroup.class}) @TableLogic(value = "1",delval = "0") @ApiModelProperty(value = "显示状态[0-不显示;1-显示]") private Integer showStatus; @NotBlank(message = "检索首字母不能为空!",groups = {AddGroup.class}) @Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母",groups = {UpdateGroup.class, AddGroup.class}) @ApiModelProperty(value = "检索首字母") private String firstLetter; @NotNull(message = "排序不能为空!",groups = {AddGroup.class}) @Min(value = 0, message = "排序必须大于等于0",groups = {UpdateGroup.class, AddGroup.class}) @ApiModelProperty(value = "排序") private Integer sort; }
3、修改 Controller 将原来的 @Valid
换为 @Validated
注解,然后在注解中指定此 Controller 接口使用的校验组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @PostMapping("save") @ApiOperation("保存品牌信息") public R save (@Validated({AddGroup.class}) @RequestBody Brand brand) { brandService.save(brand); return R.ok(); } @PostMapping("update") @ApiOperation("更新品牌信息") public R update (@Validated({UpdateGroup.class}) @RequestBody Brand brand) { brandService.updateStatus(brand); return R.ok(); }
10.4、自定义校验注解 1、创建自定义注解 这个注解用于校验 showStatus
,其中 showStatus
不能为空,且只能为 0 或 1
1 2 3 4 5 6 7 8 9 10 @Documented @Constraint(validatedBy = {AllowValuesValidator.class}) @Target( {ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE} ) public @interface AllowValues { String message () default " {com.hzx.grain.common.valid.AllowValues.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // 可以包含的值 int[] values() default { }; }
2、创建一个类,用于校验自定义注解修饰的字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class AllowValuesValidator implements ConstraintValidator <AllowValues , Integer > { private Set<Integer> set = new HashSet<>(); @Override public void initialize (AllowValues constraintAnnotation) { int [] values = constraintAnnotation.values(); for (int value : values) { set.add(value); } } @Override public boolean isValid (Integer value, ConstraintValidatorContext context) { return set.contains(value); } }
3、添加自定义注解 1 2 3 4 5 6 7 8 @NotNull(message = "状态不能为空!",groups = {AddGroup.class}) @AllowValues(values = {0,1},groups = {UpdateGroup.class, AddGroup.class}) @TableLogic(value = "1",delval = "0") @ApiModelProperty(value = "显示状态[0-不显示;1-显示]") private Integer showStatus;
4、测试 Java应用学习(八)-使用spring-boot-starter-validation完成参数校验