1. 注解@FutureOrPresent的使用
1.1 举个小栗子
在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 上强度
在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 问题来了
@FutureOrPresent注解用于校验日期字段是否在当前时间之后或等于当前时间,那么问题来了,今天的日期为什么会报错?
2. 今天的日期为什么会校验不过
2.1 找原因
看下源代码,校验是在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 怎么解决
真没想到@FutureOrPresent注解在校验时竟然比较的是两个Date类型的毫秒值,直呼一声好严谨。
但是如此严谨并不符合我的要求,我只需要在天这个精度下作校验就行了。
怎么做呢,我想到了三个办法。
手写一个if判断一下,能够实现但不够优雅。
自定义一个注解,优雅但我还是想使用@FutureOrPresent这个注解(自定义注解的方式参考以下这篇文章)。
https://tch.cool/archives/AjP11V6r 对@FutureOrPresent的校验方式作个修改,够优雅。
3. 解决办法
3.1 解决方法
从上文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 <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> 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<?, ?> 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 测试一下
还是这三个参数,这次都校验通过了。说明这种方式是可行的,接下来就是真正实现isValid这个方法了。
3.3 实现isValid方法
原理很简单,就是比较两个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没有校验通过,说明已经实现功能。
打完收工。