在我们实际编程中,几乎都会使用到后端数据校验功能,是SpringBoot中,更是简化了这一操作,让我们来简单看一下!
本教程仅作为我个人的学习记录,避免遗忘,方便下次快速使用,如有错误,请指出,谢谢观看~
本文完整示例代码下载:https://download.youkuaiyun.com/download/qq_42628989/13125444
一、SpringBoot模拟使用场景
我们模拟这么一个场景:在系统中添加一本新的图书
@Data
public class BookModel {
private String id;
private String name;
private String coverUrl;
private String describe;
private Integer isDelete;
}
@RestController
@Slf4j
public class BookController {
@PostMapping("/addBook")
public Map<String,Object> addBook(@RequestBody BookModel book)
{
/* book....... 一系列数据库操作*/
log.error("book.id:{}",book.getId());
log.error("book.name:{}",book.getName());
log.error("book.coverUrl:{}",book.getCoverUrl());
log.error("book.describe:{}",book.getDescribe());
log.error("book.isDelete:{}",book.getIsDelete());
return returnTools(200,"书籍信息录入成功!");
}
public Map<String,Object> returnTools(Integer code,String msg)
{
Map<String,Object> map = new HashMap<>();
map.put("code",code);
map.put("msg",msg);
return map;
}
}
然后我们来测试访问这个接口:
可以看到图书信息被正常保存,并且成功返回信息!
但是在这里我们可以发现,我们并没有输入全部信息,接口也成功执行,并成功返回数据,这在很多情境下是不被允许的!
所以在我们的代码中经常可以看到无数个if(){}else{}组成的校验代码,可以使用,但是代码量非常多,并且每个需要数据校验的Controller中都需要手动调用。
所以接下在我们就介绍一下Jsr303规范下的validation数据校验框架!
二.数据校验简单使用
1.简单使用注解校验
我们直接在需要校验的Model字段上加上条件即可
@Data
public class BookModel {
@NotNull(message = "请输入书籍ID")
private String id;
@NotNull(message = "请输入书籍名称")
private String name;
@NotNull(message = "封面图片链接必须填写")
@URL(message = "封面必须为URL格式")
private String coverUrl;
@NotNull(message = "请填写书籍简介")
@Length(min = 5,message = "简介最小长度为5")
private String describe;
@NotNull(message = "isDelete字段不存在")
private Integer isDelete;
}
然后在需要校验的Controller上加入@Valid注解即可!
我们再次进行测试:
此时我们访问该接口就可以发现,校验注解已生效,返回了错误信息。
2.自定义校验错误处理
但是在我们的实际项目中,这样的返回格式显然不是我们想要的,我们需要返回自定义格式,这时只需要在需要校验的模型后紧跟BindingResult对象即可:
@PostMapping("/addBook")
public Map<String,Object> addBook(@Valid @RequestBody BookModel book, BindingResult result)
{
if(result.hasErrors())
{
return returnTools(500,"书籍信息输入不全~");
}
else {
/* book....... 一系列数据库操作*/
log.error("book.id:{}", book.getId());
log.error("book.name:{}", book.getName());
log.error("book.coverUrl:{}", book.getCoverUrl());
log.error("book.describe:{}", book.getDescribe());
log.error("book.isDelete:{}", book.getIsDelete());
return returnTools(200, "书籍信息录入成功!");
}
}
我们再来测试一下:
此时就可以发现自定义返回信息生效!
注意:
此方法虽能有效解决数据校验问题,但是需要侵入式较强的修改代码,且每一个需要校验的Controller都需要修改,比较繁琐,不推荐使用,在此不过多介绍!
3.全局统一异常处理
使用SpringBoot的注解校验后,在校验异常时,会自动触发MethodArgumentNotValidException异常,我们只需要使用SpringBoot中的全局异常处理接收即可!
我们将工具类ReturnTools提取为公共方法:
并建立全局异常处理类:
@RestControllerAdvice
public class ExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler(value = MethodArgumentNotValidException.class)
public Map<String,Object> handleVaildException(MethodArgumentNotValidException e)
{
BindingResult bindingResult = e.getBindingResult();
Map<String,Object> map = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
map.put(error.getField(),error.getDefaultMessage());
});
return ReturnTools.returnTools(10001, "书籍信息录入不全",map);
}
@org.springframework.web.bind.annotation.ExceptionHandler(value = Throwable.class)
public Map<String,Object> handleAllException(Throwable t)
{
return ReturnTools.returnTools(10000,"系统未知异常",null);
}
}
删除以前的错误处理逻辑,只保留业务逻辑即可:
开始测试:
测试发现:我们不但可以返回自定义的数据格式,还可以更灵活的取得校验错误信息,是最推荐使用的异常处理方式!
三、分组校验
在我们的实际项目中,增加和修改需要校验的规则不是相同的:
- 在新增时,不可以填写ID字段,其他信息必须填写
- 在更新时,必须填写ID字段,其他信息可以选择性填写
这时候就是用到了分组校验这功能:
在分组校验中,规定了一个分组必须是一个接口,这个接口为空接口就可以,只是用来识别使用哪一种校验规则!
例如:我们添加一个Update接口吗,实现添加书籍和修改书籍时分别使用两种不同的校验规则!
- 首先建立两个接口,用于区别新增和修改(名称可以自定义,自己可分辨即可,并且无需实现)
2.在Model中编写第二套校验逻辑,并使用groups字段区分!
@Data
public class BookModel {
@Null(message = "新增时不能传入书籍ID",groups = {AddGroup.class})
@NotNull(message = "更新时必须传入书籍ID",groups = {UpdateGroup.class})
private String id;
@NotNull(message = "请输入书籍名称",groups = {AddGroup.class})
private String name;
@NotNull(message = "封面图片链接必须填写",groups = {AddGroup.class})
@URL(message = "封面必须为URL格式",groups = {AddGroup.class,UpdateGroup.class})
private String coverUrl;
@NotNull(message = "请填写书籍简介",groups = {AddGroup.class})
@Length(min = 5,message = "简介最小长度为5",groups = {AddGroup.class,UpdateGroup.class})
private String describe;
@NotNull(message = "isDelete字段不存在",groups = {AddGroup.class})
private Integer isDelete;
}
注意:
- 一个注解中可填写多个分组,比如“coverUrl和describe”,此时校验规则在新增和修改时都生效
- “coverUrl”字段中,我们同事加入了两个校验注解,此时“@URL”注解在新增和更新时都生效,但“ @NotNull”注解之在更新时生效,换句话说:在更新时可以不填写“coverUrl”字段,但是如果填写,则必须为URL的格式(“describe”字段同理,更新是可以不填写,但填写则必须长度大于5)
3.在Controller中加入分组校验注解并指定分组
@RestController
@Slf4j
public class BookController {
@PostMapping("/addBook")
public Map<String,Object> addBook(@Validated(value = {AddGroup.class}) @RequestBody BookModel book)
{
/* book....... 一系列数据库操作*/
log.error("add:book.id:{}", book.getId());
log.error("add:book.name:{}", book.getName());
log.error("add:book.coverUrl:{}", book.getCoverUrl());
log.error("add:book.describe:{}", book.getDescribe());
log.error("add:book.isDelete:{}", book.getIsDelete());
return ReturnTools.returnTools(200, "书籍信息录入成功!",null);
}
@PostMapping("/updateBook")
public Map<String,Object> updateBook(@Validated(value = {UpdateGroup.class}) @RequestBody BookModel book)
{
/* book....... 一系列数据库操作*/
log.error("update:book.id:{}", book.getId());
log.error("update:book.name:{}", book.getName());
log.error("update:book.coverUrl:{}", book.getCoverUrl());
log.error("update:book.describe:{}", book.getDescribe());
log.error("update:book.isDelete:{}", book.getIsDelete());
return ReturnTools.returnTools(200, "书籍信息更新成功!",null);
}
}
此时,如果访问addBook接口,则新增校验逻辑生效(除了ID其他必须不为空,ID必须为空)
如果访问updateBook接口,则更新校验逻辑生效(ID必须不为空,其他可以为空)
4.测试
测试结果完全正确!
四、自定义校验注解
在这个字段中,我们想让他赋值为0时显示,赋值为1时删除,但是我们并没有这样的注解供我们使用(也可以使用正则表达式和最大最小值等,但是为了做笔记嘛,假装没有~)
我们来字写一个注解@IsDelete,用来验证字段的值是不是1或0
其中,上方元注解不变即刻,下方的“message,groups,payload”为固定格式,直接复制即可!
我们使用Values指定符合条件的值,如果当前字段的值在数组中,则通过校验。
注解编写完成后,还需要编写相应的实现类,然后进行配置!
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {isDeleteImpl.class}
)
public @interface IsDelete {
/*这三条为固定格式,必须填写*/
String message() default "值不符合要求";
Class<?> [] groups() default {};
Class<? extends Payload> [] payload() default {};
int[] value();//如果当前数组包括字段的值则校验成功
}
public class isDeleteImpl implements ConstraintValidator<IsDelete, Integer> {
//两个参数
// 1.自定义注解的类型
//2.自定义注解修饰的目标的类型
int[] values;
@Override
public void initialize(IsDelete constraintAnnotation) {
values = constraintAnnotation.value();
}
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
if(values == null)
{
return false;
}
if(integer == null)
{
return true;
}
for (int value : values) {
if(value == integer.intValue())
{
return true;
}
}
return false;
//通过校验返回True,校验失败返回False
}
}
注意:不要忘了进行配置关联
然后进行测试:
可以发现测试成功!
4.更多校验注解(转载)
注解 作用类型 解释
@NotNull 任何类型 属性不能为null
@NotEmpty 集合 集合不能为null,且size大于0
@NotBlanck 字符串、字符 字符类不能为null,且去掉空格之后长度大于0
@AssertTrue Boolean、boolean 布尔属性必须是true
@Min 数字类型(原子和包装) 限定数字的最小值(整型)
@Max 同@Min 限定数字的最大值(整型)
@DecimalMin 同@Min 限定数字的最小值(字符串,可以是小数)
@DecimalMax 同@Min 限定数字的最大值(字符串,可以是小数)
@Range 数字类型(原子和包装) 限定数字范围(长整型)
@Length 字符串 限定字符串长度
@Size 集合 限定集合大小
@Past 时间、日期 必须是一个过去的时间或日期
@Future 时期、时间 必须是一个未来的时间或日期
@Email 字符串 必须是一个邮箱格式
@Pattern 字符串、字符 正则匹配字符串