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 = &quot;date1&quot;, &quot;date3&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 "校验的日期必须早于指定的日期";
Class&lt;?&gt;[] groups() default { };
Class&lt;? extends Payload&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 = "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.-%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 "校验的日期必须早于指定的日期";
// 校验的日期
String checkedField();
// 被校验的指定的日期
String[] targetFields();
Class&lt;?&gt;[] groups() default { };
Class&lt;? extends Payload&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}
需要说明的是
-
泛型指定为Object而不是DemoDto,还是考虑到灵活性,注解不能局限于某个特定的类。
-
在initialize方法中获取checkedField和targetFields的变量名,用于在isValid方法里获取变量的值。
-
在isValid方法中第一个参数Object就是添加注解的bean对象,通过这个object获取想要的变量值。
-
有了变量名个bean对象怎么获取变量的值呢?反射,已经写在getDateByFieldName方法里了。
-
因为被校验的指定的日期,也就是targetFields是一个字符串数组,对其进行遍历,每一个变量都和checkedField的值做比较。
-
日期做比较的逻辑抽出到方法isBefore里,只要有一个被校验的日期不满足,就直接返回false结束循环。
-
日期的比较逻辑是在天这一精度下的。
/**
* @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(&quot;类 &quot; + object.getClass().getName() + &quot; 自定义注解使用有误: &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("Asia/Shanghai");
// 要校验的日期转换为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 = "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 测试一下 {#3.4-%E6%B5%8B%E8%AF%95%E4%B8%80%E4%B8%8B}
能看出来需求是实现了,但是报错内容并不清晰,还可以让报错更清晰一些,这里就先不讲了。
打完收工。