51工具盒子

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

如何修正@FutureOrPresent注解以支持天级精度校验

1. 注解@FutureOrPresent的使用 {#1.-%E6%B3%A8%E8%A7%A3%40futureorpresent%E7%9A%84%E4%BD%BF%E7%94%A8}

1.1 举个小栗子 {#1.1-%E4%B8%BE%E4%B8%AA%E5%B0%8F%E6%A0%97%E5%AD%90}

在Jakarta Bean Validation中,@FutureOrPresent注解用于校验日期字段是否在当前时间之后或等于当前时间。

在DemoDto中有个Date类型的变量date1,添加了校验注解@FutureOrPresent。

@Data
public class DemoDto implements Serializable {
private static final long serialVersionUID = 2251918778365338096L;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@FutureOrPresent(message = "date1必须是以后或现在的时间")
private Date date1;

}


随便写个接口测试一下这个注解。

@RestController
public class DemoController {
@PostMapping("/test")
public String test(@Validated @RequestBody DemoDto demoDto) {
    return "OK";
}

}


测试之前先看下当前时间,北京时间2024年12月1日17:28。

用"2024-12-2"测试了一下,返回了OK,结果符合预期。

1.2 上强度 {#1.2-%E4%B8%8A%E5%BC%BA%E5%BA%A6}

在DemoDto里新增了date2和date3,date1用来测试过去日期,date2测试当前日期,date3测试以后的日期。

@Data
public class DemoDto implements Serializable {
private static final long serialVersionUID = 2251918778365338096L;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@FutureOrPresent(message = "date1必须是以后或现在的时间")
private Date date1;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@FutureOrPresent(message = "date2必须是以后或现在的时间")
private Date date2;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@FutureOrPresent(message = "date3必须是以后或现在的时间")
private Date date3;

}


发送请求,看看结果(由于对数据校验异常MethodArgumentNotValidException设置了全局异常处理,所以只返回校验失败的message)。

  • date1:"2024-11-30"是过去的日期,报错了没问题。

  • date2: "2024-12-1"是今天的日期,报错了?有问题。

  • date3:"2024-12-2"是以后得日期,没报错没问题。

1.3 问题来了 {#1.3-%E9%97%AE%E9%A2%98%E6%9D%A5%E4%BA%86}

@FutureOrPresent注解用于校验日期字段是否在当前时间之后或等于当前时间,那么问题来了,今天的日期为什么会报错?

2. 今天的日期为什么会校验不过 {#2.-%E4%BB%8A%E5%A4%A9%E7%9A%84%E6%97%A5%E6%9C%9F%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BC%9A%E6%A0%A1%E9%AA%8C%E4%B8%8D%E8%BF%87}

2.1 找原因 {#2.1-%E6%89%BE%E5%8E%9F%E5%9B%A0}

看下源代码,校验是在AbstractInstantBasedTimeValidator的isValid方法里做的,value就是前边请求的参数date2的值("2024-12-1")。

/org/hibernate/validator/hibernate-validator/6.2.5.Final/hibernate-validator-6.2.5.Final.jar!/org/hibernate/validator/internal/constraintvalidators/bv/time/AbstractInstantBasedTimeValidator.class

点进去看看getInstant(value) ,在getInstant方法里,参数value仍旧是date2的值("2024-12-1")。

/org/hibernate/validator/hibernate-validator/6.2.5.Final/hibernate-validator-6.2.5.Final.jar!/org/hibernate/validator/internal/constraintvalidators/bv/time/futureorpresent/FutureOrPresentValidatorForDate.class

接着往下走,就是/jdk-17.jdk/Contents/Home/lib/src.zip!/java.base/java/time/Instant.java的ofEpochMilli方法。

参数epochMilli的值是1732982400000,其实就是date2("2024-12-1")的毫秒值。

ofEpochMilli方法就是用给定的一个毫秒值创建一个Instant对象。

到这就不看了,再回头去看referenceClock.instant() 。

其实是/jdk-17.jdk/Contents/Home/lib/src.zip!/java.base/java/time/Clock.java下的instant方法。

该方法返回的是一个当前时间的Instant。

最后这个compareTo方法就是对两个Instant作比较了。

返回的比较结果是-1。

再往下在/org/hibernate/validator/internal/constraintvalidators/bv/time/futureorpresent/AbstractFutureOrPresentInstantBasedValidator.java的方法isValid里,当入参为-1时,返回false。

到这里就知道为什么今天的日期校验不过了。

2.2 怎么解决 {#2.2-%E6%80%8E%E4%B9%88%E8%A7%A3%E5%86%B3}

真没想到@FutureOrPresent注解在校验时竟然比较的是两个Date类型的毫秒值,直呼一声好严谨。

但是如此严谨并不符合我的要求,我只需要在天这个精度下作校验就行了。

怎么做呢,我想到了三个办法。

  1. 手写一个if判断一下,能够实现但不够优雅。

  2. 自定义一个注解,优雅但我还是想使用@FutureOrPresent这个注解(自定义注解的方式参考以下这篇文章)。

    https://tch.cool/archives/AjP11V6r

  3. 对@FutureOrPresent的校验方式作个修改,够优雅。

3. 解决办法 {#3.-%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95}

3.1 解决方法 {#3.1-%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95}

从上文2.1可以看到,校验的内容都是写在方法isValid里的,所以只需要实现接口ConstraintValidator,并重写isValid方法就行了。

需要说明的是FutureOrPresent是校验的注解,Date是校验的数据类型。

isValid方法先默认返回true,具体的内容稍后再实现,先确认这个做法的可行性。

public class CustomConstraintValidator implements ConstraintValidator<FutureOrPresent, Date> {
@Override
public boolean isValid(Date date, ConstraintValidatorContext context) {
    return true;
}

}


为了使Spring使用自定义的验证器,需要在配置文件中指定它,可通过ConstraintValidatorFactory来实现。

在key是FutureOrPresentValidatorForDate时,替换成自定义的校验器CustomConstraintValidator。

/**
 * 通过自定义的 ConstraintValidatorFactory 替换默认的 FutureOrPresentValidatorForDate 验证器为 CustomConstraintValidator。
 * 当应用中使用 @FutureOrPresent 注解时,实际使用的是自定义验证器CustomConstraintValidator。
 */
class CustomConstraintValidatorFactory implements ConstraintValidatorFactory {
@Override
public &amp;lt;T extends ConstraintValidator&amp;lt;?, ?&amp;gt;&amp;gt; T getInstance(Class&amp;lt;T&amp;gt; key) {
    try {
        // FutureOrPresentValidatorForDate时替换成自定义校验器
        if (key.equals(FutureOrPresentValidatorForDate.class)) {
            return (T) new CustomConstraintValidator();
        }
        return key.getDeclaredConstructor().newInstance();
    } catch (Exception e) {
        // 抛出自定义异常,用于全局异常捕获
        throw new BusinessException(e.getMessage());
    }
}

@Override
public void releaseInstance(ConstraintValidator&amp;lt;?, ?&amp;gt; instance) {

}

}


然后自定义配置类,设置自定义的约束验证器工厂类CustomConstraintValidatorFactory。

@Configuration
public class ValidationConfig {
@Bean
public Validator validator() {
    ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 设置自定义的CustomConstraintValidatorFactory,用于创建约束验证器实例
            .constraintValidatorFactory(new CustomConstraintValidatorFactory())
            .buildValidatorFactory();

    return factory.getValidator();
}

}


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

还是这三个参数,这次都校验通过了。说明这种方式是可行的,接下来就是真正实现isValid这个方法了。

3.3 实现isValid方法 {#3.3-%E5%AE%9E%E7%8E%B0isvalid%E6%96%B9%E6%B3%95}

原理很简单,就是比较两个Date类型的数据了,实现如下。

public boolean isValid(Date date, ConstraintValidatorContext context) {
    // 指定时区为Asia/Shanghai
    ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
    // 获取当前日期的
    LocalDate now = LocalDate.now(shanghaiZone);
    // 要校验的日期转换为LocalDate
    LocalDate compare = date.toInstant().atZone(shanghaiZone).toLocalDate();
// 判断校验的日期是否是今天或以后
return !now.isAfter(compare);

}


可以看到now和compare转换为LocalDate是同一天。

还是同样的三个参数,现在只有date1没有校验通过,说明已经实现功能。


打完收工。



赞(2)
未经允许不得转载:工具盒子 » 如何修正@FutureOrPresent注解以支持天级精度校验