Custom Feign Client Builder library in Spring Boot
Creating a custom spring module to make HTTP client library using OpenFeign
Creating a custom spring module to make HTTP client library using OpenFeign
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));
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())
...
}
}
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
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.
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.