前言:
在实际开发中,除了前端需要在表单中验证用户的输入。后台服务也需要对用户传入的参数进行效验,避免他人在得知请求格式后,直接通过类似Postman这样的测试工具进行非常数据请求。
JSR303是一套JavaBean参数校验的标准,定义了很多常用的校验注解
可以直接将这些注解加在我们JavaBean的属性上面就可以在需要校验的时候进行校验了
接下来以添加品牌为例
我们可以使用javax.validation.constraints
包中提供的注解,给实体类字段添加规则。
@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;
}
@Valid
开启验证功能 @RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
在上例中,虽然我们完成了数据验证,但是返回给前端的数据并不友好。在项目中应该返回一个统一的结果。
使用BindingResult接受参数效验的结果
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
//1.出现参数非法情况
Map<String, String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach(fieldError -> {
map.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return R.error(400, "提交的数据不合法").put("data", map);
} else {
//2.参数验证通过, 执行正常逻辑
brandService.save(brand);
return R.ok();
}
}
添加BindingResult参数后,虽然可以使后台在出现异常时,进行处理并返回统一的结果。但是我们会发现,我们写了许多与业务不相关的代码,为了解决这个问题,我们可以通过@ControllerAdvice
进行异常的统一处理。
1、编写统一异常处理类
//@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());
}
}
2、把之前加的BindingResult去掉,还原成原先最干净的代码
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand) {
brandService.save(brand);
return R.ok();
}
测试结果一样的。
在我们后台的返回结果中,有个code状态码,前端可以根据这个状态码判断是出现了什么问题,如何解决。就好像是我们进行http请求时,我们知道200响应码代表请求成功、404代表找不到资源、500代表服务器出错等。
随着业务越来越复杂,异常的类型越来越多,为了统一规范,我们就不该将code状态码写死在代码中,而应该统一管理起来。
我们可以使用枚举类进行管理,如下。
/***
* 错误码和错误信息定义类
* 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;
}
}
2、修改我们全局异常处理类中的代码
@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注解为例
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
//分组验证时使用
Class<?>[] groups() default { };
...
我们通过@NotNul
注解的groups指定属于哪个组
实现步骤:
1、创建AddGroup
和UpdateGroup
接口分别表示添加组和更新组
//这俩个接口只是用来标记的,不需要实现
public interface AddGroup {
}
public interface UpdateGroup {
}
2、实体类中使用注解时,标明该验证规则属于哪个组
@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;
}
3、使用@Validated替代@Valid
@Validated是@Valid的变体,它支持分组效验功能
@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();
}
4、测试
/**
* 显示状态[0-不显示;1-显示]
*/
// @Pattern(regexp = "[0-1]") //pattern不支持Integer
private Integer showStatus;
在@Pattern的注释中,有下面这一段话,说明了该注解不支持Interger类型。那怎么办呢?
Accepts {@code CharSequence}. {@code null} elements are considered valid.
当提供的验证规则中没有我们需要的时,它支持我们自定义验证规则。(我知道有办法实现只能0和1,我只是想说可以自定义效验注解,别杠( o=^•ェ•)o ┏━┓)
自定义效验注解步骤:
1、添加依赖
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
<version>2.0.1.Finalversion>
dependency>
2、编写一个自定义的效验注解
该注解的功能,验证输入的参数是否在value中。
@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();
}
3、在工程resource中创建ValidationMessages.properties配置文件
com.fcp.common.valid.ListValue.message=The committed number is not in the specified array
4、编写一个自定义的效验器
/**
* 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);
}
}
5、关联自定义的效验器和自定义的效验注解
第二步已经做了,就是这个
@Constraint(validatedBy = { ListValueConstraintValidator.class})
至此,自定义效验器完成,可以开心的使用了:
//表示输入的参数,必须要在value指定的数组中,也就是0和1
@ListValue(value = {0, 1},groups = {AddGroup.class,UpdateGroup.class})
private Integer showStatus;