统一异常处理

  1. 1. 统一异常处理与JSR303
    1. 1.1. 1. 自定义全局统一异常
    2. 1.2. 2. 自定义异常状态
  2. 2. jsr303校验
    1. 2.1. 1. 高版本spring校验依赖添加
    2. 2.2. 2. 简单校验参数
    3. 2.3. 3.分组异常校验
    4. 2.4. 4. 自定义校验注解

Spring统一异常处理与jsr303分组验证

统一异常处理与JSR303

1. 自定义全局统一异常

  在需要使用统一异常处理的微服务下创建exception包,并创建统一异常处理类。

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

/**
* 自定义全局统一异常
* RestControllerAdvice注解:表示要捕获异常,同时返回给前端json
* basePackages表示要拦截异常的包,这里指定拦截com.example.example.example.controller包里的异常
* @author hcxx
* @date 2022/07/05
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.example.example.example.controller")
public class ExampleExceptionControllerAdvice {

// 缩小范围异常,才能拿到其中的异常(才有具体的方法), 如果能够精确匹配这个异常,就匹配,否则就去下面的最大的异常
// 这里的MethodArgumentNotValidException是入参jsr303校验异常。就是后面说的jsr303验证不同过时
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现异常: {} , 异常类型:{}", e.getMessage(), e.getClass());
BindingResult bindingResult = e.getBindingResult();

HashMap<String, String> errorMap = new HashMap<>();
// 将所有异常信息存储到map中,返回给前端
bindingResult.getFieldErrors().forEach(fieldError -> {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(BIzCodeEnum.VALID_EXCEPTION.getCode(), BIzCodeEnum.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}

/**
* 最大的异常
* 注解@ExceptionHandler表示要捕获的异常
* @param throwable 可抛出的
*/
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
log.error("系统异常信息:{}", throwable.getMessage());
return R.error(BIzCodeEnum.UNKNOWN_EXCEPTION.getCode(), BIzCodeEnum.UNKNOWN_EXCEPTION.getMsg());
}
}

2. 自定义异常状态

这里的异常状态是可以通用的,所以可以写入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
/**
* 1. 错误代码定义为5位数字
* 2. 前两位表示业务场景
* 3. 错误描述,定义为枚举
*/
public enum BIzCodeEnum {

/**
* 异常状态枚举
*/
UNKNOWN_EXCEPTION(10000, "系统未知异常"),
VALID_EXCEPTION(10001, "参数格式校验失败");

private Integer code;
private String msg;

public Integer getCode() {
return code;
}

public String getMsg() {
return msg;
}

BIzCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}

jsr303校验

1. 高版本spring校验依赖添加

  考虑到大多数服务多需要校验,可以直接添加在common服务里。这里的版本最好和srping-boot版本一致。

1
2
3
4
5
6
<!-- Bean 校验注解 ,高版本要用starter的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.6.8</version>
</dependency>

2. 简单校验参数

  如果要对controller的入参进行校验,则需要使用注解显示的开启校验。使用@Validated,由spring提供,(另有@Valid由javax提供)。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 保存
* Validated(AddGroup.class) 注解,标识为只校验添加组的规则
* @Validated 注解表明要进行校验;后面的(AddGroup.class) 表示要进行第3步的分组校验,这里是更新标记类,
* 注意,这个类是我们自己创建的一个空类,仅用来标记,我们业务上约定,有这个类的属于更新。
* AddGroup.class 可以不写,则为不开启分组校验,则实体类上也不要写groups=?。则为校验所有类型,不分组
* BindingResult result 是在没有开启统一异常前,所有校验异常都会在这里,用于获取异常,统一异常处理后不再需要
*/
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand /*, BindingResult result*/ ) {
brandService.save(brand);
return R.ok();
}

3.分组异常校验

  有时候,某个字段,例如Id,再新增的时候必须为null,在更新的时候必须是不为null,实体类上的校验注解无法直接区分,这时候就需要进行分组校验了。分组校验就是在实体类上的校验注解加上groups数组的属性,数组中的类是空标记类,用来标记分组。同时controller方法上指明是哪一个标记。表示这个方法只校验所属groups的注解。
  message是出现异常的时候返回的异常信息,groups表示这个实体类被标记为哪些操作下需要校验。例如brandId字段我们同时指定Null与NotNull,在更新时必须不能为空,新增时必须为空。
  业务上我们规定:AddGroup类为新增标记类,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
35
36
37
38
39
40
41
42
43
44
45
46
47
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;

/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class})
@Null(message = "新增不能指定id", groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址, 可以有多个注解, 如果不指定组,则不会生效,所以即使所有都用,也必须指定组。
*/
@NotBlank(message = "logo不能为空", groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class, UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母, A-Z,a-z,使用正则
*/
@NotEmpty(message = "检索首字母不能为空", groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序, 最小为0, Integer不能使用empty约束
*/
@NotNull(message = "排序不能为空", groups = {AddGroup.class})
@Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;

}

4. 自定义校验注解

  1. 正则表达式:可以使用正则表达式来实现自定义的目的。@Pattern
  2. 编写一个自定义的校验注解:@ListValue(value=(0,1))。该自定义注解规定只能使用values值0和1
  3. 编写时可以参考已经存在的官方注解,例如@NotNull等。该注解有三个属性messagegroupspayload
    1. message:在使用者不指定message的情况下,默认返回的报错信息。
    2. groups:支持分组校验,默认不分组
    3. payload:使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) // 注解可以标注的位置
@Retention(RetentionPolicy.RUNTIME) // 校验注解的时机:运行时
@Repeatable(NotNull.List.class)
@Documented
@Constraint(validatedBy = {}) // 指明使用哪个校验器(类) 去校验使用了此标注的元素。不指定则必须在初始化的时候指定,可以指定多个校验器注解,使用时自动适配
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";

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

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

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotNull[] value();
}
}
  1. 自定校验注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class}) // 指定校验器为ListValueConstraintValidator类,第6点
@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.example.common.valid.ListValue.message}"; // 在resources下的配置文件ValidationMessages.properties中取出该值。第5点

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

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

/**
* 自定义的属性
* vals默认为空
*/
int[] vals() default {};
}

  1. resources下创建配置文件ValidationMessages.properties
1
com.example.common.valid.ListValue.message=必须提交指定的值
  1. 编写自定义校验器类
    1. 要实现ConstraintValidator类,两个范形,第一个是ListValue,表示要校验的注解。第二个是Integer,是该注解只能作用于Integer字段。然后重写两个方法
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
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {

private Set<Integer> set = new HashSet<>();

/**
* 初始化方法,可以用来获取所有校验规则传入的值
* @date 2022/7/10
* @param constraintAnnotation:
*/
@Override
public void initialize(ListValue constraintAnnotation) {
// 给的的值必须是vals, 注解上的val。(获取注解上的vals自定义的属性的值)
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}

/**
* 上一个方法获取所有的值后,可以用来判断是否校验成功
* @date 2022/7/10
* @param value: 前端提交过来的值
* @param constraintValidatorContext: 校验的上下文环境信息
* @return boolean
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(value);
}
}
  1. 使用自定义注解
1
2
3
4
5
6
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;