在项目中如何自定义字段校验注解,扩展javax的validation-api?


highlight: xcode

theme: vuepress

概述

如本篇文章标题所述,本篇文章要给大家介绍如何扩展 javax 的 validation-api。为什么要扩展它原有的注解呢,那肯定是因为原有的注解无法满足业务需求嘛。

准备工作

首先我们需要导入几个依赖包:

```xml javax.validation validation-api 2.0.1.Final

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version>
</dependency>

```

@RangeIn注解

在某次需求开发中,我突然发现 Hibernate 的 validator 库的 @Range 注解太局限了,只能限定最小值和最大值,而不能限定枚举值。如下所示:

java @Range(min = 0, max = 10) private Integer amount;

这样的写法表示 amount 字段只能最小为 0,最大为 10。如果我想让这个字段的值限定在某个集合中那就不行了。于是我想到了开发自定义注解,比如命名为 @RangeIn

@RangeIn 注解:

```java package org.codeart;

import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;

@Target({ ElementType.FIELD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = { RangeInValidator.class }) // 指定校验器 public @interface RangeIn {

long[] value() default {};

// 默认的错误消息
String message() default "value must range in the given array";

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

// 负载信息
Class<? extends Payload>[] payload() default {};

} ```

long[] 类型的 value 属性表示字段的值限定在某个数组之中。这个注解的顶部使用了 @Constraint 注解修饰,内部 validatedBy 属性指定了由那个类来处理这个注解。注意它是一个数组,说明可以指定多个校验器。

元注解 @Target 的参数为 ElementType.FIELD, ElementType.TYPE_USE 表示它可以修饰成员变量以及泛型的类型参数。

实现校验器类

下一步需要声明校验器 RangeInValidator 类,这是官方固定写法不要问我为什么。

RangeInValidator 校验器(注解解析器)定义如下:

```java package org.codeart;

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.Arrays;

public class RangeInValidator implements ConstraintValidator {

private long[] values;

@Override
public void initialize(RangeIn annotation) {
    this.values = annotation.value();
}

@Override
public boolean isValid(Number number, ConstraintValidatorContext context) {
    long fieldVal = number.longValue();
    for (long value : values) {
        if (value == fieldVal) {
            return true;
        }
    }
    context.disableDefaultConstraintViolation();
    context.buildConstraintViolationWithTemplate(getMessage()).addConstraintViolation();
    return false;
}

private String getMessage() {
    return "field value must range in the given array: " + Arrays.toString(values);
}

} ```

这个校验器类的成员属性中有 long[] 类型的 value 字段,用来接收注解的 value 字段的值。这个校验器类实现了 ConstraintValidator 接口,需要实现内部的抽象方法 isValid。这个方法的返回值表示是否校验通过。

通过重写 initialize 方法,我们拿到注解对象。因为在声明类的时候指定了 ConstraintValidator 的泛型是 <RangeIn, Number>,所以我们可以直接设置方法的参数是 RangIn 类型,从而获取注解的 value 的字段值赋值给成员变量。

isValid 中,因为设置了接口泛型第二个类型使用(type use)参数是 Number 类型,所以 isValid 第一个参数可以直接设置为 Number 类型,从而获取需要校验的字段的值,然后再进行校验。

成员变量 value 已经赋值,然后现在开始遍历这个数组,在数组中查找是否存在需要校验字段的值。假如存在那么就返回 true,否则返回 false

最后调用 ConstraintValidatorContext#disableDefaultConstraintViolation 方法表示禁用默认消息,使用自定义的返回错误消息。

context.buildConstraintViolationWithTemplate(getMessage()).addConstraintViolation(); 这一段表示生成自定义的错误消息模板,最后添加到 ConstraintValidatorContext 内部的集合中去,这个由实现类实现。

测试一下

在运行之前先声明一个实体类 Person

```java package org.codeart.pojo;

import lombok.Data; import org.codeart.validator.RangeIn;

import java.util.List;

@Data public class Person {

private String name;

@RangeIn({10, 20, 50})
private Integer age;

private List<@RangeIn({8, 88, 888}) Integer> luckNumbers;

} ```

Person 的字段添加了我们自定义的字段校验注解。

运行测试一下:

```java package org.codeart;

import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;

import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.util.ArrayList; import java.util.List; import java.util.Set;

public class Main {

public static void main(String[] args) {
    // 创建校验器工厂类
    ValidatorFactory factory = Validation.byDefaultProvider()
                                         .configure()
                                         .messageInterpolator(new ParameterMessageInterpolator())
                                         .buildValidatorFactory();
    Validator validator = factory.getValidator();

    Person person = new Person();
    person.setAge(15);
    person.setName("Trump");
    List<Integer> luckNumbers = new ArrayList<>();
    luckNumbers.add(999);
    luckNumbers.add(666);
    person.setLuckNumbers(luckNumbers);

    // 输入校验信息
    Set<ConstraintViolation<Person>> violations = validator.validate(person);
    for (ConstraintViolation<Person> violation : violations) {
        System.out.println(violation.getMessage());
    }
}

} ```

结果如下:

image.png

因为 Personage 字段设置为了 15 所以不在限定值集合中。luckNumbers 添加了 999 和 666,也不在限定集合中,所以也触发了校验失败。

注意小坑

在导入依赖的时候,假如你没有导入 hibernate-validator,会提示一个错误,没有服务提供者。因为 javax.constraint 是一个规范只声明了接口和注解,但是并没有实现,所以需要实现的库。

image.png

结合SpringBoot使用

当然最后肯定要将注解应用到 SpringBoot 的 Restful 接口中去,现在来定义一个接口来接收一个请求体参数并校验:

```java @RestController public class TestController {

@PostMapping("/person")
public String addPerson(@Valid @RequestBody Person person) {
    System.out.println(person);
    return "ok";
}

} ```

使用 ApiPost 简单测试一下:

image.png

果然报错了:

image.png

被校验住了。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值