Tch
Tch
Published on 2024-12-12 / 16 Visits
0
0

使用自定义注解@BeforeThanTargetDate实现多日期字段校验

1. 提出需求

1.1 提出需求

有这样一个需求,在请求参数中有两个(或以上)的日期类型的数据,在收到请求后对日期类型数据做一个校验,要求某一个日期数据要早于另一个(或多个)日期数据。

就像下边这个例子,要求date3要早于date1和date2。

1.2 解决思路

这个问题可以通过自定义注解来实现,先写一个Dto用于接受这三个date数据。

@Data
public class DemoDto implements Serializable {

    private static final long serialVersionUID = 2251918778365338096L;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date1;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date2;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date3;
}

DemoDto写好了,接下来有个问题,要对一个Dto里的多个变量做校验,自定义注解应该怎么写?

2. 注解怎么写

2.1 注解加在变量上

因为要实现的需求是date3要早于date1和date2,所以很容易就想到把注解加到变量上。

/**
 * @author denchouka
 * @description 自定义用于字段的注解,校验的日期必须早于指定的日期
 * @date 2024/12/7 17:10
 */
@Documented
@Constraint(validatedBy = BeforeThanTargetDateValidatorField.class)
@Target({ ElementType.FIELD })
@Retention(RUNTIME)
public @interface BeforeThanTargetDateField {

    String message() default "校验的日期必须早于指定的日期";

    // 被校验的指定的日期
    String[] targetFields();

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

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

注解就写好了,targetFields是用于被校验的指定的日期,也就是date2和date3。

使用的时候就像这样。

@Data
public class DemoDto implements Serializable {

    private static final long serialVersionUID = 2251918778365338096L;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date1;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date2;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @BeforeThanTargetDateField(targetFields = {"date1", "date2"})
    private Date date3;
}

然后再来写自定义校验器,在initialize方法里获取了targetFields的值,也就是上面使用注解时指定的"date1"和"date2"。

date的值就是date3的值。

那么问题来了,现在date3的值有了,要和date1和date2作比较,date1和date2的值怎么取,在哪取?

只剩下一个参数ConstraintValidatorContext了,这是一个验证器的上下文信息,主要用于定制错误消息或添加额外的约束违规。好像也没办法取到校验的对象,从而取到date1和date2的值。看来此路不通(谁有办法可以告诉我)。

/**
 * @author denchouka
 * @description 注解BeforeThanTargetDateField的自定义验证器
 * @date 2024/12/7 17:15
 */
public class BeforeThanTargetDateValidatorField implements ConstraintValidator<BeforeThanTargetDateField, Date> {

    // 被校验的指定的日期
    private String[] targetFields;

    @Override
    public void initialize(BeforeThanTargetDateField constraintAnnotation) {
        targetFields = constraintAnnotation.targetFields();
    }

    @Override
    public boolean isValid(Date date, ConstraintValidatorContext context) {

        // targetFields = "date1", "date3"
                
        // date = date3
        
        return true;
    }
}

2.2 注解加到类上

一个用于类的注解就写好了。

/**
 * @author denchouka
 * @description 自定义用于类的注解,校验的日期必须早于指定的日期
 * @date 2024/12/7 17:10
 */
@Documented
@Constraint(validatedBy = BeforeThanTargetDateValidator.class)
@Target({ ElementType.TYPE })
@Retention(RUNTIME)
public @interface BeforeThanTargetDateType {

    String message() default "校验的日期必须早于指定的日期";

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

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

使用的时候就像这样,加在类上就可以了。

/**
 * @author denchouka
 * @description TODO
 * @date 2024/12/7 17:28
 */
@Data
@BeforeThanTargetDateType
public class DemoDto implements Serializable {

    private static final long serialVersionUID = 2251918778365338096L;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date1;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date2;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date3;
}

还是一样再来写自定义校验器,把逻辑写在isValid方法里。

/**
 * @author denchouka
 * @description 注解BeforeThanTargetDateType的自定义验证器
 * @date 2024/12/7 17:15
 */
public class BeforeThanTargetDateValidatorType implements ConstraintValidator<BeforeThanTargetDateType, DemoDto> {


    @Override
    public void initialize(BeforeThanTargetDateType constraintAnnotation) {
        // 可省略
    }

    @Override
    public boolean isValid(DemoDto dto, ConstraintValidatorContext context) {

        Date date1 = dto.getDate1();

        Date date2 = dto.getDate2();

        Date date3 = dto.getDate3();

        // 作比较,省略

        return true;
    }
}

这样就可以了,但是还有个问题。

isValid方法里的校验逻辑写死了,需求实现了但是并不灵活。

这个自定义注解就只能给DemoDto这一个类用,而且只能校验date3早于date1和date2这一个条件。

3. 最终解决方法

3.1 自定义注解

自定义注解就写好了,checkedField用于指定校验的日期,就是date3。

targetFields用于指定被校验的指定的日期,就是date1和date2。

/**
 * @author denchouka
 * @description 自定义注解,校验的日期必须早于指定的日期
 * @date 2024/12/7 17:10
 */
@Documented
@Constraint(validatedBy = BeforeThanTargetDateValidator.class)
@Target({ ElementType.TYPE })
@Retention(RUNTIME)
public @interface BeforeThanTargetDate {

    String message() default "校验的日期必须早于指定的日期";

    // 校验的日期
    String checkedField();

    // 被校验的指定的日期
    String[] targetFields();

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

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

3.2 自定义校验器

需要说明的是

  1. 泛型指定为Object而不是DemoDto,还是考虑到灵活性,注解不能局限于某个特定的类。

  2. 在initialize方法中获取checkedField和targetFields的变量名,用于在isValid方法里获取变量的值。

  3. 在isValid方法中第一个参数Object就是添加注解的bean对象,通过这个object获取想要的变量值。

  4. 有了变量名个bean对象怎么获取变量的值呢?反射,已经写在getDateByFieldName方法里了。

  5. 因为被校验的指定的日期,也就是targetFields是一个字符串数组,对其进行遍历,每一个变量都和checkedField的值做比较。

  6. 日期做比较的逻辑抽出到方法isBefore里,只要有一个被校验的日期不满足,就直接返回false结束循环。

  7. 日期的比较逻辑是在天这一精度下的。

/**
 * @author denchouka
 * @description 注解BeforeThanTargetDate的自定义验证器
 * @date 2024/12/7 17:15
 */
public class BeforeThanTargetDateValidator implements ConstraintValidator<BeforeThanTargetDate, Object> {

    // 校验的日期
    private String checkedField;

    // 被校验的指定的日期
    private String[] targetFields;

    @Override
    public void initialize(BeforeThanTargetDate constraintAnnotation) {
        checkedField = constraintAnnotation.checkedField();
        targetFields = constraintAnnotation.targetFields();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {

        // 此处不用判断null,所有的Date类型数据都已做null检查

        try {
            // 获取校验的日期
            Date checkedDate = getDateByFieldName(object, checkedField);

            for(String targetField : targetFields) {
                // 获取指定日期
                Date targetDate = getDateByFieldName(object, targetField);

                // 作比较
                if (!isBefore(checkedDate, targetDate)) {
                    return false;
                }
            }

            return true;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new BusinessException("类 " + object.getClass().getName() + " 自定义注解使用有误: " + e.getMessage());
        }
    }

    /**
     * 使用反射获取对象中执行字段的值
     * @param object
     * @param fieldName
     * @return
     */
    private Date getDateByFieldName(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException {

            Field field = object.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return (Date) field.get(object);
    }

    /**
     * 判断校验的日期是否早于target
     * @param checked 校验的日期
     * @param target 注解指定的日期
     * @return
     */
    private boolean isBefore(Date checked, Date target) {
        // 只要有一个是null,就直接返回true(null会通过@NotNull注解校验)
        if (checked == null || target == null) {
            return true;
        }

        // 指定时区为Asia/Shanghai
        ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
        // 要校验的日期转换为LocalDate
        LocalDate currentDate = checked.toInstant().atZone(shanghaiZone).toLocalDate();
        // 要校验的日期转换为LocalDate
        LocalDate targetDate = target.toInstant().atZone(shanghaiZone).toLocalDate();

        return currentDate.isBefore(targetDate);
    }
}

3.3 自定义注解的使用

使用就很简单了,注解加在类上,checkedField指定校验的变量也就是date3,targetFields指定和date3作比较的date1和date2。

这样注解就能在别的地方使用,且使用方式比较灵活。

/**
 * @author denchouka
 * @description TODO
 * @date 2024/12/7 17:28
 */
@Data
@BeforeThanTargetDate(checkedField = "date3", targetFields = {"date1", "date2"})
public class DemoDto implements Serializable {

    private static final long serialVersionUID = 2251918778365338096L;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date1;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date2;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date date3;
}

3.4 测试一下

打完收工。


Comment