Intro to Feign

1. Overview

In this tutorial, we will introduce and explain Feign, a declarative HTTP client developed by Netflix.

Feign aims at simplifying HTTP API clients. Simply put, the developer needs only to declare and annotate an interface while the actual implementation will be provisioned at runtime.

2. Example

We will present an example of a bookstore service REST API, that is queried and tested based on the Feign HTTP client.

Before we build a sample Feign client, we’ll add the needed dependencies and startup the REST service.

The bookstore service example can be cloned from here.

After downloading the service application, we’ll run it with:

$> mvn install spring-boot:run

3. Setup

First, we’ll create a new Maven project and include these dependencies:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>9.3.1</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>9.3.1</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-slf4j</artifactId>
    <version>9.3.1</version>
</dependency>

Besides the feign-core dependency (which is also pulled in), we’ll use a few plugins, especially: feign-okhttp for internally using Square’s OkHttp client to make requests, feign-gson for using Google’s GSON as JSON processor and feign-slf4j for using the Simple Logging Facade to log requests.

To actually get some log output, you’ll need your favorite, SLF4J-supported logger implementation on your classpath.

Before we continue to create our client interface, we’ll set up a Book model for holding our data:

public class Book {
    private String isbn;
    private String author;
    private String title;
    private String synopsis;
    private String language;

    // standard constructor, getters and setters
}

NOTE: At least a “no arguments constructor” is needed by a JSON processor.

In fact, our REST provider is a hypermedia-driven API, so we’ll need a simple wrapper class:

public class BookResource {
    private Book book;

    // standard constructor, getters and setters
}

Note: Well keep the BookResource simple because our sample Feign client doesn’t benefit from hypermedia features!

4. Server Side

To understand how to define a Feign client, we’ll first look into some of the methods and responses supported by our REST provider.

Let’s try it out with a simple curl shell command to list all books. Don’t forget to prefix your calls with ‘/api’, which is the servlet-context defined in the application.properties:

$> curl http://localhost:8081/api/books

we will get a complete book repository represented as JSON:

[
  {
    "book": {
      "isbn": "1447264533",
      "author": "Margaret Mitchell",
      "title": "Gone with the Wind",
      "synopsis": null,
      "language": null
    },
    "links": [
      {
        "rel": "self",
        "href": "http://localhost:8081/api/books/1447264533"
      }
    ]
  },

  ...

  {
    "book": {
      "isbn": "0451524934",
      "author": "George Orwell",
      "title": "1984",
      "synopsis": null,
      "language": null
    },
    "links": [
      {
        "rel": "self",
        "href": "http://localhost:8081/api/books/0451524934"
      }
    ]
  }
]

We can also query individual Book resource, by appending the ISBN to a get request:

$> curl http://localhost:8081/api/books/1447264533

5. Feign Client

Now we’ll define our Feign client.

We will use the @RequestLine annotation to specify the HTTP verb and a path part as argument, and the parameters will be modeled using the @Param annotation:

public interface BookClient {
    @RequestLine("GET /{isbn}")
    BookResource findByIsbn(@Param("isbn") String isbn);

    @RequestLine("GET")
    List<BookResource> findAll();

    @RequestLine("POST")
    @Headers("Content-Type: application/json")
    void create(Book book);
}

NOTE: Feign clients can be used to consume text-based HTTP APIs only, which means that they cannot handle binary data, e.g. file uploads or downloads.

That’s all! Now we’ll use the Feign.builder() to configure our interface-based client. The actual implementation will be provisioned at runtime:

BookClient bookClient = Feign.builder()
  .client(new OkHttpClient())
  .encoder(new GsonEncoder())
  .decoder(new GsonDecoder())
  .logger(new Slf4jLogger(BookClient.class))
  .logLevel(Logger.Level.FULL)
  .target(BookClient.class, "http://localhost:8081/api/books");

Feign supports various plugins such as JSON/XML encoders and decoders or an underlying HTTP client for making the requests.

6. Unit Test

Let’s create a unit test class, containing three @Test methods, to test our client. The test will use static imports from the packages org.hamcrest.CoreMatchers.* and org.junit.Assert.*:

@Test
public void givenBookClient_shouldRunSuccessfully() throws Exception {
   List<Book> books = bookClient.findAll().stream()
     .map(BookResource::getBook)
     .collect(Collectors.toList());

   assertTrue(books.size() > 2);
}

@Test
public void givenBookClient_shouldFindOneBook() throws Exception {
    Book book = bookClient.findByIsbn("0151072558").getBook();
    assertThat(book.getAuthor(), containsString("Orwell"));
}

@Test
public void givenBookClient_shouldPostBook() throws Exception {
    String isbn = UUID.randomUUID().toString();
    Book book = new Book(isbn, "Me", "It's me!", null, null);
    bookClient.create(book);
    book = bookClient.findByIsbn(isbn).getBook();

    assertThat(book.getAuthor(), is("Me"));
}

These tests are pretty self-explanatory. To run it, simply execute the Maven test goal:

$> mvn test

7. Further Reading

If you need some kind of fallback, in the case of service unavailability, you can add HystrixFeign to your classpath and build your client with the HystrixFeign.builder() instead.

To learn more about Hystrix, please follow this dedicated tutorial series.

If you want to integrate Spring Cloud Netflix Hystrix with Feign, you can read more about this here.

It’s also possible to add client-side load-balancing and/or service discovery to your client.

The former is done by adding Ribbon to your classpath and call the builder like so:

BookClient bookClient = Feign.builder()
  .client(RibbonClient.create())
  .target(BookClient.class, "http://localhost:8081/api/books");

For service discovery, you have to build up your service with Spring Cloud Netflix Eureka enabled. Then simply integrate with Spring Cloud Netflix Feign and you get Ribbon load-balancing for free. More about this can be found here.

8. Conclusion

This article explained how to build a declarative HTTP client using Feign to consume text-based APIs.

As usual, you’ll find the sources on GitHub.

Leave a Reply

Your email address will not be published.