概述:
在做项目的过程中,除了要在前端进行数据校验外,服务端也必须做相应的校验,因为高手可绕过前端的校验,直接进入服务端调用相关的方法,进行资料的盗取或破坏。在前端如果使用Vue+ElementUI的方式,ElementUI提供了强大的校验功能,同时也可自定校验,而后端,使用Springboot 框架的项目则提供了JSR303校验,那么什么是JSR303校验?在项目中如何使用?本篇让我进行详细的讲解
一、什么是JSR303校验
- JSR是Java Specification Requests的缩写,意思是Java 规范提案
- JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,即JSR 303
- Bean Validation规范 ,为Bean验证定义了元数据模型和API.。默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展。
二、SR303数据校验使用步骤
1、创建Maven工程,在pom.xml文件引入依赖
<!--校验Valid-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!--自定义注解-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
2、创建品牌表
CREATE TABLE `kmall_product`.`pro_brand` (
`brand_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
`name` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌名',
`logo` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌logo地址',
`descript` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '介绍',
`show_status` tinyint(4) NULL DEFAULT NULL COMMENT '显示状态[0-不显示;1-显示]',
`first_letter` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '检索首字母',
`sort` int(11) NULL DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`brand_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '品牌' ROW_FORMAT = Dynamic;
3、根据表创建品牌实体类,在类中添加验证规则
- @NotNull 不为Null 验证,UpdateGroup、UpdateStatusGroup表示在更新时进行不为空的验证
- @Null为Null 验证,AddGroup表示在添加时进行为空的验证
- @NotBlank不为空验证,AddGroup、UpdateGroup表示在新增与更新时都要验证
- @URL地址合法性验证,
- @ListValue验证值是否在列举的范围内
- @Pattern自定义正则表达式进行验证
- @Min最小值验证
@Data
@TableName("pro_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class, UpdateStatusGroup.class})
@Null(message = "新增不能指定id", groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class, UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(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;
/**
* 检索首字母
*/
@NotEmpty(groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = {AddGroup.class})
@Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class, UpdateGroup.class})
private Integer sort;
}
4、在Controller中的方法形参前加上@Valid注解
- 需要在参数前添加注解: @Validated(AddGroup.class)
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand, BindingResult result){
HashMap<String, String> map = new HashMap<>();
if(result.hasErrors()){
result.getFieldErrors().forEach((item)->{
String field = item.getField();
String message = item.getDefaultMessage();
map.put(field,message);
});
return R.error(400,"提交的数据不合法").put("data",map);
}else{
brandService.save(brand);
}
return R.ok();
}
三、对校验的错误信息进行统一处理
Springboot提供了@RestControllerAdvice注解,可以对错误信息进行拦截
- 定义验证错误拦截方法handleValidException
- 添加注解 @ExceptionHandler(value = MethodArgumentNotValidException.class),表示拦截验证错误信息
- BindingResult result = e.getBindingResult(); 获取校验错误信息
- 遍历把错误结构放入errorMap 中
- 最后将errorMap 信息返回
@Slf4j
@RestControllerAdvice(basePackages = "com.koo.modules.product.controller")
public class MyExceptionControllerAdvice {
/**
* 统一处理异常,可以使用Exception.class先打印一下异常类型来确定具体异常
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleValidException(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{}, 异常类型:{}", e.getMessage(), e.getClass());
BindingResult result = e.getBindingResult();
Map<String, String> errorMap = new HashMap<>(16);
// 获取校验的错误结果
result.getFieldErrors().forEach((item) -> {
// 获取错误的属性名字 + 获取到错误提示FieldError
errorMap.put(item.getField(), item.getDefaultMessage());
});
return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(), BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data", errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleValidException(Throwable throwable) {
log.error("Throwable错误,未处理:" + throwable);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
/**
* 自定义异常
* @param e
* @return
*/
@ExceptionHandler(value = BusinessException.class)
public R handleBusinessException(BusinessException e) {
R r = new R();
r.put("code", e.getCode());
r.put("msg", e.getMessage());
return r;
}
}
四、分组校验
场景:当我们插入一条数据时,我们可能需要对很多字段进行校验,但在修改时,我们可能只需要对修改的相关字段进行校验,此外,也有可能存在,添加时需要校验而修改不需要校验等情况,所以,我们应该针对不同的操作场景使用不同的校验规则,以此来确保精准校验。
特别注意:
一旦我们在注解@Validated中指定了校验分组,那么对于哪些没有指定校验分组的字段将不再生效。例如下面的这个字段,@NotBlank(message = “logo地址不能为空”) 校验由于没有指定校验分组,而@Validated又指定了Insert分组,所以,该校验无效。
@URL(message = "需要是一个合法的URL",groups = {Insert.class, Update.class})
@NotBlank(message = "logo地址不能为空")
@ApiModelProperty("品牌logo")
private String logo;
1.在校验注解中有个groups属性,里面存放一个接口数组,例如:
@NotNull(message = “修改时品牌id不能为空”,groups = {UpdateGroup.class,AddGroup.class})
/**
* 检索首字母
*/
@NotEmpty(groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {UpdateGroup.class,AddGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = {AddGroup.class})
@Positive(message = "排序必须为正整数",groups = {UpdateGroup.class,AddGroup.class})
private Integer sort;
2、进行了统一异常与分组处理后,可将上面的保存方法精简
@RequestMapping("/save")
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand/*, BindingResult result*/){
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);
return R.ok();
}
3、异常结果:
五、自定义校验规则
虽然Springboot提供了很多的校验规则,但是不能完全满足项目的实际需要,例如:品牌表中有展示/影藏的字段,限制在[0、1]范围内,不在这个范围的数值插入进来提示“必须提交指定的值”。这个就需要我们自定义校验规则。
根据Springboot校验的源码,我们知道,所有的注解都有三个必须的属性
String message() default "{com.koo.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
1、创建ListValue注解校验
- @Target 表示可以标注在哪些位置 方法、参数、构造器
- @Retention(RUNTIME) 可以在什么时候获取到
- @Constraint 使用哪个校验器进行校验的(这里不指定,在初始化的时候指定)
- int[] vals() default {}; 指定数据只能是vals数组指定的值
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})// 可以标注在哪些位置 方法、参数、构造器
@Retention(RUNTIME)// 可以在什么时候获取到
@Documented//
@Constraint(validatedBy = {ListValueConstraintValidator.class})// 使用哪个校验器进行校验的(这里不指定,在初始化的时候指定)
public @interface ListValue {
// 默认会找ValidationMessages.properties
String message() default "{com.koo.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 可以指定数据只能是vals数组指定的值
int[] vals() default {};
}
2、添加校验约束
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
if (vals != null && vals.length != 0) {
for (int val : vals) {
set.add(val);
}
}
}
/**
* 校验逻辑
* @param value 需要校验的值
* @param context 上下文
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);// 如果set length==0,会返回false
}
}
3、在ValidationMessages.properties配置提示信息
valid.com.koo.common.valid.ListValue.message=必须提交指定的值
4、在实体类添加注解
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
5、更新状态时添加@Validated({UpdateStatusGroup.class})
/**
* 修改状态
*/
@RequestMapping("/update/status")
public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
6、测试
故意设置 showStatus 为2
源码下载:
https://gitee.com/charlinchenlin/koo-erp