Custom request parameter conversion for Spring Cloud Feign clients

This article is going to cover a very specific piece with Spring Cloud OpenFeign, so if you don’t have any xp, I suggest you read my other interesting articles. 😉

In this one, I’m going to show you a quick and easy way on how the request parameter transformation can be adjusted with Spring Cloud OpenFeign, similarly how the regular Feign Param expanders work.

Use-case

A common use-case for parameter conversion is sending datetime information between services.

For example let’s say you have an API that’s capable of listing Products. And every API like that, it provides some level of filtering. For filtering, one frequent attribute is like a creation time, last updated time or something like that.

The API could look like the following:

GET /products?from=<datetime>

Where the <datetime> is some string representation of the from datetime value. If you deal with OffsetDateTime in your app, the string representation looks the following: 2021-11-09T20:15:05.894464900+01:00

That’s most probably not what you want in your API, the invocation would look like this:

GET /products?from=2021-11-09T20:15:05.894464900+01:00

Instead, you could switch to a millisec representation, like this:

GET /products?from=1636485381420

Much better right? We’ll look at how to achieve this with Spring Cloud OpenFeign.

Parameter expansion with regular Feign

With regular Feign, this is fairly straightforward.

The Feign client for this API could look the following:

public interface InventoryServiceClient
    @RequestLine("GET /products?from={from}")
    List<Products> listProducts(@Param(value = "from") OffsetDateTime from);
}

In the @RequestLine annotation, we specify the HTTP method, the URL and the query parameters. In the @Param annotation you can specify which parameter value shall be substituted to which placeholder in the query parameters.

Luckily with regular Feign, the conversion configuration from Object to String is easy. The @Param annotation has a second parameter, called expander. Using the expander, you can specify a class that’s converting the Object value to the String representation.

The expander class needs to implement the Param.Expander interface.

public class OffsetDateTimeToMillisExpander implements Param.Expander {
    @Override
    public String expand(Object value) {
        if (!OffsetDateTime.class.isAssignableFrom(value.getClass())) {
            throw new IllegalArgumentException("This expander is not supporting the type");
        }
        long millis = ((OffsetDateTime) value).toInstant().toEpochMilli();
        return "" + millis;
    }
}

The expander implementation is easy although not typesafe unfortunately. We have to deal with raw objects.

Therefore the implementation first checks if it’s an OffsetDateTime and then converts it to milliseconds and returns it as a string.

The Feign client looks like this:

public interface InventoryServiceClient
    @RequestLine("GET /products?from={from}")
    List<Products> listProducts(@Param(value = "from", expander = OffsetDateTimeToMillisExpander.class) OffsetDateTime from);
}

And magically it will work.

Transforming parameters with Spring Cloud OpenFeign

How to achieve the same functionality with Spring Cloud OpenFeign? The Feign client would use Spring MVC annotations and the @RequestParam annotation doesn’t have an expander.

You can’t set the usual Spring MVC formatters either which you use for the controllers, so what can we do?

The client would look like this:

@FeignClient(name = "inventory-service")
public interface InventoryServiceClient
    @GetMapping("/products")
    List<Products> listProducts(@RequestParam(value = "from") OffsetDateTime from);
}

This will simply generate the normal string representation from the OffsetDateTime: 2021-11-09T20:15:05.894464900+01:00. Not cool.

What we can do instead is to register a Formatter specifically for the Feign clients.

Let’s create the Formatter first.

public class OffsetDateTimeToMillisFormatter implements Formatter<OffsetDateTime> {
    @Override
    public OffsetDateTime parse(String text, Locale locale) throws ParseException {
        long millis = Long.parseLong(text);
        return OffsetDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC);
    }

    @Override
    public String print(OffsetDateTime object, Locale locale) {
        return "" + object.toInstant().toEpochMilli();
    }
}

Similar to the previous code except we have types now. The Formatter is a 2-way conversion interface. Transforming the object into a textual representation and vica-versa.

Where to set it? Spring Cloud OpenFeign provides an extension point as well to register Formatters, the FeignFormatterRegistrar.

Usage is easy, just create a configuration class that implements it and registers the Formatter.

@Configuration
public class FeignConfiguration implements FeignFormatterRegistrar {
    @Override
    public void registerFormatters(FormatterRegistry registry) {
        registry.addFormatter(new OffsetDateTimeToMillisFormatter());
    }
}

That’s it, and now the result is the expected behavio. Sending over milliseconds as a query parameter instead of the OffsetDateTime textual representation.

GET /products?from=1636485381420

Check out my other articles and make sure to follow me on social media for more. Also, you can check out my new Mastering microservice communication with Spring Cloud Feign course that covers the entire spectrum of Feign.

Leave a Reply

Your email address will not be published.