JSR303是一套JavaBean参数校验的标准,定义了很多常用的校验注解。我们需要对前端传的参数进行校验。

使用javax.validation.constraints包中提供的注解,给实体类字段添加校验。

给实体类添加验证规则

 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
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId
    private Long brandId;

    //约束name不能为null,且至少有一个非空字符
    @NotBlank(message = "品牌名必须提交")
    private String name;

    @NotBlank(message = "logo不能为空")
    //URL是hibernate提供的注解,实现了JSR303规范。约束如果logo不为null的话,必须符合url格式
    @URL(message = "logo格式不符")		
    private String logo;

    private String descript;

    //	@Pattern(regexp = "[0-1]")		//pattern不支持Integer
    private Integer showStatus;

    //使用正则表达式约束字段
    @Pattern(regexp = "^[a-zA-Z]$" , message = "首字母必须是一个字母")
    private String firstLetter;

    @Min(value = 0 , message = "排序字段必须大于等于0")
    private Integer sort;
}

controller层使用@Valid开启验证功能

1
2
3
4
5
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
    brandService.save(brand);
    return R.ok();
}

用 postman 进行验证,返回:

1
2
3
4
5
6
7
{
    "timestamp": "2022-09-01T08:04:19.480+00:00",
    "status": 400,
    "error": "Bad Request",
    "message": "",
    "path": "/save"
}

不是理想的返回结果。

全局异常处理器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//@RestControllerAdvice其实就是@ControllerAdvice+@ResponseBody,表示返回的是json格式
@RestControllerAdvice
@Slf4j
public class GlobalExceptionControllerAdvice {

    //捕获MethodArgumentNotValidException类型的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handlerMethodArgumentNotValidException(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            map.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(400,"提交的数据不合法").put("data",map);
    }

    //兜底
    @ExceptionHandler(Exception.class)
    public R handlerException(Exception e){
        return R.error(10000,"未知的系统异常").put("data",e.getMessage());
    }
}

测试结果:

1
2
3
4
5
6
7
8
{
    "msg": "提交的数据不合法",
    "code": 400,
    "data": {
        "name": "品牌名必须提交",
        "logo": "logo不能为空"
    }
}

code状态码

在我们后台的返回结果中,有个code状态码,前端可以根据这个状态码判断是出现了什么问题,如何解决。就好像是我们进行http请求时,我们知道200响应码代表请求成功、404代表找不到资源、500代表服务器出错等。

随着业务越来越复杂,异常的类型越来越多,为了统一规范,我们就不该将code状态码写死在代码中,而应该统一管理起来。

我们可以使用枚举类进行管理,如下。

 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
/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

修改我们全局异常处理类中的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestControllerAdvice
@Slf4j
public class GlobalExceptionControllerAdvice {

    //捕获MethodArgumentNotValidException类型的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R handlerMethodArgumentNotValidException(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> map = new HashMap<>();
        bindingResult.getFieldErrors().forEach(fieldError -> {
            map.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",map);
    }

    //兜底
    @ExceptionHandler(Exception.class)
    public R handlerException(Exception e){
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg()).put("data",e.getMessage());
    }
}

分组校验

在简单的数据验证中,我们使用完成了数据验证。但是还存在一些问题,如在添加品牌的时候brandId为null,但在修改品牌的时候brandId不能为null,这样的话,就冲突了。那怎么办呢?我们可以给他们分个组,添加操作使用一组验证规则,修改操作使用一组验证规则。这就是分组验证的功能。

我们观察@NotNull

1
2
3
4
5
6
7
@Constraint(validatedBy = { })
public @interface NotNull {
    String message() default "{javax.validation.constraints.NotNull.message}";

    //分组验证时使用
    Class<?>[] groups() default { };
}

可以通过@NotNul注解的groups指定属于哪个组。

创建AddGroupUpdateGroup接口分别表示添加组和更新组。

1
2
3
4
5
6
//这俩个接口只是用来标记的,不需要实现
public interface AddGroup {
}

public interface UpdateGroup {
}

实体类中使用注解时,标明该验证规则属于哪个组。

 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
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId
    //只有在AddGroup组才会生效
    @Null(message = "添加操作  不要传brandId",groups = AddGroup.class)
    //只有在UpdateGroup组才会生效
    @NotNull(message = "修改操作  brandId不能为null",groups = UpdateGroup.class)
    private Long brandId;

    @NotBlank(message = "添加操作 品牌名必须提交",groups = AddGroup.class)
    private String name;

    @NotBlank(message = "添加操作 logo不能为空",groups = AddGroup.class)
    //在AddGroup组和UpdateGroup组中都会生效
    @URL(message = "logo格式不符",groups = {AddGroup.class,UpdateGroup.class})		//
    private String logo;

    private String descript;

    /**
	 * 显示状态[0-不显示;1-显示]
	 */
    //	@Pattern(regexp = "[0-1]")		//pattern不支持Integer
    private Integer showStatus;

    @Pattern(regexp = "^[a-zA-Z]$" , message = "首字母必须是一个字母" , groups = {AddGroup.class,UpdateGroup.class})
    private String firstLetter;

    @Min(value = 0 , message = "排序字段必须大于等于0",groups = {AddGroup.class,UpdateGroup.class})
    private Integer sort;
}

使用@Validated替代@Valid,@Validated是@Valid的变体,它支持分组效验功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@RequestMapping("/save")
//使用AddGroup组中的验证规则
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {
    brandService.save(brand);
    return R.ok();
}

@PutMapping("/update")
//使用UpdateGroup组中的验证规则
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand) {
    brandService.updateById(brand);
    return R.ok();
}

自行测试。

自定义效验注解

1
2
//	@Pattern(regexp = "[0-1]")		//pattern不支持Integer
private Integer showStatus;

在@Pattern的注释中,有下面这一段话,说明了该注解不支持Interger类型。那怎么办呢?

1
Accepts {@code CharSequence}. {@code null} elements are considered valid.

当提供的验证规则中没有我们需要的时,它支持我们自定义验证规则。(我知道有办法实现只能0和1,我只是想说可以自定义效验注解,别杠。

自定义效验注解步骤:

  1. 添加依赖
  2. 编写一个自定义的效验注解
  3. 编写一个自定义的效验器
  4. 关联自定义的效验器和自定义的效验注解
1
2
3
4
5
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

编写一个自定义的效验注解,该注解的功能,验证输入的参数是否在value中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
//指定使用哪个效验器,如果不指定的话,就需要在初始化的时候指定
//可以指定多个不同的效验器,适配不同类型的效验
@Constraint(validatedBy = { ListValueConstraintValidator.class})
public @interface ListValue {
    //JSR303规范中,要求必须有message、groups、payload这三个方法
    //default: 当message为null时,默认会到ValidationMessages.properties配置文件中找com.fcp.common.valid.ListValue.message的值
    String message() default "{com.fcp.common.valid.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    //用来存放符合规则的数字
    int[] value();
}

在工程resource中创建ValidationMessages.properties配置文件

1
com.fcp.common.valid.ListValue.message=The committed number is not in the specified array

编写一个自定义的效验器

 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
/**
* ListValue:使用的效验注解类型
* Integer: 被验证目标类型。我们验证的目标都是数字所以是Integer
*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> contain = new HashSet<>();

    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] values = constraintAnnotation.value();
        if (values==null) return;

        //将符合规则的值放到容器中
        for (int value : values) {
            contain.add(value);
        }
    }

    //该方法判断参数合不合法
    //value是需要验证的值,即用户输入的参数
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        //返回用户输入的参数是否在容器中
        return contain.contains(value);
    }
}

关联自定义的效验器和自定义的效验注解,第二步已经做了,就是这个:

1
@Constraint(validatedBy = { ListValueConstraintValidator.class})

至此,自定义效验器完成,可以开心的使用了:

1
2
3
//表示输入的参数,必须要在value指定的数组中,也就是0和1
@ListValue(value = {0, 1},groups = {AddGroup.class,UpdateGroup.class})
private Integer showStatus;