目录
前言
数据校验是一个我们开发过程中经常用到的功能,不论是前端还是后端,都需要对数据进行校验,所以,我们有必要总结一些常用方式。
一、引入依赖
首先,我们需要引入数据校验所需要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
引入该starter之后,我们就能很轻松的完成数据校验工作,只需要在想要校验的实体类的字段上加上对应的校验注解即可对该字段进行校验规则设置。
@Getter
@Setter
@TableName("pms_brand")
@ApiModel(description = "品牌表")
public class PmsBrand implements Serializable {
private static final long serialVersionUID = -21640018136618420L;
@TableId(value = "brand_id", type = IdType.ASSIGN_ID)
@ApiModelProperty("品牌id")
private Long brandId;
@NotBlank(message = "品牌名不能为空")
@ApiModelProperty("品牌名")
private String name;
@URL(message = "需要是一个合法的URL")
@NotBlank(message = "logo地址不能为空")
@ApiModelProperty("品牌logo")
private String logo;
}
到这里我们就完成了初步的校验规则设置工作。
此处需要特别注意的是:@URL校验注解只在该字段有值时有效,没值或者为空时无效,所以,如果我们想在该字段没值或者为空时也进行校验则可以另外添加一个 @NotBlank 注解
二、开启校验功能
开启校验功能只需要在Controller层方法中的需要校验的实体参数前添加一个 @Valid 注解即可
@PostMapping("/insert")
public R insert(@Valid @RequestBody PmsBrand pmsBrand) {
return R.ok(this.pmsBrandService.save(pmsBrand));
}
至此,我们就完成了数据校验的基本实现配置,也就是说,配置到这里,我们就能实现数据校验了,但是,当数据校验过程中发现数据校验不通过而抛出异常时,我们并不能很好的知道哪些数据校验失败,以及校验失败的信息是啥?为了解决这个问题,我们通常可以使用基于AOP的全局异常处理来帮助我们进行一些异常信息的处理。
三、全局异常处理
@Slf4j
@RestControllerAdvice(basePackages = "com.boot.shop.controller")
public class ValidationExceptionHandler {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R validationException(MethodArgumentNotValidException e){
log.error("出现了数据校验异常,异常类型为:{}.异常信息为:{}",e.getClass(),e.getMessage());
// 从特定异常中拿到异常结果集
BindingResult bindingResult = e.getBindingResult();
// 从异常结果集中拿到字段校验异常信息
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
Map<String, String> errorMap = new HashMap<>();
fieldErrors.forEach(error -> errorMap.put(error.getField(),error.getDefaultMessage()));
System.out.println(errorMap.size());
return R.fail(errorMap,"数据校验失败!");
}
}
需要注意的几点是:1、建议要在@RestControllerAdvice注解中添加包扫描路径
2、对于数据校验异常推荐将@ExceptionHandler异常处理器的目标异常设置为MethodArgumentNotValidException.class,因为这个异常对象中封装了数据校验的一些信息。
四、分组校验
场景:当我们插入一条数据时,我们可能需要对很多字段进行校验,但在修改时,我们可能只需要对修改的相关字段进行校验,此外,也有可能存在,添加时需要校验而修改不需要校验等情况,所以,我们应该针对不同的操作场景使用不同的校验规则,以此来确保精准校验。
首先,如下图新建两个校验分组,这两个分组都是空接口,只是为了作为分组标识进行区分。没有实际内容。
然后,基于不同的操作,设置字段在不同操作场景下的归属的分组,设置好分组之后,
@Getter
@Setter
@TableName("pms_brand")
@ApiModel(description = "品牌表")
public class PmsBrand implements Serializable {
private static final long serialVersionUID = -21640018136618420L;
@NotNull(message = "修改时id必须指定",groups = {Update.class})
@Null(message = "新增时id不能指定,因为id自增",groups = {Insert.class})
@TableId(value = "brand_id", type = IdType.AUTO)
@ApiModelProperty("品牌id")
private Long brandId;
@NotBlank(message = "品牌名不能为空",groups = {Insert.class, Update.class})
@ApiModelProperty("品牌名")
private String name;
@URL(message = "需要是一个合法的URL",groups = {Insert.class, Update.class})
@NotBlank(message = "logo地址不能为空")
@ApiModelProperty("品牌logo")
private String logo;
}
在对实体类对应的字段设置完分子之后,在Controller层方法的校验实体类前加上注解@Validated
并指定分组为Insert ;至此,当新增数据时,将使用insert新增分组校验规则,代码如下:
@PostMapping("/insert")
public R insert(@Validated(value = {Insert.class}) @RequestBody PmsBrand pmsBrand) {
return R.ok(this.pmsBrandService.save(pmsBrand));
}
特别注意:一旦我们在注解@Validated中指定了校验分组,那么对于哪些没有指定校验分组的字段将不再生效。例如下面的这个字段,@NotBlank(message = "logo地址不能为空") 校验由于没有指定校验分组,而@Validated又指定了Insert分组,所以,该校验无效。
@URL(message = "需要是一个合法的URL",groups = {Insert.class, Update.class})
@NotBlank(message = "logo地址不能为空")
@ApiModelProperty("品牌logo")
private String logo;
五、自定义校验
首先,我们需要知道的是,几乎所有的校验都是基于JSR303规则实现的,所以,如果我们想要自定义一个校验注解,我们也必须遵循JSR303的校验规则。至于该如何自定义,我们可以参考已有的校验注解实现。例如 @NotBlank
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {
String message() default "{javax.validation.constraints.NotBlank.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
可以看到的是,该校验注解除了一些元注解外,还包含了一个@Constraint(validatedBy = { })特殊的注解,起作用就是为了方便进行自定义校验实现的。我们是需要将我们自定实现的校验器传入其中就能够实现自定义校验。
想要实现自定义校验需要进行3个步骤的操作:
1)添加一个自定义的校验注解
2)编写一个自定义的校验器
3)将自定义检验注解和自定义校验器关联
那么,现在我们进行第一步,添加一个自定义校验注解:
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.boot.shop.annotation.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
// 自定义属性
int[] value() default { };
}
为了能够使校验失败时,提示我们自定义的提示信息,我们可以自己指定默认的message取值路径,如上图com.boot.shop.annotation.ListValue.message我就是取的自己定义的值,同时该值的配置需要符合JSR303的配置规范,也就是,我们需要在resource目录下创建一个校验提示信息配置文件,如下图所示:
此外,如果存在properties配置文件乱码情况时,可以如下配置:
第二步、编写一个对应的校验器,在编写之前,我们先看一下@Constraint(validatedBy = { })该注解需要我们传入一个什么样的校验器,源码如下:
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
/**
* 实现约束的 ConstraintValidator 类。
* 给定的类必须为给定的 ValidationTarget 引用不同的目标类型。
* 如果两个 ConstraintValidators 引用同一个类型,就会发生异常。
* 最多接受一个针对方法或构造函数的参数数组(也称为交叉参数)的 ConstraintValidator。
* 如果存在两个或更多,则会发生异常。
*
* 返回:实现约束的 ConstraintValidator 类数组
*/
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
可以看到的是,我们传入的是一个ConstraintValidator类型的类对象,但ConstraintValidator本身是一个接口,所以,我们需要传入一个他的实现类。在这个实现类中实现他的校验方法isValid
ConstraintValidator源码如下,值得注意的是,在实现该接口时,我们需要指定校验器关联的校验注解和校验的数据类型
public interface ConstraintValidator<A extends Annotation, T> {
/**
* Initializes the validator in preparation for
* {@link #isValid(Object, ConstraintValidatorContext)} calls.
* The constraint annotation for a given constraint declaration
* is passed.
* <p>
* This method is guaranteed to be called before any use of this instance for
* validation.
* <p>
* The default implementation is a no-op.
*
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
default void initialize(A constraintAnnotation) {
}
/**
* Implements the validation logic.
* The state of {@code value} must not be altered.
* <p>
* This method can be accessed concurrently, thread-safety must be ensured
* by the implementation.
*
* @param value object to validate
* @param context context in which the constraint is evaluated
*
* @return {@code false} if {@code value} does not pass the constraint
*/
boolean isValid(T value, ConstraintValidatorContext context);
}
第三步、实现一个校验器
public class CustomeConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private final Set<Integer> values = new HashSet<>();
// 该初始化方法将自定义注解的详细信息封装成constraintAnnotation对象,从中我们可以取到一些我们想要的信息,比如注解的数据值
@Override
public void initialize(ListValue constraintAnnotation) {
for (int value : constraintAnnotation.value()) {
values.add(value);
}
}
// 数据校验
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return values.contains(value);
}
}
编写完校验器后,将检验注解和校验器关联
最后,我们就可以在想要校验的字段上使用注解进行校验规则约束了。使用如下:
@Getter
@Setter
@TableName("pms_brand")
@ApiModel(description = "品牌表")
public class PmsBrand implements Serializable {
private static final long serialVersionUID = -21640018136618420L;
@NotNull(message = "修改时id必须指定",groups = {Update.class})
@Null(message = "新增时id不能指定,因为id自增",groups = {Insert.class})
@TableId(value = "brand_id", type = IdType.AUTO)
@ApiModelProperty("品牌id")
private Long brandId;
@NotBlank(message = "品牌名不能为空",groups = {Insert.class, Update.class})
@ApiModelProperty("品牌名")
private String name;
@URL(message = "需要是一个合法的URL",groups = {Insert.class, Update.class})
@NotBlank(message = "logo地址不能为空")
@ApiModelProperty("品牌logo")
private String logo;
@ListValue(value = {0,1},groups = {Insert.class})
@ApiModelProperty("0:显示、1:隐藏")
private Integer showStatus;
}
效果如下:
总结
哈哈哈,继续加油吧!少年