likes
comments
collection
share

从源码到实践:构建个性化Spring Boot参数校验器

作者站长头像
站长
· 阅读数 6

上篇文章介绍了 @Valid@Validated 的区别和参数校验的进阶使用,本文将介绍如何自定义一个参数校验约束注解和校验器。

看源码

想要自定义,先找个官方提供的注解看看它是怎么实现的,然后我们就照葫芦画瓢写一个呗。

葫芦就随便找个@NotNull 注解吧,看下它是怎么实现的,下面是@NotNull 注解的源码截图:

从源码到实践:构建个性化Spring Boot参数校验器 只是因为在@NotNull 注解多看了一眼,再也没有忘记@Constraint注解,很明显,@Constraint注解就是让@NotNull 校验生效的注解,那接下来我们就来看看@Constraint

从源码到实践:构建个性化Spring Boot参数校验器 @Constraint用于标注自定义约束注解。它有一个属性:

  • validatedBy:指定一个或多个实现了ConstraintValidator接口的验证器类,用于定义对应的验证逻辑。这个属性的值是一个Class数组,可以指定一个或多个验证器类。

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);
}

ConstraintValidator接口用于定义自定义约束注解的验证逻辑。它定义了两个泛型参数:第一个参数表示要验证的注解类型,第二个参数表示要验证的字段类型。

ConstraintValidator接口有两个方法:

  • initialize()方法: 这个方法在验证器初始化时调用,可以用于获取注解中的属性值,进行一些初始化操作。
  • isValid()方法:这是ConstraintValidator接口中最重要的方法,用于实际执行验证逻辑。在这个方法中编写验证规则的具体逻辑,判断字段值是否符合约束条件,并返回一个布尔值表示验证结果。

介绍了这么多,下面我们就来自定义一个约束注解和校验器。

实操

实际工作中我们可能会遇到这样的情况,添加用户时可能要校验性别字段传值是否在性别数组或者枚举中,以此来校验性别传递的数据是否正确,下面我们就以这个例子自定义一个参数校验器。

前戏

实操动手之前先要准备一些东西。

首先定义一个接口,实现该接口之后将数据放到集合中,方便校验时获取。

public interface EnumValid {
    List<Integer> validValues();
}

这里定义一个枚举GenderEnum,实现EnumValid接口把枚举值放入到集合中。

/**
 *  性别枚举
 * author: 公众号:索码理(suncodernote)
 */
@AllArgsConstructor
@Getter
public enum GenderEnum implements EnumValid{

    MALE(1),
    FEMALE(2),
    UNKNOWN(0),
    ;
    private final Integer gender;

    @Override
    public List<Integer> validValues() {
        return Arrays.stream(GenderEnum.values()).map(GenderEnum::getGender).collect(Collectors.toList());
    }
}

自定义约束注解

仿照@NotNull 注解定义一个约束注解InEnum,它用于约束枚举值字段必须在集合中。message属性表示校验失败时的提示语。

/**
 *  判断值是否在枚举中
 * author: 公众号:索码理(suncodernote)
 */
@Documented
@Constraint(validatedBy = {InEnumValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(InEnum.List.class)
public @interface InEnum {

    /**
     * 提示语
     * @return
     */
    String message() default "{site.suncodernote.validation.constraints.InEnum.message}";

    /**
     * 分组
     * @return
     */
    Class<?>[] groups() default { };

    /**
     * 枚举类
     * @return
     */
    Class<? extends EnumValid> value();

    Class<? extends Payload>[] payload() default { };

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        InEnum[] value();
    }
}

Class<? extends Payload>[] payload() default { } 一定要有,不管是否用到。payload是一种用于将额外信息传递到验证约束的机制。实际上,payload本身并不具有具体的功能,它只是一个用于携带额外信息的容器。 这里不过多介绍payload,感兴趣的可以自己试试。

自定义校验器

下面来自定义一个参数校验器InEnumValidator实现 ConstraintValidator接口,initialize初始化时将实现了EnumValid接口,并重写了validValues()方法的子类中的集合赋值给list属性,然后在isValid方法中获取被InEnum注解标记的字段的值,并判断该字段的值是否在list中。

/**
 *  是否在枚举中验证器
 * author: 公众号:索码理(suncodernote)
 */
public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {

    private List<Integer> list;

    @Override
    public void  initialize(InEnum constraintAnnotation) {
        Class<? extends EnumValid> value = constraintAnnotation.value();
        EnumValid[] enumConstants = value.getEnumConstants();
        list = enumConstants[0].validValues();
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return list.contains(value);
    }
}

测试

上面准备好了,下面来测试一下,依旧用UserBean,用InEnum注解标记 gender属性。

@Data
public class UserBean {

    @NotEmpty
    private String username;

    @Min(value = 18 )
    private Integer age;

    private String email;

    @InEnum(value = GenderEnum.class)
    private Integer gender;
}

定义一个访问接口:

@RestController
@RequestMapping("validation")
public class ValidationController {

    @GetMapping("user")
    public UserBean validUserBean(@Validated UserBean userBean) {
        System.out.println(userBean);
        return userBean;
    }
}

测试: 从源码到实践:构建个性化Spring Boot参数校验器 从测试结果中可以看到校验是成功的,message也是我们在InEnum注解中定义的message。为了友好的提示,我们可以在resources目录下新建一个ValidationMessages_zh_CN.properties 文件,文件内容如下:

site.suncodernote.validation.constraints.InEnum.message=不在枚举中

然后修改配置文件,加上如下配置:

# 国际化
# 默认名称,可以写多个,用逗号分隔
spring.messages.basename=ValidationMessages
spring.messages.encoding=UTF-8

以上步骤就是配置了参数校验的国际化信息,关于Springboot国际化操作可以参考我之前的文章。

接下来再测试一下,可以看到结果已经是我们配置的国际化信息的数据了。 从源码到实践:构建个性化Spring Boot参数校验器 到此就结束了。

总结

本文介绍了如何在Springboot中自定义参数校验,用好参数校验能帮助我们节省很多重复的校验逻辑。你发现了吗?在本文示例中,我们使用参数校验都是在Controller控制层进行校验的,在工作中并不是所有的校验都是在Controller控制层,那如果这样该怎么办呢?敬请关注,下篇文章将为你揭晓答案。