Java请求日期验证:对于任何具有两个可变参数的类



我正在为请求日期约束创建一个共享组件,开始日期在结束日期之前。我想接受我当前的Validation请求,并使其通用,所以我键入(任何类的Begin和EndDate类成员(,它就会起作用。如何做到这一点?我在下面的ProductRequest中使用请求类上方的注释。

注意:如何在注释中设置开始日期和结束日期参数;它们可能并不总是";"开始/结束";现场成员,有时他们可能是";"开始/完成";在另一个班级。

@DatesRequestConstraint
public class ProductRequest {
private Long productId;
private DateTime startDate;
private DateTime EndDate;
private List<String> productStatus;
}
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ProductValidator.class)
@Documented
public @interface DatesRequestConstraint {
String message() default "Invalid dates request.";
Class <?> [] groups() default {};
Class <? extends Payload> [] payload() default {};
}
public class ProductValidator implements ConstraintValidator<DatesRequestConstraint, ProductRequest> {
@Override
public void initialize(DatesRequestConstraint constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(ProductRequest productRequest, ConstraintValidatorContext constraintValidatorContext) {
if (productRequest.getStartDate() != null && 
productRequest.getEndDate() != null && 
productRequest.getStartDate().isAfter(productRequest.getEndDate())) {
return false;
}
else return true;
}

您可以:

  1. 实现ConstraintValidator<DatesMatch, Object>,以便可以在任何类型上应用@DatesMatch注释
  2. 将自定义String字段添加到@DatesMatch注释中,您可以在其中指定要验证的字段的名称
  3. 在运行时使用反射可以按字段值的指定名称访问字段值

这里有一个类似的在多个自定义字段上进行类级验证的例子:Baeldung:Spring MVC custom validation(向下滚动到"9"。自定义类级别验证"(。

根据您的示例进行定制,这样的东西应该可以工作:

@Constraint(validatedBy = DatesMatchValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DatesMatch {
String message() default "The dates don't match.";
String startField();
String endField();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface List {
DatesMatch[] value();
}
}
// Accept a list of items so that you can validate more than one pair of dates on the same object if needed
@DatesMatch.List({
@DatesMatch(
startField = "startDate",
endField = "endDate",
message = "The end date must be after the start date."
)
})
public class ProductRequest {
private Long productId;
private Instant startDate;
private Instant endDate;
private List<String> productStatus;
/* Getters and setters omitted */
}
public class DatesMatchValidator implements ConstraintValidator<DatesMatch, Object> {
private String startField;
private String endField;
public void initialize(DatesMatch constraintAnnotation) {
this.startField = constraintAnnotation.startField();
this.endField = constraintAnnotation.endField();
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
Instant startFieldValue = (Instant) new BeanWrapperImpl(value)
.getPropertyValue(startField);
Instant endFieldValue = (Instant) new BeanWrapperImpl(value)
.getPropertyValue(endField);
if (startFieldValue == null || endFieldValue == null) {
return true;
}
return endFieldValue.isAfter(startFieldValue);
}
}

更新:(回应评论(:

这个答案很好,允许多对日期,但键入字符串不安全,用户可以在产品字段中键入任何字段

实现ConstraintValidator<DatesMatch, Object>是一个简单的包罗万象的解决方案,可以应用于任何类。

但是,您绝对可以用一种更安全的方式来实现,方法是为要验证的每个类型(即ConstraintValidator<DatesMatch, ProductRequest>ConstraintValidator<DatesMatch, AnotherRequest>…(实现一个单独的ConstraintValidator,然后在@Constraint(validatedBy={...})属性中指定所有类型:

@Constraint(validatedBy = {ProductRequestDatesMatchValidator.class, AnotherRequestDatesMatchValidator.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DatesMatch {
String message() default "Invalid dates request.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@DatesMatch(message = "Start and end dates do not match!")
public class ProductRequest {
private Long productId;
private Instant startDate;
private Instant endDate;
private List<String> productStatus;
/* Getters and setters omitted */
}
@DatesMatch(message = "Begin and finish dates do not match!")
public class AnotherRequest {
private Long productId;
private Instant beginDate;
private Instant finishDate;
private List<String> productStatus;
/* Getters and setters omitted */
}
public class ProductRequestDatesMatchValidator implements ConstraintValidator<DatesMatch, ProductRequest> {
@Override
public boolean isValid(ProductRequest value, ConstraintValidatorContext context) {
// No need to cast here
Instant startDate = value.getStartDate();
Instant endDate = value.getEndDate();
// You could reuse this logic between each implementation by putting it in a parent class or a utility method
if (startDate == null || endDate == null) {
return true;
}
return startDate.isBefore(endDate);
}
}
public class AnotherRequestDatesMatchValidator implements ConstraintValidator<DatesMatch, AnotherRequest> {
@Override
public boolean isValid(AnotherRequest value, ConstraintValidatorContext context) {
Instant beginDate = value.getBeginDate();
Instant finishDate = value.getFinishDate();
if (beginDate == null || finishDate == null) {
return true;
}
return beginDate.isBefore(finishDate);
}
}

但是,请注意,这仍然不是编译时类型安全的,因为您可以将@DatesMatch注释放在尚未编写实现的类上,并且验证只会在运行时失败。

(使用注释处理可以实现编译时类型的安全性,但这是另一个主题。(

您可以使用自定义注释来注释startDate和endDate,例如:

@StartDateField
private DateTime startDate;
@EndDateField
private DateTime endDate;

然后,在isValid()中,您可以通过对所有类字段(在您的情况下,是所有ProductRequest字段(进行迭代并检查以下内容,通过它们的注释来访问startDate和endDate字段:

field.isAnnotationPresent(StartDateField.class)
field.isAnnotationPresent(EndDateField.class)

完整的代码可能如下:

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.util.Arrays;
import java.util.List;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ ANNOTATION_TYPE.TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = ProductValidator.class)
@Documented
@interface DatesRequestConstraint {
String message() default "Invalid dates request.";
Class <?> [] groups() default {};
Class <? extends Payload> [] payload() default {};
}
@DatesRequestConstraint
class ProductRequest {
private Long productId;
@StartDateField
private DateTime startDate;
@EndDateField
private DateTime EndDate;
private List<String> productStatus;
}
@Target({ ElementType.FIELD })
@Retention(RUNTIME)
@Documented
@interface StartDateField {
}
@Target({ ElementType.FIELD })
@Retention(RUNTIME)
@Documented
@interface EndDateField {
}
public class ProductValidator implements ConstraintValidator<DatesRequestConstraint, Object> {
@Override
public void initialize(DatesRequestConstraint constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(Object requestObject, ConstraintValidatorContext constraintValidatorContext) {
DateTime startDate = getDateFieldByAnnotation(requestObject, StartDateField.class);
DateTime endDate = getDateFieldByAnnotation(requestObject, EndDateField.class);
if (startDate != null &&
endDate != null &&
startDate.isAfter(endDate)) {
return false;
} else return true;
}
private DateTime getDateFieldByAnnotation(Object requestObject, Class<? extends Annotation> annotationClass) {
return Arrays.stream(requestObject.getClass().getDeclaredFields()).filter(field -> field.isAnnotationPresent(annotationClass)).map(field -> {
try {
return field.get(requestObject);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}).map(DateTime.class::cast).findAny().orElse(null);
}
}

最新更新