51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

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

1. 提出需求 {#1.-%E6%8F%90%E5%87%BA%E9%9C%80%E6%B1%82}

1.1 提出需求 {#1.1-%E6%8F%90%E5%87%BA%E9%9C%80%E6%B1%82}

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

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

1.2 解决思路 {#1.2-%E8%A7%A3%E5%86%B3%E6%80%9D%E8%B7%AF}

这个问题可以通过自定义注解来实现,先写一个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.-%E6%B3%A8%E8%A7%A3%E6%80%8E%E4%B9%88%E5%86%99}

2.1 注解加在变量上 {#2.1-%E6%B3%A8%E8%A7%A3%E5%8A%A0%E5%9C%A8%E5%8F%98%E9%87%8F%E4%B8%8A}

因为要实现的需求是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 = &amp;quot;date1&amp;quot;, &amp;quot;date3&amp;quot;
        
// date = date3

return true;

}

}


2.2 注解加到类上 {#2.2-%E6%B3%A8%E8%A7%A3%E5%8A%A0%E5%88%B0%E7%B1%BB%E4%B8%8A}

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

/**
 * @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 &quot;校验的日期必须早于指定的日期&quot;;

Class&amp;lt;?&amp;gt;[] groups() default { };

Class&amp;lt;? extends Payload&amp;gt;[] 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 = &quot;yyyy-MM-dd&quot;, timezone = &quot;GMT+8&quot;) private Date date1;

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

@JsonFormat(pattern = &quot;yyyy-MM-dd&quot;, timezone = &quot;GMT+8&quot;) 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.-%E6%9C%80%E7%BB%88%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95}

3.1 自定义注解 {#3.1-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3}

自定义注解就写好了,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 &quot;校验的日期必须早于指定的日期&quot;;

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

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

Class&amp;lt;?&amp;gt;[] groups() default { };

Class&amp;lt;? extends Payload&amp;gt;[] payload() default { };

}


3.2 自定义校验器 {#3.2-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%A1%E9%AA%8C%E5%99%A8}

需要说明的是

  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(&amp;quot;类 &amp;quot; + object.getClass().getName() + &amp;quot; 自定义注解使用有误: &amp;quot; + 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(&quot;Asia/Shanghai&quot;); // 要校验的日期转换为LocalDate LocalDate currentDate = checked.toInstant().atZone(shanghaiZone).toLocalDate(); // 要校验的日期转换为LocalDate LocalDate targetDate = target.toInstant().atZone(shanghaiZone).toLocalDate();

    return currentDate.isBefore(targetDate); }

}


3.3 自定义注解的使用 {#3.3-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3%E7%9A%84%E4%BD%BF%E7%94%A8}

使用就很简单了,注解加在类上,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 = &quot;yyyy-MM-dd&quot;, timezone = &quot;GMT+8&quot;) private Date date1;

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

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

}


3.4 测试一下 {#3.4-%E6%B5%8B%E8%AF%95%E4%B8%80%E4%B8%8B}

能看出来需求是实现了,但是报错内容并不清晰,还可以让报错更清晰一些,这里就先不讲了。


打完收工。

赞(1)
未经允许不得转载:工具盒子 » 使用自定义注解@BeforeThanTargetDate实现多日期字段校验