1. 概述

在使用Feign时,有时我们需要设置HTTP调用的请求头。Feign允许我们仅通过声明性语法构建HTTP客户端。

在这篇简短教程中,我们将学习如何使用注解配置请求头,并通过拦截器包含常见的请求头。

2. 示例

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

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

$ mvn install spring-boot:run

现在让我们深入研究客户端实现。

3. 使用Header注解

设想一个场景:某些API调用始终应包含静态请求头。在这种情况下,我们可以在客户端中配置这些请求头。一个典型例子是包括Content-Type头。

使用@Header注解,我们可以轻松配置静态请求头。我们可以将头的值定义为静态的,也可以动态定义。

3.1. 设置静态头值

让我们在BookClient中配置两个静态头,即Accept-LanguageContent-Type

@Headers("Accept-Language: en-US")
public interface BookClient {
    
    @RequestLine("GET /{isbn}")
    BookResource findByIsbn(@Param("isbn") String isbn);

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

在上述代码中,由于Accept-Language应用到了BookClient,所以所有API都将包含它。然而,create方法还有额外的Content-Type头。

接下来,让我们使用Feign的Builder方法创建BookClient,并传递HEADERS日志级别:

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

现在,让我们测试create方法:

String isbn = UUID.randomUUID().toString();
Book book = new Book(isbn, "Me", "It's me!", null, null);
        
bookClient.create(book);

book = bookClient.findByIsbn(isbn).getBook();

然后,我们在输出日志中验证头信息:

18:01:15.039 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#create] Accept-Language: en-US
18:01:15.039 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#create] Content-Type: application/json
18:01:15.096 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#findByIsbn] Accept-Language: en-US

需要注意的是,如果客户端接口和API方法中的头名称相同,它们不会互相覆盖。相反,请求将包含所有这样的值。

3.2. 设置动态头值

使用@Header注解,我们还可以设置动态头值。为此,我们需要将值表示为占位符。

让我们在BookClient中添加一个名为requester的占位符来包含x-requester-id头:

@Headers("x-requester-id: {requester}")
public interface BookClient {
   
    @RequestLine("GET /{isbn}")
    BookResource findByIsbn(@Param("requester") String requester, @Param("isbn") String isbn);
}

这里,我们将x-requester-id设置为一个可传递到每个方法的变量。我们使用了@Param注解来匹配变量的名称,它会在运行时扩展以满足由@Headers注解指定的头。

现在,让我们使用带有x-requester-id头的调用BookClient API:

String requester = "test";
book = bookClient.findByIsbn(requester, isbn).getBook();

然后,我们在输出日志中验证请求头:

18:04:27.515 [main] DEBUG c.b.f.c.h.s.parameterized.BookClient - [BookClient#findByIsbn] x-requester-id: test

4. 使用HeaderMaps注解

设想一个场景,头的键和值都是动态的,且可能的键范围在调用之前未知。此外,同一个客户端的不同方法调用之间,头可能有所变化。一个典型例子是设置某些元数据头。

使用一个被@HeaderMap注解的Map参数可以设置动态头:

@RequestLine("POST")
void create(@HeaderMap Map<String, Object> headers, Book book);

现在,让我们尝试使用头映射测试create方法:

Map<String,Object> headerMap = new HashMap<>();
        
headerMap.put("metadata-key1", "metadata-value1");
headerMap.put("metadata-key2", "metadata-value2");
        
bookClient.create(headerMap, book);

然后,我们在输出日志中验证头信息:

18:05:03.202 [main] DEBUG c.b.f.c.h.dynamicheader.BookClient - [BookClient#create] metadata-key1: metadata-value1
18:05:03.202 [main] DEBUG c.b.f.c.h.dynamicheader.BookClient - [BookClient#create] metadata-key2: metadata-value2

5. 请求拦截器

拦截器可以为每次请求或响应执行各种隐式任务,如日志记录或身份验证。

Feign提供了一个RequestInterceptor接口,我们可以借此为每个请求添加头。

当知道某个头应该包含在每次调用中时,添加请求拦截器是有意义的。这消除了调用代码实现诸如身份验证或跟踪等非功能性需求的依赖。

让我们尝试实现一个AuthorisationService,用于生成授权令牌:

public class ApiAuthorisationService implements AuthorisationService {

    @Override
    public String getAuthToken() {
        return "Bearer " + UUID.randomUUID();
    }
}

现在,让我们实现自定义请求拦截器:

public class AuthRequestInterceptor implements RequestInterceptor {
    
    private AuthorisationService authTokenService;
   
    public AuthRequestInterceptor(AuthorisationService authTokenService) {
        this.authTokenService = authTokenService;
    }

    @Override
    public void apply(RequestTemplate template) {
        template.header("Authorisation", authTokenService.getAuthToken());
    }
}

请注意,请求拦截器可以读取、移除或修改请求模板的任何部分

现在,我们使用builder方法将AuthInterceptor添加到BookClient

Feign.builder()
  .requestInterceptor(new AuthInterceptor(new ApiAuthorisationService()))
  .encoder(new GsonEncoder())
  .decoder(new GsonDecoder())
  .logger(new Slf4jLogger(type))
  .logLevel(Logger.Level.HEADERS)
  .target(BookClient.class, "http://localhost:8081/api/books");

然后,让我们带有Authorization头测试BookClient API:

bookClient.findByIsbn("0151072558").getBook();

现在,我们在输出日志中验证头信息:

18:06:06.135 [main] DEBUG c.b.f.c.h.staticheader.BookClient - [BookClient#findByIsbn] Authorisation: Bearer 629e0af7-513d-4385-a5ef-cb9b341cedb5

也可以在Feign客户端上应用多个请求拦截器,尽管没有关于它们应用顺序的确切保证。

6. 总结

在这篇文章中,我们讨论了Feign客户端如何支持设置请求头。我们使用@Headers@HeaderMaps注解和请求拦截器实现了这一功能。

如往常一样,本教程的所有示例代码可在GitHub上找到。