Custom Feign Client Builder library in Spring Boot

Feign is a declarative client which makes it easy to create an HTTP client by defining only Java Interfaces and endpoints as interface methods. Simple and straightforward easy-to-use client can be used as OpenFeign when you want to use it with Spring Boot.

But, there would be cases in your project where you might want to reuse the client in multiple services or modules such that you could maintain the REST service endpoints by versioning them via the release version of your library.

Feign provides an easy to either create a config-based initiation supporting auto-enabled configuration provided by Spring via @EnableFeignClients annotation. When using OpenFeign with Spring configuration can be done via the prefix:

feign.client.config.<client-name>.*

An example config application.yaml would look something like

feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

But, if you want to customize, Feign Builder can be used to construct Feign Clients beans as well. In order to create multiple custom beans in Spring, we could use one of  BeanDefinitionRegistryPostProcessor or ImportBeanDefinitionRegistrar and implement the required methods and annotate your class as @Configuration

Both of these configurations would let you use class BeanDefinitionRegistryPostProcessor which lets you register a bean as

registry.registerBeanDefinition("", createBeanDefination(feignClient));


private BeanDefinition createBeanDefination(Object client) {
    var definition = new RootBeanDefinition();
    definition.setBeanClass(client.getClass());
    definition.setInstanceSupplier(() -> client);

    return definition;
}

When using BeanDefinitionRegistryPostProcessor your configuration class would look something like this:

@Configuration
public class AppConfig implements BeanDefinitionRegistryPostProcessor {
 
    private BeanDefinition createBeanDefination(Object client) {
        var definition = new RootBeanDefinition();
        definition.setBeanClass(client.getClass());
        definition.setInstanceSupplier(() -> client);
 
        return definition;
    }
 
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // init builder
        var feignClient = Feign.builder().build();
 
        registry.registerBeanDefinition("", createBeanDefination(feignClient));
    }
 
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
 
    }
}

The ImportBeanDefinitionRegistrar would let you get metadata information from your Custom Enable Annotation in Spring. So, for example, if you want to initiate your clients via custom annotation and want to get annotation metadata we can do so like.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableCustomFeignClients {
    Class[] clients() default {};
}

@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
    Map attrs = annotationMetadata.getAnnotationAttributes(EnableCustomFeignClients.class.getName(),
            true);
    Class[] clients = attrs == null ? null : (Class[])attrs.get("clients");

    // init specific client classes

    // init builder
}

If you are writing a library, it's much better to let the configuration command the builder. The configuration mentioned in your application.yml with your custom prefix should govern which clients to initiate. This allows users of the library to configure only those clients which are required to be and only those would get initiated with your custom initiation configuration.

You can write your custom configuration with your prefix based on your needs.

mycompany.client.name.configX=1000
...


Problem: Cannot inject your Custom Configuration properties.

Since we are doing pre-initiation processing, the @ConfigurationProperties does not work here for us. The way to do this is to use Binder which can bind environment properties from application.yml configuration with prefixes to your custom DTOs.

Also in order to get Environment Object, we can make our @Configuration class EnvoirnmentAware.

@Configuration
public class AppConfig implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {

    private Environment environment;
    private Map config;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // init binder
        this.config = Binder.get(this.environment)
                .bind("myconfig.web.client", Bindable.mapOf(String.class, MyConfig.class))
                .orElseThrow(IllegalStateException::new);
    
        // init builders
        ...
    }

}

Now, let's talk about registering your @FeignClient annotated classes in your custom feign client registration configuration class.

To create the client, you can configure a few basic configurations listed here.

var feignClient = Feign.builder()
                .contract()
                .encoder()
                .decoder()
                .errorDecoder()
                .retryer()
                .target(, "url");

registry.registerBeanDefinition("clientName", createBeanDefination(feignClient));
  • Contract
  • Encoder
  • Decoder
  • ErrorDecoder
  • Retryer
  • Target

Contract

When using Spring, we can use class SpringMvcContract class which allows Spring to use its own MVC web annotations like @RequestMapping, @GetMapping, @PostMapping etc. on Feign client methods.

.contract(new SpringMvcContract())

However, you might end up in a situation where you want your client's endpoints uri too needs to be configurable. For this Spring already have placeholder resolution in place but this would not work directly out of the box when creating a custom client.

@FeignClient(name = "pet")
interface PetClient {

    @GetMapping(value = "${pet.getpets.url}")
    JsonNode getPets();
}

In order to resolve the placeholder like ${pet.getpets.url} we would have to set ResourceLoader it to our SpringMvcContract. We can easily do that by implementing ResourceLoaderAware class like this and then setting the ResourceLoader in our SpringMvcContract.  

@Configuration
public class AppConfig implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware {

    private ResourceLoader resourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Bean
    Contract springMvcContract() {
        var contract = new SpringMvcContract();
        contract.setResourceLoader(this.resourceLoader);

        return contract;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // init builder
        var feignClient = Feign.builder()
                .contract(springMvcContract())
                ...
    }
}

Encoder/Decoder

Based on your client config you can pick and set the required Encoder and Decoder for the client. For eg. if your content is application/json type, we can register the encoder and decoder as:

@Bean
Encoder feignEncoder() {
    var jsonMessageConverters = new MappingJackson2HttpMessageConverter(new ObjectMapper());
    return new SpringEncoder(() -> new HttpMessageConverters(jsonMessageConverters));
}

@Bean
Decoder feignDecoder() {
    var jsonMessageConverters = new MappingJackson2HttpMessageConverter(new ObjectMapper());
    return new ResponseEntityDecoder(new SpringDecoder(() -> new HttpMessageConverters(jsonMessageConverters)));
}

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    // init builder
    var feignClient = Feign.builder()
            .contract(springMvcContract())
            .encoder(feignEncoder())
            .decoder(feignDecoder())
            ...
}

ErrorDecoder

ErrorDecoder is where we can write about custom exception throwing for a handler method and then can handle it later in the code. However, when writing a library we might want the most generic way to handle exceptions. By default, Feign would throw FeignException whenever there is no 2XX response code.

Along with throwing exceptions, Feign also allows retry of exception marking via ErrorDecoder . If a method throws RetryException then instead of throwing it, first it invokes the Retryer. A default implementation of the error decoder is also available which can be directly hooked into your custom client builder.

Retryer with ErrorDecoder

Feign has given an interface to handle in case you would want to retry in specific cases. A simple custom retryer would look like this:

static class Retryer implements feign.Retryer {

    @Override
    public void continueOrPropagate(RetryableException e) {
        
    }

    @Override
    public feign.Retryer clone() {
        return null;
    }
}

Every single method invocation is wrapped around InvocationHandler where the client's Retry info is used to check they it is registered and do we want to retry the handler again. The method continueOrProgagate lets us hook logic if we want to continue to retry.

However, you might want to retry only in certain cases or probably on certain methods only in case of specific configured error codes. I did not find any direct way to configure retry per method inside the client feign client.

Problem: No feature to enable Retry on a single client method based on configuration by default.

In order to resolve that, I used ErrorDecoder with Custom Retryer as follows:

First, implement a custom exception that extends feign provided exception class RetryException. Here, we would want to capture information about the method by which the exception has occurred.

But before this, let us customize our configuration to allow per-method retry configuration. The property retryMethod would have this configuration per client and then per method.

static class MyConfig {
    private String url;
    private Logger.Level loggerLevel;
    private Integer connectTimeout;
    private Integer readTimeout;
    private RetryConfig retry; // client level retryconfig
    private Map<String, RetryConfig> retryMethod; // methodname-key : retryconfig
}

static class RetryConfig {
    private Integer maxAttempts;
    private Long period;
    private Long maxPeriod;
    private List<Integer> retryCodes = List.of(500, 502, ...);
}

An example of the config would look like

myconfig.web.client.petClient.getPets.maxAttempts=4

Extending the RetryException class:

static class MyRetryException extends RetryableException {

    private final RetryConfig retryConfig;
    public MyRetryException(RetryConfig retryConfig, int status, Request.HttpMethod httpMethod, Request request) {
        super(status, "", httpMethod, null, request);
        this.retryConfig = retryConfig;
    }
}

Here, we have captured information about the retry config, which we expect from our Custom Error Decoder ErrorDecoder,  which is declared as:

static class MyErrorDecoder implements ErrorDecoder {

    private final ErrorDecoder decoder = new ErrorDecoder.Default();
    private final RetryConfig clientRetryConfig;
    private final Map methodRetryConfig;

    public MyErrorDecoder(RetryConfig clientRetryConfig, Map methodRetryConfig) {
        this.clientRetryConfig = clientRetryConfig;
        this.methodRetryConfig = methodRetryConfig;
    }

    @Override
    public Exception decode(String s, Response response) {
        // get method name
        var methodName = response.request().requestTemplate().methodMetadata().method().getName();

        RetryConfig retryConfig = null;
        // if methodName config found in any method or client level config
        // retryConfig = this.methodRetryConfig.get(methodName);

        // check if retry status code
        var status = response.status();
        if (retryConfig != null && retryConfig.retryCodes.contains(status)) {
            var httpMethod = response.request().httpMethod();
            return new MyRetryException(retryConfig, status, httpMethod, response.request());
        }

        return decoder.decode(s, response);
    }
}

Now, based on our global or method-level retry config, we can throw our custom RetryException called MyRetryException.

Customizing the Retryer would be a little hacky. We would check if thrown an exception if that is instanceof our custom MyRetryException, if yes we would initiate the internally created instance of the Default Retryer provided by Feign. The Default implementation of Retryer by default uses backoff as exponential which is the preferred way in microservices.

static class MyRetryer implements feign.Retryer {

    private Retryer retryer;
    private Retryer initAndGet(RetryConfig retryConfig) {
        if (retryer == null) {
            retryer = new feign.Retryer.Default(
                    retryConfig.period,
                    retryConfig.maxPeriod,
                    retryConfig.maxAttempts
            );
        }

        return retryer;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (e instanceof MyRetryException) {
            MyRetryException retryException = (MyRetryException) e;
            initAndGet(retryException.retryConfig).continueOrPropagate(e);
        }

        throw e;
    }

    @Override
    public feign.Retryer clone() {
        return new MyRetryer();
    }
}


Now, that all components of your custom feign builder are ready, we can start registering the client. We can loop through the configured clients and can initiate the builder and then we can register all candidate client beans.

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    // init binder
    this.config = Binder.get(this.environment)
            .bind("myconfig.web.client", Bindable.mapOf(String.class, MyConfig.class))
            .orElseThrow(IllegalStateException::new);

    // get classes annotated with @FeignClient
    var annotatedTypeScanner = new AnnotatedTypeScanner(FeignClient.class);
    var candidateClients = annotatedTypeScanner.findTypes("...base.package.lib.client");

    candidateClients.forEach(candidateClient -> {
        FeignClient annotation = candidateClient.getAnnotation(FeignClient.class);
        if (config.containsKey(annotation.name())) { // check if client name matches config name
            var clientConfig = config.get(annotation.name());
            var feignClient = Feign.builder()
                    .contract(springMvcContract())
                    .encoder(feignEncoder())
                    .decoder(feignDecoder())
                    .options(new Request.Options(clientConfig.connectTimeout, clientConfig.readTimeout))
                    .errorDecoder(new MyErrorDecoder(clientConfig.retry, clientConfig.retryMethod))
                    .retryer(new MyRetryer())
                    .target(candidateClient, clientConfig.url);

            var clientName = String.format("%sClient");
            registry.registerBeanDefinition(clientName, createBeanDefination(feignClient));
        }
    });
}

That is it. Now once this setup is wrapped as a lib, we can just use @EnableCustomFeignClients along with relevant connection configuration in application.yml to use it in any Spring Boot application.