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指定属于哪个组。
创建AddGroup
和UpdateGroup
接口分别表示添加组和更新组。
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
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;
|