概述

在这个教程中,我们将介绍Netflix开发的声明式HTTP客户端——Feign。Feign的目标是简化HTTP API客户端的创建,开发者只需声明并注解一个接口,实际的实现将在运行时动态提供。

示例

在整个教程中,我们将使用一个示例的书店应用,它提供了REST API端点。

我们可以轻松地克隆项目并在本地运行:

mvn install spring-boot:run

设置

首先,我们需要添加必要的依赖:

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

除了feign-core依赖(它也会被自动引入)之外,我们还会用到几个插件,特别是feign-okhttp用于内部使用Square的OkHttp客户端发送请求,feign-gson用于使用Google的Gson作为JSON处理器,以及feign-slf4j用于使用简单日志门面来记录请求。

为了获得实际的日志输出,我们需要在类路径上包含支持SLF4J的首选日志实现。

在我们开始创建客户端接口之前,先设置一个Book模型来存储数据:

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

    // standard constructor, getters and setters
}

注意:JSON处理器至少需要一个无参数构造函数。

实际上,我们的REST提供者是一个基于超媒体驱动的API,所以还需要一个简单的包装类:

public class BookResource {
    private Book book;

    // standard constructor, getters and setters
}

注意:由于我们的示例Feign客户端并未利用超媒体特性,所以BookResource保持简单!

服务器端

为了理解如何定义Feign客户端,我们将先看看我们的REST提供者支持的一些方法和响应。

让我们尝试使用简单的curl命令列出所有书籍。

我们需要记住,在所有调用前加上/api,这是应用的servlet上下文:

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

结果将得到一个完整的书库,以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"
      }
    ]
  }
]

我们还可以通过在GET请求后附加ISBN来查询单个Book资源:

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

Feign客户端

现在,让我们定义我们的Feign客户端。

我们将使用@RequestLine注解来指定HTTP动词和一个参数路径部分。

参数将使用@Param注解进行建模:

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);
}

注意:Feign客户端只能消费文本基础的HTTP API,这意味着它们不能处理二进制数据,如文件上传或下载。

就这样!现在我们将使用Feign.builder()来配置基于接口的客户端。实际的实现将在运行时生成:

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支持各种插件,如JSON/ XML编码器和解码器,或者用于发送请求的底层HTTP客户端。

单元测试

让我们创建三个测试用例来测试我们的客户端。请注意,我们使用了静态导入org.hamcrest.CoreMatchers.*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"));
}

进一步阅读

如果我们需要在服务不可用时提供某种备份,可以在类路径上添加HystrixFeign,然后使用HystrixFeign.builder()构建客户端。

查看这个专门的教程系列了解Hystrix的更多信息。

此外,如果想将Spring Cloud Netflix Hystrix与Feign集成,可以参考这篇文章

此外,我们还可以在客户端添加负载均衡和/或服务发现功能。

通过在类路径上添加Ribbon,并使用构建器实现:

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

对于服务发现,我们需要启用Spring Cloud Netflix Eureka的服务。然后,只需与Spring Cloud Netflix Feign集成,即可免费获得Ribbon的负载均衡。更多关于此内容的信息可以在这里找到:这里

结论

在这篇文章中,我们解释了如何使用Feign构建声明式HTTP客户端来消费文本基础的API。

如往常一样,本文中展示的所有代码示例均可在GitHub上找到:这里