Spring Boot OpenFeign Client Tutorial

OpenFeign is a declarative webservice client which makes writing web service clients easier. When calling other services using Feign, we don’t need to write any code. We just need to create an interface and annotate it. It has support for OpenFeign and Jakarta JAX-RS annotations.

To reiterate as we know, a declarative REST client is an interface that helps reduce the boilerplate code, generates a proxy implementing this interface, and performs the exchanges at the framework level.

In this tutorial, we will explore Spring Cloud OpenFeign client which is a declarative web service client.

1. Feign vs. OpenFeign vs. Spring HttpExchange

Feign, OpenFeign, and Spring HttpExchange are all Java-based libraries used for creating declarative HTTP clients in Java applications. However, it is important to understand their differences and use cases:

FeatureFeignOpenFeignSpring HttpExchange
Origin and PurposeDeveloped by NetflixForked from Feign with additional featuresPart of the Spring framework
Spring IntegrationCan be used with Spring and non-Spring appsIntegrated using Spring Cloud OpenFeign moduleExclusive to Spring
CustomizationCustomizable components, request, and responseCustomizable components, request, and responseFine-grained control over HTTP interactions within Spring apps
Activeness and CommunityLess active recentlyActive and supportedActive and supported
Use CasesMicroservices, RESTful API integrationMicroservices, RESTful API integrationCustom HTTP interactions within Spring apps

For Spring Boot applications, the choice between OpenFeign and HttpExchange is mostly dependent on the desired features in the applications.

For example, OpenFeign integrates well with other Spring cloud components like service discovery and load balancing. Whereas HttpExchange uses WebClient to invoke HTTP requests, thus providing better control over request and response processing.

Similarly, OpenFeign is a blocking client based on a thread-per-request model where for each request, the assigned thread blocks until it receives the response. It can hamper the performance in high-traffic spikes and eventually bring the service down The WebClient, used by HttpExchange, is non-blocking and reactive, thus easily overcomes the above performance bottlenecks

2. Setting Up Feign Client with Spring Boot

Let’s set up a simple Spring Boot web application and enable it to use the Feign Client.

2.1. Maven

In order to use Feign Client, we would need to add the spring-cloud-starter-openfeign dependency. Since we are creating a web application, let’s add the spring-boot-starter-web as well.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

We would also need to add the spring-cloud-dependencies since we need Spring Cloud in our project.

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>2022.0.3</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

2.2. Enabling Feigh Clients with @EnableFeignClients

Next, we need to enable Feign Clients using @EnableFeignClients annotation that enables component scanning for all interfaces annotated with @FeignClient.

@EnableFeignClients(basePackages="com.howtodoinjava.feign.client")
@Configuration
public class FeignConfig {

  //...
}

3. Creating Declarative Interfaces with @FeignClient

Interfaces can be declared as Feign Clients using the @FeignClient annotation. Let’s create a simple client to fetch users using the Json Placeholder APIs.

@FeignClient(value = "userFeignClient", url = "https://jsonplaceholder.typicode.com/")
public interface UserFeignClient {

  @RequestMapping(method = RequestMethod.GET, value = "/users")
  ResponseEntity<List<User>> getUsers();
}

In the example above we added two arguments to the @FeignClient annotation.

  • value: Specifies the name of the client. It is mandatory and can be any arbitrary value.
  • url: The base URL for the API.

We also declared a GET method getUsers() , which would get the user details from the relative path /users.

4. Executing Remote API

To execute the remote API, we use the reference to the Interface and call its methods. The methods can return either the ResponseEntity or the direct JSON response body. We need to handle the response accordingly.

@Autowired
private UserFeignClient userFeignClient;

//...

ResponseEntity<List<User>> responseEntity = userFeignClient.getUsers();

if (responseEntity.getStatusCode().is2xxSuccessful()) {
  //Process response body
  List<User> usersList = responseEntity.getBody();
  usersList.forEach(System.out::println);
} else if(responseEntity.getStatusCode().is4xxClientError()){
  throw new BadRequestException("Bad Request");
} else {
  throw new RuntimeException("Server Error");
}

5. Decorating Feign Client with FeignBuilder API

There may be cases where we need to customize our Feign Clients such as adding custom interceptors or decoders. In such cases, we can build the Feign Clients manually using the Feign Builder API.

Let’s create a new interface PostsFeignClient and add a method getPosts(). This will help us get the posts from the same JSON Placeholder APIs. We will annotate the method with the @RequestLine which is a core Feign annotation.

The @RequestLine specifies the HTTP method, path and request parameters (if any). The parameters can be specified using the @Param annotation.

public interface PostsFeignClient {
  
  @RequestLine("GET /posts")
  List<Post> getPosts();
}

Let’s modify the controller where we decorate the Feigh client and use it to invoke the remote API.

@RestController
public class FeignController {

  private PostsFeignClient postsFeignClient;
  
  @Autowired
  public FeignController() {
    this.postsFeignClient = Feign.builder()
        .decoder(new GsonDecoder())
        .target(PostsFeignClient.class, "https://jsonplaceholder.typicode.com/");
  }

  @GetMapping(value = "/posts")
  public List<Post> getAllPosts() {
    return postsFeignClient.getPosts();
  }
}

Notice that we have specified an override for the default decoder Jackson using the GsonDecoder(). Note that GsonDecoder is an external dependency and needs to be added to our pom.xml.

<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-gson</artifactId>
</dependency>

6. Handling Inheritance

To avoid boilerplate code, Feign allows grouping common functionality into base interfaces. In the following example, we create a new AccountFeignClient which extends from our UserFeignClient created in the second section.

@FeignClient(value = "accountFeignClient", url = "https://jsonplaceholder.typicode.com/")
public interface AccountFeignClient extends UserFeignClient {

  @GetMapping(value = "/account/{userId}")
  String getAccountByUser(@PathVariable("userId") Integer userId);
}

We could now replace the UserFeignClient with the AccountFeignClient in our Controller and access methods of both clients. Note that Feign does not support inheritance of more than one level.

7. OpenFeign Configuration

7.1. Default Configuration

Each Feign Client is composed of a set of components that work together for a remote service call. Whenever a named client is created, Spring Cloud creates a default value for these components using FeignClientsConfiguration.

Let’s look at the default values of these components:

  • Decoder: To decode the Response, ResponseEntityDecoder is used, which wraps SpringDecoder.
  • EncoderSpringEncoder is used to encode the RequestBody.
  • LoggerSlf4jLogger is the default logger used by Feign.
  • ContractSpringMvcContract that provides annotation processing.
  • Feign-BuilderFeignCircuitBreaker.Builder is used to construct the Feign components.
  • Client: If Spring Cloud LoadBalancer is on the classpath, LoadBalancerFeignClient is used, else default Feign client is used.

7.2. Custom Configurations

Spring Cloud allows us to customize one or more of these default configurations on top of FeignClientsConfiguration by specifying an additional configuration.

For example, let’s override the default HttpClient in our AccountFeignClient to use ApacheHttp5Client. We will need to create a configuration class AccountFeignConfiguration and declare the client bean.

public class AccountFeignConfiguration {

  @Bean
  public CloseableHttpClient feignClient() {
    return HttpClients.createDefault();
  }
}

We can use the AccountFeignConfiguration in our FeignClient using the configuration parameter.

@FeignClient(value = "accountFeignClient", 
  url = "https://jsonplaceholder.typicode.com/", 
  configuration = AccountFeignConfiguration.class)
public interface AccountFeignClient extends UserFeignClient {

  //...
}

Do not use @Configuration annotation as it would apply to all Feign clients unless we exclude the package from @ComponentScan or putting it in a separate, non-overlapping package from any @ComponentScan or @SpringBootApplication.

Since ApacheHttp5Client is an external dependency, we need to add it to our dependencies.

<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-hc5</artifactId>
</dependency>

7.3. YAML/Properties Configuration

We can also override configs using application properties. Here too, we can have overrides for specific Feign Client or all the clients.

Let’s configure connection timeout for all feign clients to 5 seconds. To do so, we specify properties using the “default” feign name.

feign:
  client:
    config:
      default:
        connectTimeout: 5000

Similarly, if we want to override timeout for just the AccountFeignClient we can set it as follows using the name of the Feign Client specified using the “value” attribute.

feign:
  client:
    config:
      accountFeignClient:
        connectTimeout: 5000

If we create both @Configuration bean and configuration properties, property values will override @Configuration values. We can change the priority by setting spring.cloud.openfeign.client.default-to-properties.

spring.cloud.openfeign.client.default-to-properties = false

8. Common OpenFeign Configuration Examples

In this section, we will explore some of the common configurations used with Feign Clients.

8.1. Interceptors

Feign provides RequestInterceptor interface which may be useful for performing tasks such as Authentication or Logging.

Let’s say we want to add a custom header to each request we send via the Feign Client. We could achieve that using Interceptors.

@Bean
RequestInterceptor requestInterceptor() {
  return requestTemplate -> {
    requestTemplate.header("requestID", "UUID");
  };
}

8.2. BasicAuth and OAuth2 Authentication

To integrate HTTP basic authentication, Feign provides BasicAuthRequestInterceptor which adds the necessary headers to perform basic authentication.

@Bean
BasicAuthenticationInterceptor basicAuthenticationInterceptor() {
  return new BasicAuthenticationInterceptor("admin", "password");
}

OAuth2 is an authorization method to provide access to protected resources over the HTTP protocol. To get an overview of OAuth2, please refer to this article.

We can easily add OAuth2 authentication support by using the spring-cloud-starter-oauth2 module and registering the OAuth2 client. Spring will handle everything behind the scene for all secured methods.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency

Next, register the client. The following configuration is for GitHub OAuth2 client.

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: YOUR_GITHUB_CLIENT_ID
            client-secret: YOUR_GITHUB_CLIENT_SECRET
            scope:
              - read:user
              - user:email
        provider:
          github:
            issuer-uri: https://github.com/login/oauth/authorize

8.3. Exception Handling

Feign’s default ErrorDecoder throws FeignException by default in case of errors. This may not always be useful for meaningful error handling. In such scenarios, we can write our own custom ErrorDecoder. This way we can have specific error handling.

public class CustomErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {

    return switch (response.status()) {
    case 400 -> new BadRequestException(response);
    case 429 -> new TooManyRequestsException(response);
    default -> new Exception("Generic exception");
    };
  }
}

Let’s add this to our configuration.

@Bean
ErrorDecoder errorDecoder() {
  return new CustomErrorDecoder();
}

8.4. Read and Connection Timeouts

Feign allows us to configure the connection and read timeouts. These can be configured per client or for all clients.

By default, the connection timeout is set to 2s and read timeout to 60s. Let’s change the connection timeout to 5s for all clients.

feign:
  client:
    config:
      default:
        connectTimeout: 5000

Let’s configure read timeout to 10s for our AccountFeignClient.

feign:
  client:
    config:
      accountFeignClient:
        readTimeout: 10000

8.5. Retry Handling

Feign disables retry by default. We can change this to retry on IOException by using the default Retryer implementation provided by Feign. This will enable retries of a maximum of 5 times after an interval of 1s.

@Bean
public Retryer retryer() {
  return new Retryer.Default();
}

We may want to change this and have retries enabled for specific response statuses. To do this, we will need to raise a RetryableException.

Let’s modify our ErrorDecoder to send a RetryableException in case of 5xx response status.

public class CustomErrorDecoder implements ErrorDecoder {

  @Override
  public Exception decode(String methodKey, Response response) {

    if (HttpStatusCode.valueOf(response.status()).is5xxServerError()) {
      return new RetryableException(response.status(), "5xx exception", null, null, response.request());
    }
    return new Exception("Generic exception");
  }
}

In a real-life project, we may want to retry for a specific number of times with some delays between retries before eventually giving up.

In our use case, we will re-try for a maximum of 3 times with a delay of 2s between each retry. Post the retries are exhausted, we will propagate the exception.

To do the same, we need to have our custom Retryer implementation. To retry or not, is decided by implementing the method continueOrPropagate which takes a RetryableException as a parameter.

public class CustomRetryer implements Retryer {

  private int maxRetries;
  private long retryInterval;
  private int attempt = 1;

  public CustomRetryer(int maxRetries, Long retryInterval) {
    this.maxRetries = maxRetries;
    this.retryInterval = retryInterval;
  }

  @Override
  public void continueOrPropagate(RetryableException e) {
    if (attempt++ >= maxRetries) {
      throw e;
    }
    try {
      Thread.sleep(retryInterval);
    } catch (InterruptedException ignored) {
      Thread.currentThread().interrupt();
    }
  }

  @Override
  public Retryer clone() {
    return new CustomRetryer(1, 1000L);
  }
}

To stop retrying and propagate the error, we have to throw the RetryableException this method receives. Otherwise, it will continue to retry. In our implementation, once the maxRetries are exhausted, we throw the exception to stop retries.

8.6. Logging

A logger is created for each Feign Client by default. To enable logging we need to declare it in the application properties using the package name of the clients.

logging.level.com.howtodoinjava.feign.client=DEBUG

This will enable logging for all clients in the package. If we need to enable logging for only a specific client, we can do that by explicitly specifying the client in the properties.

logging.level.com.howtodoinjava.feign.client.AccountFeignClient=DEBUG

Note that Feign logging only responds to the DEBUG level.

Feign provides Logging levels to indicate the level of logging we need for our clients. Let’s add some basic logging to our configuration class.

@Bean
Logger.Level feignLoggerLevel() {
  return Logger.Level.BASIC;
}

There are four logging levels:

  • NONE – No logging
  • BASIC – Logs the request method and URL and the response status code and execution time
  • HEADERS – In addition to basic logging, it also logs request and response headers
  • FULL – Logs the headers, body, and metadata for both requests and responses

9. Demo

Now that we have all the configurations in place, let’s test them out. To invoke the Feign clients, we need to expose some endpoints in our application.

Below is what our FeignController looks like.

@RestController
public class FeignController {

  private PostsFeignClient postsFeignClient;

  @Autowired
  private AccountFeignClient accountFeignClient;

  @Autowired
  public FeignController() {

    this.postsFeignClient = Feign.builder()
      .decoder(new GsonDecoder())
      .target(PostsFeignClient.class, "https://jsonplaceholder.typicode.com/");
  }

  @GetMapping(value = "/users")
  public List<User> getAllUsers() {

    return accountFeignClient.getUsers();
  }

  @GetMapping(value = "/posts")
  public List<Post> getAllPosts() {

    return postsFeignClient.getPosts();
  }

  @GetMapping(value = "/account/{userId}")
  public String getAccountByUserId(@PathVariable("userId") Integer userId) {
  
    return accountFeignClient.getAccountByUserId(userId);
  }

}

Start the Spring Boot application and hit the endpoints.

HTTP GET http://localhost:8080/users

HTTP GET http://localhost:8080/account/1

10. Conclusion

In this tutorial, we learned about Feign Client and how to enable and use them in a Spring Boot application. We learned the default Feign configurations and also how to customize various Feign Client configurations.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.