简体   繁体   中英

Best practice for using date parameters in Spring controllers?

After written several backend APIs, I found that the following code duplicates in almost every method which needs to filter data by dates:

@GetMapping(value="/api/test")
@ResponseBody
public Result test(@RequestParam(value = "since", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate since,
                   @RequestParam(value = "until", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate until) {
    // Date validation code I want to eliminate
    since = ObjectUtils.defaultIfNull(since, DEFAULT_SINCE_DATE);
    until = ObjectUtils.defaultIfNull(until, LocalDate.now().plusDays(1));
    if(since.isAfter(until)) {
        throw new SinceDateAfterUntilDateException();
    }

    // Do stuff
}

Obviously this is some kind of code smell. But, since I do need to validate since and until before using them to query the service/DAO, I am not sure where should I extract these code to?

Any advice?

  1. Implement org.springframework.core.convert.converter.Converter; interface.
  2. Register with spring conversion service.
  3. Use in you controller Sharing sample code below :
 public class MyCustomDateTypeConverter implements Converter<String, LocalDate> { @Override public LocalDate convert(String param) { //convert string to DateTime return dateTiemObjectCreatedfromParam; } }
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">  <property name="converters">    <list>
        <bean class="com.x.y.z.web.converters.MyCustomDateTypeConverter"/>  </list>     </property> 
</bean>


<mvc:annotation-driven conversion-service="conversionService">

</mvc:annotation-driven>


public Result test(LocalDate since,LocalDate until) {

    since = ObjectUtils.defaultIfNull(since, DEFAULT_SINCE_DATE);
    until = ObjectUtils.defaultIfNull(until, LocalDate.now().plusDays(1));
    if(since.isAfter(until)) {
        throw new SinceDateAfterUntilDateException();
    }

    // Do stuff
}

As ol' gud-procedural approach suggests:

public Result test(@RequestParam(value = "since", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate since,
               @RequestParam(value = "until", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate until) {
     checkInputDates();
     // Do stuff
}

private checkInputDates(LocalDate since, LocalDate until) {
    since = ObjectUtils.defaultIfNull(since, DEFAULT_SINCE_DATE);
    until = ObjectUtils.defaultIfNull(until, LocalDate.now().plusDays(1));
    if(since.isAfter(until)) {
        throw new SinceDateAfterUntilDateException();
    }
}
//so on..

If you have request params received from object then you can do it by using Bean level Validation(JSR 303) and custom Date Deserializer by extending Jackson serializers. This way you dont have check the params for null.

public class yourBeanName{
 public LocalDate since;
 public LocalDate until;

  @JsonDeserialize(using = CustomDateDeserializer.class)
   public void setSince(LocalDate since) {
    this.since = since;
   }

    // same for until
}

 // Custom jackson Desealizer

  @Component
  public class CustomDateDeserializer extends StdDeserializer<Date> {
    @Override
    public Date deserialize(JsonParser jsonparser, DeserializationContext 
  context)
    throws IOException, JsonProcessingException {
    // Here check the date for null and assign default with your dateTimeFormat
    }

 }

I would suggest a model type holding the parameters since and until with a custom bean validation (using Lombok but you can also write getters and setters). The defaults are now field initializers:

@Ordered({"since", "until"})
@Data
public class DateRange {
  @NotNull
  @PastOrPresent
  private LocalDate since = DEFAULT_SINCE_DATE;
  @NotNull
  private LocalDate until = LocalDate.now().plusDays(1);
}

@GetMapping(value="/api/test")
@ResponseBody
public Result test(@Valid DateRange dateFilter) {
    // Do stuff
}

To make the validation work, you need a custom bean validation constraint:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = OrderedValidator.class)
public @interface Ordered {
    /** The property names with comparable values in the expected order. **/
    String[] value();

    String message() default "{com.stackoverflow.validation.Ordered.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

And the validator to check the constraint (sorry, litte generics hell to make it work for any kind of Comparable values instead of LocaleDate only):

public class OrderedValidator implements ConstraintValidator<Ordered, Object>
{
    private String[] properties;

    @Override
    public void initialize(Ordered constraintAnnotation) {
        if (constraintAnnotation.value().length < 2) {
            throw new IllegalArgumentException("at least two properties needed to define an order");
        }
        properties = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return isValid(value));
    }

    private <T extends Comparable<? super T>> boolean isValid(Object value)
    {
        List<T> values = getProperties(value);
        return isSorted(values);
    }

    private <T extends Comparable<? super T>> List<T> getProperties(Object value)
    {
        BeanWrapperImpl bean = new BeanWrapperImpl(value);
        return Stream.of(properties)
            .map(bean::getPropertyDescriptor)
            .map(pd -> this.<T>getProperty(pd, value))
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }

    // See https://stackoverflow.com/a/3047160/12890
    private <T extends Comparable<? super T>> boolean isSorted(Iterable<T> iterable) {
        Iterator<T> iter = iterable.iterator();
        if (!iter.hasNext()) {
            return true;
        }
        T t = iter.next();
        while (iter.hasNext()) {
            T t2 = iter.next();
            if (t.compareTo(t2) > 0) {
                return false;
            }
            t = t2;
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private <T extends Comparable<? super T>> T getProperty(PropertyDescriptor prop, Object bean) {
        try {
            return prop.getReadMethod() == null ? null : (T)prop.getReadMethod().invoke(bean);
        } catch (ReflectiveOperationException noAccess) {
            return null;
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM