JSR 303 数据校验
https://www.jianshu.com/p/48725b7328c9
JSR 是 Java Specification Requests 的缩写,即 Java 规范提案。简单的理解为 JSR 是一种 Java 标准,存在各种各样的 JSR,JSR 303 就是数据检验的一个标准。JSR-303 是 Java EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是hibernate Validator。此实现与 Hibernate ORM 没有任何关系。 JSR 303 用于对 Java Bean 中的字段的值进行验证。 Spring MVC 3.x 之中也大力支持 JSR-303,可以在控制器中对表单提交的数据方便地验证。
Spring Boot 2.2 版本之前的场景启动器依赖中默认内置了该依赖(2.3之后的版本失效),如果需要单独引入依赖,可以使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.0.Final</version> </dependency> <dependency> <groupId>javax.el</groupId> <artifactId>el-api</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>javax.el</artifactId> <version>2.2.4</version> </dependency>
|
相关注解
Bean Validation 中内置的 constraint:
Constraint |
详细信息 |
@Null |
被注释的元素必须为 null |
@NotNull |
被注释的元素必须不为 null |
@AssertTrue |
被注释的元素必须为 true |
@AssertFalse |
被注释的元素必须为 false |
@Min(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) |
被注释的元素的大小必须在指定的范围内 |
@Digits(integer, fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past |
被注释的元素必须是一个过去的日期 |
@Future |
被注释的元素必须是一个将来的日期 |
@Pattern(value) |
被注释的元素必须符合指定的正则表达式 |
Hibernate Validator 附加的 constraint:
Constraint |
详细信息 |
@Email |
被注释的元素必须是电子邮箱地址 |
@Length |
被注释的字符串的大小必须在指定的范围内 |
@NotEmpty |
被注释的字符串的必须非空 |
@Range |
被注释的元素必须在合适的范围内 |
使用示例
- 首先在实体类的相关属性上添加相应注解:
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
|
@Data @TableName("pms_brand") public class BrandEntity implements Serializable { private static final long serialVersionUID = 1L;
@TableId private Long brandId;
@NotBlank(message = "品牌名必须提交") private String name;
@NotEmpty @URL(message = "log必须是一个合法的url地址") private String logo;
private String descript;
private Integer showStatus;
@NotEmpty @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母") private String firstLetter;
@NotNull @Min(value = 0, message = "排序必须大于等于0") private Integer sort;
}
|
- 在 Controller 层相应实体类参数上开启校验
@Valid
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
@RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){ if (result.hasErrors()) { Map<String, String> map = new HashMap<>(); result.getFieldErrors().forEach(item -> map.put(item.getField(), item.getDefaultMessage())); return R.error(400, "提交的数据不合法").put("data", map); } brandService.save(brand); return R.ok(); }
|
参数中的 BindingResult
用于绑定校验异常的信息。一旦解析时发现某个参数不合法,就会将相应信息(字段名、注解中配置的 message
等)包装到该对象中,通过调用该对象的相关方法即可得到字段与信息。
如果没有在参数列表中写 BindingResult
,则异常就会向外抛出,不会进入到业务方法内。此时就可以写一个异常处理的切面类,统一处理这些抛出的校验异常。
统一异常处理
创建统一处理异常的切面类 MallExceptionControllerAdvice
,使用 Spring MVC 提供的 @ControllerAdvice
注解并指定要扫描的包,这样这些 Controller 在抛出异常时就会被该切面类拦截到,并做相应处理,从而实现业务和异常处理的解耦。
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
|
@Slf4j @RestControllerAdvice(basePackages = "com.zhao.yunmall.product.controller")
public class MallExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class) public R handleValidException(MethodArgumentNotValidException e) { log.error("数据校验出现问题{},异常类型{}", e.getMessage(), e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String, String> map = new HashMap<>(); bindingResult.getFieldErrors().forEach(item -> map.put(item.getField(), item.getDefaultMessage())); return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data", map); }
@ExceptionHandler(value = Throwable.class) public R handlException(Throwable throwable) { return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMsg()); } }
|
这样 Controller 层的业务就不需要修改,实现了无侵入的异常处理:
1 2 3 4 5 6 7 8 9 10
|
@RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand){ brandService.save(brand); return R.ok(); }
|
其中,错误码和错误信息定义类用于同一规定错误码信息,在 mall-common
包下:
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
| package com.zhao.common.exception;
public enum BizCodeEnum {
UNKNOWN_EXCEPTION(10000,"系统未知异常"), VALID_EXCEPTION(10001,"参数格式校验失败");
private final int code; private final String msg;
BizCodeEnum(int code,String msg){ this.code = code; this.msg = msg; }
public int getCode() { return code; }
public String getMsg() { return msg; } }
|
JSR 303 分组校验
当需要多场景复杂校验时(例如新增时 id
字段必须为空,修改时不能为空),就要给校验注解标注什么情况需要进行校验,也就是分组校验。
- 添加分组后的实体类注解:
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
| package com.zhao.yunmall.product.entity;
import com.zhao.common.valid.AddGroup; import com.zhao.common.valid.UpdateGroup; import com.zhao.common.valid.UpdateStatusGroup;
@Data @TableName("pms_brand") public class BrandEntity implements Serializable { private static final long serialVersionUID = 1L;
@TableId @NotNull(message = "修改时必须指定品牌id", groups = {UpdateGroup.class}) @Null(message = "新增时不能指定品牌id", groups = {AddGroup.class}) private Long brandId;
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class}) private String name;
@NotEmpty(groups = {AddGroup.class}) @URL(message = "log必须是一个合法的url地址", groups = {AddGroup.class,UpdateGroup.class}) private String logo;
private String descript;
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class}) private Integer showStatus;
@NotEmpty(groups = {AddGroup.class}) @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class,UpdateGroup.class}) private String firstLetter;
@NotNull(groups = {AddGroup.class}) @Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class,UpdateGroup.class}) private Integer sort;
}
|
其中,groups = {AddGroup.class,UpdateGroup.class}
中的接口在 mall-common
模块的 valid
包下定义:
- 在 Controller 上添加
@Validated({AddGroup.class})
注解(该注解由 Spring 提供,非 Java 标准 JSR),并在其中指定当前请求的分组:
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
|
@RequestMapping("/save") public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){ brandService.save(brand); return R.ok(); }
@RequestMapping("/update/status") public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand){ brandService.updateById(brand); return R.ok(); }
@RequestMapping("/update") public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){ brandService.updateById(brand); return R.ok(); }
|
注意:如果一些字段的注解没有指定分组,则在分组校验情况下其校验逻辑不生效。这些字段只会在 @Validated()
内容为空时生效,一旦 @Validated()
中带了分组信息,那些没有指定分组的字段就会失效。
- 测试,发送不合法的请求,返回错误信息:
1 2 3 4 5 6 7 8 9 10
| { "msg": "参数格式校验失败", "code": 10001, "data": { "brandId": "新增时不能指定品牌id", "showStatus": "必须提交指定的值", "sort": "排序必须大于等于0", "firstLetter": "检索首字母必须是一个字母" } }
|
自定义校验
- 编写一个自定义的校验器
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
| public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> { private Set<Integer> set = new HashSet<>();
@Override public void initialize(ListValue constraintAnnotation) { int[] vals = constraintAnnotation.vals(); for(int val : vals) { set.add(val); } }
@Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return set.contains(value); } }
|
- 编写一个自定义的校验注解并绑定校验注解和其对应的校验类
@Constraint(validatedBy = {ListValueConstraintValidator.class})
。一个校验注解可以绑定多个校验类,其将根据该注解标注的字段的实际类型决定使用哪个校验类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class, xxx.class, xxx.class}) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface ListValue { String message() default "{com.zhao.common.valid.ListValue.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { };
// 自定义的属性 int[] vals() default { }; }
|
- 按照 JSR 规范,在
mall-common
模块添加配置文件:ValidationMessages.properties
,在其内填写自定义的异常回显信息:
1
| com.zhao.common.valid.ListValue.message=必须提交指定的值
|
- 在字段上添加自定义校验注解:
1 2 3 4 5 6
|
@ListValue(vals = {0, 1}) private Integer showStatus;
|
这样如果前端发来的数据中的 showStatus
字段的范围不在 0,1 之内,则会返回:
1 2 3 4 5 6 7
| { "msg": "参数格式校验失败", "code": 10001, "data": { "showStatus": "必须提交指定的值" } }
|