7

This time I was working with Declarative REST Client, Feign in some Spring Boot App.

What I wanted to achieve is to call one of my REST API's, which looks like:

@RequestMapping(value = "/customerslastvisit", method = RequestMethod.GET)
    public ResponseEntity customersLastVisit(
            @RequestParam(value = "from", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date from,
            @RequestParam(value = "to", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date to) {

As You can see, the API accepts calls with from and to Date params formatted like (yyyy-MM-dd)

In order to call that API, I've prepared following piece of @FeignClient:

@FeignClient("MIIA-A")
public interface InboundACustomersClient {
    @RequestMapping(method = RequestMethod.GET, value = "/customerslastvisit")
    ResponseEntity customersLastVisit(
            @RequestParam(value = "from", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date from,
            @RequestParam(value = "to", required = true) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date to);
}

Generally, almost copy-paste. And now somewhere in my boot App, I use that:

DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
ResponseEntity response = inboundACustomersClient.customersLastVisit(formatter.parse(formatter.format(from)),
        formatter.parse(formatter.format(to)));

And, what I get back is that

nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.util.Date] for value 'Sun May 03 00:00:00 CEST 2015';

nested exception is java.lang.IllegalArgumentException: Unable to parse 'Sun May 03 00:00:00 CEST 2015'

So, the question is, what am I doing wrong with request, that it doesn't parse to "date-only" format before sending to my API? Or maybe it is a pure Feign lib problem?

patrykos91
  • 3,506
  • 2
  • 24
  • 30

5 Answers5

9

You should create and register a feign formatter to customize the date format

@Component
public class DateFormatter implements Formatter<Date> {

    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        return formatter.parse(text);
    }

    @Override
    public String print(Date date, Locale locale) {
        return formatter.format(date);
    }
}


@Configuration
public class FeignFormatterRegister implements FeignFormatterRegistrar {

    @Override
    public void registerFormatters(FormatterRegistry registry) {
        registry.addFormatter(new DateFormatter());
    }
}
Rafael Zeffa
  • 2,334
  • 22
  • 20
  • Solution works really well. Maybe a little additional info is going to be useful. By implementing thing feign-client solution, you don't need the DateTimeFormat annotation in the Feign interfaces. – Wim Van den Brande Jan 10 '18 at 07:53
  • This solution is wrong/dangerous imo. SimpleDateFormat is an outdated class that you should not use any longer in favour of [joda](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) or new [java](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) formatters. SimpleDateFormat is not thread-safe. If you want to use it, you should wrap it in a ThreadLocal. Spring components are singletons, thus 1 instance per JVM, meaning, possibly different threads accessing your SimpleDateFormat and corrupting it. – u6f6o Dec 12 '19 at 08:20
  • Furthermore, declaring this DateFormatter as a component, registers this class as the default formatter in your whole spring universe, overriding all default date formatters, spring provides. Given the use in the configuration class, this makes anyways no sense at all, since you create a new instance of the date formatter instead of injecting it and using the singleton. So either strip the the component annotation or make the class thread-safe by replacing SimpleDateFormat or wrapping it in a ThreadLocal. – u6f6o Dec 12 '19 at 08:25
1

Another simple solution is to use default interface method for Date to String conversion like

@RequestMapping(value = "/path", method = GET)
List<Entity> byDate(@RequestParam("date") String date);

default List<Entity> date(Date date) {
    return date(new SimpleDateFormat("dd.MM.yyyy").format(validityDate));
}
kapodes
  • 316
  • 2
  • 7
1

Here is a solution that is thread-safe and does not register a default date formatter in your spring universe. Keep in mind though, that this formatter will be used for all feign clients as new default and that you should actually use joda date time or the new java date time instead:

@Configuration
public class FeignFormatterRegister implements FeignFormatterRegistrar {

    @Override
    public void registerFormatters(FormatterRegistry registry) {
        registry.addFormatter(new DateFormatter());
    }

    /*
     * SimpleDateFormat is not thread safe!
     * consider using joda time or java8 time instead
     */
    private static class DateFormatter implements Formatter<Date> {
        static final ThreadLocal<SimpleDateFormat> FORMAT = ThreadLocal.withInitial(
                () -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
        );

        @Override
        public Date parse(String text, Locale locale) throws ParseException {
            return FORMAT.get().parse(text);
        }

        @Override
        public String print(Date date, Locale locale) {
            return FORMAT.get().format(date);
        }
    }
}
u6f6o
  • 2,050
  • 3
  • 29
  • 54
1

The feign client now (Dec 2020) works fine using the syntax in the original question above and a java.time.LocalDate as the parameter. That is, you can use:

  @FeignClient(name = "YOUR-SERVICE")
  interface ClientSpec {
    @RequestMapping(value = "/api/something", method = RequestMethod.GET)
    String doSomething(
        @RequestParam("startDate") 
        @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) 
        LocalDate startDate);
  }
Andy Brown
  • 11,766
  • 2
  • 42
  • 61
0

You can just use an expander object instead.

 /**
 * @see com.x.y.z.rest.configuration.SomeController#search(Integer, Integer, Date, Date)
 */
@RequestLine("GET /configuration/{customerId}?&startDate={startDate}")
Set<AnObject> searchSomething(@Param("customerId") Integer customerId, @Param(value = "startDate", expander = FeignDateExpander.class) Date startDate);


public class FeignDateExpander implements Param.Expander {

@Override
public String expand(Object value) {
    Date date = (Date)value;
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
}
}
Nanotron
  • 554
  • 5
  • 16