Feign 是一个 Java 到 HTTP 客户端绑定器,其灵感来自于Retrofit、JAXRS-2.0和WebSocket。 Feign 的第一个目标是降低将Denominator统一绑定到 HTTP API 的复杂性,而不管ReSTativity如何。
Feign 使用 Jersey 和 CXF 等工具为 ReST 或 SOAP 服务编写 Java 客户端。此外,Feign 允许您在 Apache HC 等 http 库之上编写自己的代码。 Feign 以最小的开销将您的代码连接到 http API,并且通过可自定义的解码器和错误处理来编写代码,这些代码可以写入任何基于文本的 http API。
Feign 的工作原理是将注释处理为模板化请求。在输出之前,参数会以简单的方式应用于这些模板。尽管 Feign 仅限于支持基于文本的 API,但它极大地简化了系统方面,例如重放请求。此外,知道这一点后,Feign 可以轻松地对您的转换进行单元测试。
Feign 10.x 及更高版本基于 Java 8 构建,应该适用于 Java 9、10 和 11。对于需要 JDK 6 兼容性的用户,请使用 Feign 9.x
这是 feign 提供的当前主要功能的地图:
让API客户端更轻松
- 响应缓存
- 支持缓存 api 响应。允许用户定义在什么条件下响应适合缓存以及应使用什么类型的缓存机制。
- 支持内存缓存和外部缓存实现(EhCache、Google、Spring 等...)
- 完整的 URI 模板表达式支持
- 支持级别 1 到级别 4 URI 模板表达式。
- 使用URI 模板 TCK来验证合规性。
LoggerAPI重构- 重构
LoggerAPI 以更接近 SLF4J 等框架,为 Feign 中的日志记录提供通用的思维模型。 Feign 本身将自始至终使用该模型,并为如何Logger使用提供更清晰的方向。
- 重构
RetryAPI重构- 重构
RetryAPI 以支持用户提供的条件并更好地控制回退策略。这可能会导致不向后兼容的重大更改
- 重构
- 异步执行支持通过
CompletableFuture- 允许
Future请求/响应生命周期的链接和执行器管理。 实施将需要非向后兼容的重大更改。然而,在考虑响应式执行之前,需要此功能。
- 允许
- 通过反应流的反应执行支持
- 对于 JDK 9+,请考虑使用
java.util.concurrent.Flow. - 支持JDK 8 上的Project Reactor和RxJava 2+实现。
- 对于 JDK 9+,请考虑使用
- 额外的断路器支持。
- 支持其他断路器实现,例如Resilience4J和 Spring Circuit Breaker
feign 库可从Maven Central获取。
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> <version>??feign.version??</version> </dependency>
用法通常如下所示,是规范 Retrofit 示例的改编版。
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);@RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}
public static class Contributor { String login; int contributions; }
public static class Issue { String title; String body; List<String> assignees; int milestone; List<String> labels; }
public class MyApp { public static void main(String... args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com");
<span class="pl-c">// Fetch and print a list of the contributors to this library.</span> <span class="pl-smi">List</span><<span class="pl-smi">Contributor</span>> <span class="pl-s1">contributors</span> = <span class="pl-s1">github</span>.<span class="pl-en">contributors</span>(<span class="pl-s">"OpenFeign"</span>, <span class="pl-s">"feign"</span>); <span class="pl-k">for</span> (<span class="pl-smi">Contributor</span> <span class="pl-s1">contributor</span> : <span class="pl-s1">contributors</span>) { <span class="pl-smi">System</span>.<span class="pl-s1">out</span>.<span class="pl-en">println</span>(<span class="pl-s1">contributor</span>.<span class="pl-s1">login</span> + <span class="pl-s">" ("</span> + <span class="pl-s1">contributor</span>.<span class="pl-s1">contributions</span> + <span class="pl-s">")"</span>); }} }
Feign 注释定义了Contract接口和底层客户端应该如何工作之间的关系。 Feign的默认合约定义了以下注解:
| 注解 | 接口目标 | 用法 |
|---|---|---|
@RequestLine |
方法 | 定义请求的HttpMethod和UriTemplate。 Expressions,用大括号括起来的值{expression}使用相应的带注释的参数来解析@Param。 |
@Param |
范围 | 定义一个模板变量,其值将用于解析相应的 template Expression,通过作为注释值提供的名称。如果值丢失,它将尝试从字节码方法参数名称中获取名称(如果代码是使用-parameters标志编译的)。 |
@Headers |
方法、类型 | 定义一个HeaderTemplate; a 的变体UriTemplate。它使用@Param带注释的值来解析相应的Expressions.当在 上使用时Type,模板将应用于每个请求。当用于 a 时Method,模板将仅应用于带注释的方法。 |
@QueryMap |
范围 | 定义一组Map名称-值对(或 POJO)以扩展为查询字符串。 |
@HeaderMap |
范围 | 定义一系列Map名称-值对,以扩展为Http Headers |
@Body |
方法 | 定义 a Template,类似于 aUriTemplate和HeaderTemplate,它使用@Param带注释的值来解析相应的Expressions. |
覆盖请求行
如果需要将请求定位到不同的主机,然后是创建 Feign 客户端时提供的主机,或者您想为每个请求提供目标主机,请包含一个
java.net.URI参数,Feign 将使用该值作为请求目标。@RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(URI host, Issue issue, @Param("owner") String owner, @Param("repo") String repo);
FeignExpressions表示URI 模板 - RFC 6570定义的简单字符串表达式(级别 1) 。 使用相应的带注释的方法参数Expressions进行扩展。Param
例子
public interface GitHub {@RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repository);
class Contributor { String login; int contributions; } }
public class MyApp { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com");
<span class="pl-c">/* The owner and repository parameters will be used to expand the owner and repo expressions</span>* defined in the RequestLine. * * the resulting uri will be https://api.github.com/repos/OpenFeign/feign/contributors */ github.contributors("OpenFeign", "feign"); } }
表达式必须括在花括号中{},并且可以包含正则表达式模式,并用冒号分隔: 以限制解析值。 示例 owner必须按字母顺序排列。{owner:[a-zA-Z]*}
RequestLine模板QueryMap遵循URI 模板 - RFC 6570级别 1 模板规范,该规范指定以下内容:
- 未解析的表达式被省略。
- 所有文字和变量值(如果尚未
encoded通过@Param注释进行编码或标记)均经过 pct 编码。
我们还对 3 级路径样式表达式提供有限支持,并具有以下限制:
- 地图和列表默认展开。
- 仅支持单变量模板。
例子:
{;who} ;who=fred
{;half} ;half=50%25
{;empty} ;empty
{;list} ;list=red;list=green;list=blue
{;map} ;semi=%3B;dot=.;comma=%2C
public interface MatrixService {@RequestLine("GET /repos{;owners}") List<Contributor> contributors(@Param("owners") List<String> owners);
class Contributor { String login; int contributions; } }
如果owners在上面的示例中定义为Matt, Jeff, Susan,则 uri 将扩展为/repos;owners=Matt;owners=Jeff;owners=Susan
有关详细信息,请参阅RFC 6570,第 3.2.7 节
未定义表达式是指表达式的值是显式的null或未提供值的表达式。根据URI 模板 - RFC 6570,可以为表达式提供空值。当 Feign 解析一个表达式时,它首先判断该值是否已定义,如果是则查询参数将保留。如果表达式未定义,则删除查询参数。请参阅下面的完整细分。
空字符串
public void test() { Map<String, Object> parameters = new LinkedHashMap<>(); parameters.put("param", ""); this.demoClient.test(parameters); }
结果
http://localhost:8080/test?param=
丢失的
public void test() { Map<String, Object> parameters = new LinkedHashMap<>(); this.demoClient.test(parameters); }
结果
http://localhost:8080/test
不明确的
public void test() { Map<String, Object> parameters = new LinkedHashMap<>(); parameters.put("param", null); this.demoClient.test(parameters); }
结果
http://localhost:8080/test
有关更多示例,请参阅高级用法。
斜杠呢?
/
/@RequestLine 模板默认不编码斜杠字符。要更改此行为,请将属性设置decodeSlash为。@RequestLinefalse
加号呢?
+根据 URI 规范,
+URI 的路径段和查询段中都允许使用符号,但是,查询中符号的处理可能不一致。在某些遗留系统中,+相当于空格。 Feign 采用现代系统的方法,其中+符号不应代表空格,并且应按照%2B在查询字符串中找到时的方式进行显式编码。如果您希望用作
+空格,请使用文字字符或直接将值编码为%20
该@Param注释有一个可选属性,expander允许完全控制单个参数的扩展。该expander属性必须引用实现该接口的类Expander:
public interface Expander { String expand(Object value); }
该方法的结果遵循上述相同的规则。如果结果是null空字符串,则省略该值。如果该值不是 pct 编码的,则它将是。有关更多示例,请参阅自定义 @Param 扩展。
Headers和模板遵循与请求参数扩展HeaderMap相同的规则,
但有以下更改:
- 未解析的表达式被省略。如果结果是空标头值,则删除整个标头。
- 不执行 pct 编码。
有关示例,请参阅标头。
@Param关于参数及其名称的注释:所有具有相同名称的表达式,无论它们在
@RequestLine、@QueryMap、@BodyTemplate、 或上的位置如何,@Headers都将解析为相同的值。在以下示例中, , 的值contentType将用于解析标头和路径表达式:public interface ContentService { @RequestLine("GET /api/documents/{contentType}") @Headers("Accept: {contentType}") String getDocumentByType(@Param("contentType") String type); }设计界面时请记住这一点。
Body模板遵循与请求参数扩展相同的规则,
但有以下更改:
- 未解析的表达式被省略。
- 扩展值在放置在请求正文之前不会经过 an 传递。
Encoder Content-Type必须指定标头。有关示例,请参阅正文模板。
Feign 有几个方面可以定制。
对于简单的情况,您可以使用Feign.builder()自定义组件构建 API 接口。
对于请求设置,可以使用options(Request.Options options)ontarget()设置connectTimeout、connectTimeoutUnit、readTimeout、readTimeoutUnit、followRedirects。
例如:
interface Bank { @RequestLine("POST /account/{id}") Account getAccountInfo(@Param("id") String id); }public class BankService { public static void main(String[] args) { Bank bank = Feign.builder() .decoder(new AccountDecoder()) .options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true)) .target(Bank.class, "https://api.examplebank.com"); } }
Feign可以产生多个api接口。这些被定义为Target<T>(默认HardCodedTarget<T>),允许在执行之前动态发现和装饰请求。
例如,以下模式可能会使用来自身份服务的当前 url 和身份验证令牌来装饰每个请求。
public class CloudService { public static void main(String[] args) { CloudDNS cloudDNS = Feign.builder() .target(new CloudIdentityTarget<CloudDNS>(user, apiKey)); }class CloudIdentityTarget extends Target<CloudDNS> { /* implementation of a Target */ } }
Feign 包括示例GitHub和Wikipedia客户端。实践中分母项目也可以为 Feign 刮取。特别是看看它的示例 daemon。
Feign 打算与其他开源工具很好地配合。欢迎模块与您喜欢的项目集成!
Gson包含一个编码器和解码器,您可以将其与 JSON API 一起使用。
添加GsonEncoder和/或GsonDecoder到你Feign.Builder喜欢的地方:
public class Example { public static void main(String[] args) { GsonCodec codec = new GsonCodec(); GitHub github = Feign.builder() .encoder(new GsonEncoder()) .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); } }
Jackson包含一个编码器和解码器,您可以将其与 JSON API 一起使用。
添加JacksonEncoder和/或JacksonDecoder到你Feign.Builder喜欢的地方:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(GitHub.class, "https://api.github.com"); } }
对于重量较轻的 Jackson Jr,请使用Jackson Jr 模块中的JacksonJrEncoder和。JacksonJrDecoder
Moshi包含一个编码器和解码器,您可以将其与 JSON API 结合使用。添加MoshiEncoder和/或MoshiDecoder到你Feign.Builder喜欢的地方:
GitHub github = Feign.builder() .encoder(new MoshiEncoder()) .decoder(new MoshiDecoder()) .target(GitHub.class, "https://api.github.com");
SaxDecoder允许您以与普通 JVM 和 Android 环境兼容的方式解码 XML。
以下是如何配置 Sax 响应解析的示例:
public class Example { public static void main(String[] args) { Api api = Feign.builder() .decoder(SAXDecoder.builder() .registerContentHandler(UserIdHandler.class) .build()) .target(Api.class, "https://apihost"); } }
JAXB包含可与 XML API 一起使用的编码器和解码器。
添加JAXBEncoder和/或JAXBDecoder到你Feign.Builder喜欢的地方:
public class Example { public static void main(String[] args) { Api api = Feign.builder() .encoder(new JAXBEncoder()) .decoder(new JAXBDecoder()) .target(Api.class, "https://apihost"); } }
SOAP包括可与 XML API 一起使用的编码器和解码器。
该模块添加了对通过 JAXB 和 SOAPMessage 编码和解码 SOAP Body 对象的支持。它还通过将 SOAPFault 包装到原始 SOAPFault 中来提供 SOAPFault 解码功能javax.xml.ws.soap.SOAPFaultException,这样您只需捕获SOAPFaultException即可处理 SOAPFault。
添加SOAPEncoder和/或SOAPDecoder到你Feign.Builder喜欢的地方:
public class Example { public static void main(String[] args) { Api api = Feign.builder() .encoder(new SOAPEncoder(jaxbFactory)) .decoder(new SOAPDecoder(jaxbFactory)) .errorDecoder(new SOAPErrorDecoder()) .target(MyApi.class, "http://api"); } }
SOAPErrorDecoder注意:如果响应中返回 SOAP 错误并带有错误 http 代码(4xx、5xx、...),您可能还需要添加
fastjson2包含可与 JSON API 一起使用的编码器和解码器。
添加Fastjson2Encoder和/或Fastjson2Decoder到你Feign.Builder喜欢的地方:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .encoder(new Fastjson2Encoder()) .decoder(new Fastjson2Decoder()) .target(GitHub.class, "https://api.github.com"); } }
JAXRSContract会覆盖注释处理,转而使用 JAX-RS 规范提供的标准注释处理。当前针对的是 1.1 规范。
下面是使用 JAX-RS 重写的上面的示例:
interface GitHub { @GET @Path("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo); }public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .contract(new JAXRSContract()) .target(GitHub.class, "https://api.github.com"); } }
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .contract(new JAXRSContract()) .target(GitHub.class, "https://api.github.com"); } }" tabindex="0" role="button">
OkHttpClient将 Feign 的 http 请求定向到OkHttp,从而实现 SPDY 和更好的网络控制。
要将 OkHttp 与 Feign 一起使用,请将 OkHttp 模块添加到类路径中。然后,配置 Feign 以使用 OkHttpClient:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .client(new OkHttpClient()) .target(GitHub.class, "https://api.github.com"); } }
RibbonClient覆盖 Feign 客户端的 URL 解析,添加Ribbon提供的智能路由和弹性功能。
集成要求您将功能区客户端名称作为 url 的主机部分传递,例如myAppProd。
public class Example { public static void main(String[] args) { MyService api = Feign.builder() .client(RibbonClient.create()) .target(MyService.class, "https://myAppProd"); } }
Http2Client将 Feign 的 http 请求定向到实现 HTTP/2 的Java11 New HTTP/2 Client 。
要将 New HTTP/2 Client 与 Feign 结合使用,请使用 Java SDK 11。然后,配置 Feign 以使用 Http2Client:
GitHub github = Feign.builder() .client(new Http2Client()) .target(GitHub.class, "https://api.github.com");
HystrixFeign配置Hystrix提供的断路器支持。
要将 Hystrix 与 Feign 一起使用,请将 Hystrix 模块添加到类路径中。然后使用HystrixFeign构建器:
public class Example { public static void main(String[] args) { MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd"); } }
SLF4JModule允许将 Feign 的日志记录定向到SLF4J,允许您轻松使用您选择的日志记录后端(Logback、Log4J 等)
要将 SLF4J 与 Feign 结合使用,请将 SLF4J 模块和您选择的 SLF4J 绑定添加到您的类路径中。然后,配置 Feign 使用 Slf4jLogger:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .logger(new Slf4jLogger()) .logLevel(Level.FULL) .target(GitHub.class, "https://api.github.com"); } }
Feign.builder()允许您指定其他配置,例如如何解码响应。
如果接口中的任何方法返回除Response, String,byte[]或之外的类型void,则需要配置非默认Decoder.
以下是配置 JSON 解码的方法(使用扩展feign-gson):
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); } }
如果您需要在将响应提供给解码器之前对其进行预处理,则可以使用mapAndDecode构建器方法。一个示例用例是处理仅提供 jsonp 服务的 API,您可能需要在将 jsonp 发送到您选择的 Json 解码器之前解开包装:
public class Example { public static void main(String[] args) { JsonpApi jsonpApi = Feign.builder() .mapAndDecode((response, type) -> jsopUnwrap(response, type), new GsonDecoder()) .target(JsonpApi.class, "https://some-jsonp-api.com"); } }
如果接口中的任何方法返回类型Stream,则需要配置一个StreamDecoder.
以下是如何在没有委托解码器的情况下配置流解码器:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(StreamDecoder.create((r, t) -> { BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); return bufferedReader.lines().iterator(); })) .target(GitHub.class, "https://api.github.com"); } }
以下是如何使用委托解码器配置流解码器:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(StreamDecoder.create((r, t) -> { BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); return bufferedReader.lines().iterator(); }, (r, t) -> "this is delegate decoder")) .target(GitHub.class, "https://api.github.com"); } }
将请求正文发送到服务器的最简单方法是定义一个POST方法,该方法具有String或byte[]参数,但不带任何注释。您可能需要添加Content-Type标头。
interface LoginClient { @RequestLine("POST /") @Headers("Content-Type: application/json") void login(String content); }public class Example { public static void main(String[] args) { client.login("{"user_name": "denominator", "password": "secret"}"); } }
通过配置Encoder,您可以发送类型安全的请求正文。这是使用扩展的示例feign-gson:
static class Credentials { final String user_name; final String password;Credentials(String user_name, String password) { this.user_name = user_name; this.password = password; } }
interface LoginClient { @RequestLine("POST /") void login(Credentials creds); }
public class Example { public static void main(String[] args) { LoginClient client = Feign.builder() .encoder(new GsonEncoder()) .target(LoginClient.class, "https://foo.com");
<span class="pl-s1">client</span>.<span class="pl-en">login</span>(<span class="pl-k">new</span> <span class="pl-smi">Credentials</span>(<span class="pl-s">"denominator"</span>, <span class="pl-s">"secret"</span>));} }
该@Body注释指示使用 注释的参数来扩展的模板@Param。您可能需要添加Content-Type标头。
interface LoginClient {@RequestLine("POST /") @Headers("Content-Type: application/xml") @Body("<login "user_name"="{user_name}" "password"="{password}"/>") void xml(@Param("user_name") String user, @Param("password") String password);
@RequestLine("POST /") @Headers("Content-Type: application/json") // json curly braces must be escaped! @Body("%7B"user_name": "{user_name}", "password": "{password}"%7D") void json(@Param("user_name") String user, @Param("password") String password); }
public class Example { public static void main(String[] args) { client.xml("denominator", "secret"); // <login "user_name"="denominator" "password"="secret"/> client.json("denominator", "secret"); // {"user_name": "denominator", "password": "secret"} } }
Feign 支持在请求上设置标头,作为 api 的一部分或作为客户端的一部分,具体取决于用例。
如果特定接口或调用应始终设置某些标头值,则将标头定义为 api 的一部分是有意义的。
可以使用注释在 api 接口或方法上设置静态标头@Headers。
@Headers("Accept: application/json") interface BaseApi<V> { @Headers("Content-Type: application/json") @RequestLine("PUT /api/{key}") void put(@Param("key") String key, V value); }
方法可以使用@Headers.
public interface Api { @RequestLine("POST /") @Headers("X-Ping: {token}") void post(@Param("token") String token); }
如果标头字段键和值都是动态的,并且可能的键的范围无法提前知道,并且同一 api/客户端中的不同方法调用之间可能会有所不同(例如自定义元数据标头字段,例如“x-amz- meta-*”或“x-goog-meta-*”),可以使用 Map 参数进行注释,以HeaderMap构造使用映射内容作为其标头参数的查询。
public interface Api { @RequestLine("POST /") void post(@HeaderMap Map<String, Object> headerMap); }
这些方法将标头条目指定为 api 的一部分,并且在构建 Feign 客户端时不需要任何自定义。
要为 Target 上的每个请求方法自定义标头,可以使用 RequestInterceptor。 RequestInterceptors 可以在 Target 实例之间共享,并且应该是线程安全的。 RequestInterceptors 应用于 Target 上的所有请求方法。
如果您需要按方法自定义,则需要自定义 Target,因为 RequestInterceptor 无法访问当前方法元数据。
有关使用 设置标头的示例RequestInterceptor,请参阅 参考资料Request Interceptors部分。
标头可以设置为自定义的一部分Target。
static class DynamicAuthTokenTarget<T> implements Target<T> { public DynamicAuthTokenTarget(Class<T> clazz, UrlAndTokenProvider provider, ThreadLocal<String> requestIdProvider);<span class="pl-c1">@</span><span class="pl-c1">Override</span> <span class="pl-k">public</span> <span class="pl-smi">Request</span> <span class="pl-en">apply</span>(<span class="pl-smi">RequestTemplate</span> <span class="pl-s1">input</span>) { <span class="pl-smi">TokenIdAndPublicURL</span> <span class="pl-s1">urlAndToken</span> = <span class="pl-s1">provider</span>.<span class="pl-en">get</span>(); <span class="pl-k">if</span> (<span class="pl-s1">input</span>.<span class="pl-en">url</span>().<span class="pl-en">indexOf</span>(<span class="pl-s">"http"</span>) != <span class="pl-c1">0</span>) { <span class="pl-s1">input</span>.<span class="pl-en">insert</span>(<span class="pl-c1">0</span>, <span class="pl-s1">urlAndToken</span>.<span class="pl-s1">publicURL</span>); } <span class="pl-s1">input</span>.<span class="pl-en">header</span>(<span class="pl-s">"X-Auth-Token"</span>, <span class="pl-s1">urlAndToken</span>.<span class="pl-s1">tokenId</span>); <span class="pl-s1">input</span>.<span class="pl-en">header</span>(<span class="pl-s">"X-Request-ID"</span>, <span class="pl-s1">requestIdProvider</span>.<span class="pl-en">get</span>()); <span class="pl-k">return</span> <span class="pl-s1">input</span>.<span class="pl-en">request</span>(); }}
public class Example { public static void main(String[] args) { Bank bank = Feign.builder() .target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider)); } }
这些方法取决于Feign 客户端构建时的自定义RequestInterceptor或设置,并且可以用作在每个客户端的所有 api 调用上设置标头的方法。Target这对于执行一些操作非常有用,例如在每个客户端的所有 api 请求的标头中设置身份验证令牌。当在调用 api 调用的线程上进行 api 调用时,这些方法就会运行,这允许在调用时以上下文特定的方式动态设置标头——例如,线程本地存储可用于根据调用线程设置不同的标头值,这对于为请求设置特定于线程的跟踪标识符等事情很有用。
在许多情况下,服务的 api 遵循相同的约定。 Feign 通过单继承接口支持这种模式。
考虑这个例子:
interface BaseAPI { @RequestLine("GET /health") String health();@RequestLine("GET /all") List<Entity> all(); }
您可以定义并定位特定的 api,继承基本方法。
interface CustomAPI extends BaseAPI { @RequestLine("GET /custom") String custom(); }
在许多情况下,资源表示也是一致的。因此,基本 api 接口支持类型参数。
@Headers("Accept: application/json") interface BaseApi<V> {@RequestLine("GET /api/{key}") V get(@Param("key") String key);
@RequestLine("GET /api") List<V> list();
@Headers("Content-Type: application/json") @RequestLine("PUT /api/{key}") void put(@Param("key") String key, V value); }
interface FooApi extends BaseApi<Foo> { }
interface BarApi extends BaseApi<Bar> { }
您可以通过设置Logger.这是最简单的方法:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(new GsonDecoder()) .logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log")) .logLevel(Logger.Level.FULL) .target(GitHub.class, "https://api.github.com"); } }
关于 JavaLogger 的注意事项:避免使用默认
JavaLogger()构造函数 - 它已被标记为已弃用,并将很快被删除。
SLF4JLogger(见上文)也可能令人感兴趣。
要过滤掉授权或令牌等敏感信息,请覆盖方法shouldLogRequestHeader或shouldLogResponseHeader.
当您需要更改所有请求时,无论其目标是什么,您都需要配置一个RequestInterceptor.例如,如果您充当中介,您可能想要传播X-Forwarded-For标头。
static class ForwardedForInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { template.header("X-Forwarded-For", "origin.host.com"); } }public class Example { public static void main(String[] args) { Bank bank = Feign.builder() .decoder(accountDecoder) .requestInterceptor(new ForwardedForInterceptor()) .target(Bank.class, "https://api.examplebank.com"); } }
拦截器的另一个常见示例是身份验证,例如使用内置的BasicAuthRequestInterceptor.
public class Example { public static void main(String[] args) { Bank bank = Feign.builder() .decoder(accountDecoder) .requestInterceptor(new BasicAuthRequestInterceptor(username, password)) .target(Bank.class, "https://api.examplebank.com"); } }
Param基于其扩展注释的参数toString。通过指定自定义Param.Expander,用户可以控制此行为,例如格式化日期。
public interface Api { @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date); }
可以使用 Map 参数进行注释,以QueryMap构造使用映射内容作为其查询参数的查询。
public interface Api { @RequestLine("GET /find") V find(@QueryMap Map<String, Object> queryMap); }
这也可以用于使用 .POJO 对象从 POJO 对象生成查询参数QueryMapEncoder。
public interface Api { @RequestLine("GET /find") V find(@QueryMap CustomPojo customPojo); }
当以这种方式使用时,如果不指定自定义QueryMapEncoder,将使用成员变量名称作为查询参数名称来生成查询映射。您可以CustomPojo使用注释来注释特定字段,@Param以便为查询参数指定不同的名称。以下POJO将生成“/find?name={name}&number={number}®ion_id={regionId}”的查询参数(不保证包含的查询参数的顺序,并且像往常一样,如果任何值为null,它将被遗漏)。
public class CustomPojo { private final String name; private final int number; @Param("region_id") private final String regionId;public CustomPojo (String name, int number, String regionId) { this.name = name; this.number = number; this.regionId = regionId; } }
要设置自定义QueryMapEncoder:
public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .queryMapEncoder(new MyCustomQueryMapEncoder()) .target(MyApi.class, "https://api.hostname.com"); } }
当使用@QueryMap注释对象时,默认编码器使用反射来检查提供的对象字段,以将对象值扩展为查询字符串。如果您希望使用 getter 和 setter 方法构建查询字符串(如 Java Beans API 中所定义),请使用 BeanQueryMapEncoder
public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .queryMapEncoder(new BeanQueryMapEncoder()) .target(MyApi.class, "https://api.hostname.com"); } }
如果您需要更多地控制处理意外响应,Feign 实例可以ErrorDecoder通过构建器注册自定义。
public class Example { public static void main(String[] args) { MyApi myApi = Feign.builder() .errorDecoder(new MyErrorDecoder()) .target(MyApi.class, "https://api.hostname.com"); } }
所有导致 HTTP 状态不在 2xx 范围内的响应都将触发ErrorDecodersdecode方法,允许您处理响应、将失败包装到自定义异常中或执行任何其他处理。如果您想再次重试请求,请抛出RetryableException.这将调用注册的
Retryer.
默认情况下,IOException无论 HTTP 方法如何,Feign 都会自动重试,将它们视为与网络相关的瞬态异常以及RetryableException从ErrorDecoder.要自定义此行为,请Retryer通过构建器注册自定义实例。
以下示例演示如何刷新令牌并ErrorDecoder在Retryer收到 401 响应时重试。
public class Example { public static void main(String[] args) { var github = Feign.builder() .decoder(new GsonDecoder()) .retryer(new MyRetryer(100, 3)) .errorDecoder(new MyErrorDecoder()) .target(Github.class, "https://api.github.com");<span class="pl-smi">var</span> <span class="pl-s1">contributors</span> = <span class="pl-s1">github</span>.<span class="pl-en">contributors</span>(<span class="pl-s">"foo"</span>, <span class="pl-s">"bar"</span>, <span class="pl-s">"invalid_token"</span>); <span class="pl-k">for</span> (<span class="pl-smi">var</span> <span class="pl-s1">contributor</span> : <span class="pl-s1">contributors</span>) { <span class="pl-smi">System</span>.<span class="pl-s1">out</span>.<span class="pl-en">println</span>(<span class="pl-s1">contributor</span>.<span class="pl-s1">login</span> + <span class="pl-s">" "</span> + <span class="pl-s1">contributor</span>.<span class="pl-s1">contributions</span>); } } <span class="pl-k">static</span> <span class="pl-k">class</span> <span class="pl-smi">MyErrorDecoder</span> <span class="pl-k">implements</span> <span class="pl-smi">ErrorDecoder</span> { <span class="pl-k">private</span> <span class="pl-k">final</span> <span class="pl-smi">ErrorDecoder</span> <span class="pl-s1">defaultErrorDecoder</span> = <span class="pl-k">new</span> <span class="pl-smi">Default</span>(); <span class="pl-c1">@</span><span class="pl-c1">Override</span> <span class="pl-k">public</span> <span class="pl-smi">Exception</span> <span class="pl-en">decode</span>(<span class="pl-smi">String</span> <span class="pl-s1">methodKey</span>, <span class="pl-smi">Response</span> <span class="pl-s1">response</span>) { <span class="pl-c">// wrapper 401 to RetryableException in order to retry</span> <span class="pl-k">if</span> (<span class="pl-s1">response</span>.<span class="pl-en">status</span>() == <span class="pl-c1">401</span>) { <span class="pl-k">return</span> <span class="pl-k">new</span> <span class="pl-smi">RetryableException</span>(<span class="pl-s1">response</span>.<span class="pl-en">status</span>(), <span class="pl-s1">response</span>.<span class="pl-en">reason</span>(), <span class="pl-s1">response</span>.<span class="pl-en">request</span>().<span class="pl-en">httpMethod</span>(), <span class="pl-c1">null</span>, <span class="pl-s1">response</span>.<span class="pl-en">request</span>()); } <span class="pl-k">return</span> <span class="pl-s1">defaultErrorDecoder</span>.<span class="pl-en">decode</span>(<span class="pl-s1">methodKey</span>, <span class="pl-s1">response</span>); } } <span class="pl-k">static</span> <span class="pl-k">class</span> <span class="pl-smi">MyRetryer</span> <span class="pl-k">implements</span> <span class="pl-smi">Retryer</span> { <span class="pl-k">private</span> <span class="pl-k">final</span> <span class="pl-smi">long</span> <span class="pl-s1">period</span>; <span class="pl-k">private</span> <span class="pl-k">final</span> <span class="pl-smi">int</span> <span class="pl-s1">maxAttempts</span>; <span class="pl-k">private</span> <span class="pl-smi">int</span> <span class="pl-s1">attempt</span> = <span class="pl-c1">1</span>; <span class="pl-k">public</span> <span class="pl-smi">MyRetryer</span>(<span class="pl-smi">long</span> <span class="pl-s1">period</span>, <span class="pl-smi">int</span> <span class="pl-s1">maxAttempts</span>) { <span class="pl-smi">this</span>.<span class="pl-s1">period</span> = <span class="pl-s1">period</span>; <span class="pl-smi">this</span>.<span class="pl-s1">maxAttempts</span> = <span class="pl-s1">maxAttempts</span>; } <span class="pl-c1">@</span><span class="pl-c1">Override</span> <span class="pl-k">public</span> <span class="pl-smi">void</span> <span class="pl-en">continueOrPropagate</span>(<span class="pl-smi">RetryableException</span> <span class="pl-s1">e</span>) { <span class="pl-k">if</span> (++<span class="pl-s1">attempt</span> > <span class="pl-s1">maxAttempts</span>) { <span class="pl-k">throw</span> <span class="pl-s1">e</span>; } <span class="pl-k">if</span> (<span class="pl-s1">e</span>.<span class="pl-en">status</span>() == <span class="pl-c1">401</span>) { <span class="pl-c">// remove Authorization first, otherwise Feign will add a new Authorization header</span> <span class="pl-c">// cause github responses a 400 bad request</span> <span class="pl-s1">e</span>.<span class="pl-en">request</span>().<span class="pl-en">requestTemplate</span>().<span class="pl-en">removeHeader</span>(<span class="pl-s">"Authorization"</span>); <span class="pl-s1">e</span>.<span class="pl-en">request</span>().<span class="pl-en">requestTemplate</span>().<span class="pl-en">header</span>(<span class="pl-s">"Authorization"</span>, <span class="pl-s">"Bearer "</span> + <span class="pl-en">getNewToken</span>()); <span class="pl-k">try</span> { <span class="pl-smi">Thread</span>.<span class="pl-en">sleep</span>(<span class="pl-s1">period</span>); } <span class="pl-k">catch</span> (<span class="pl-smi">InterruptedException</span> <span class="pl-s1">ex</span>) { <span class="pl-k">throw</span> <span class="pl-s1">e</span>; } } <span class="pl-k">else</span> { <span class="pl-k">throw</span> <span class="pl-s1">e</span>; } } <span class="pl-c">// Access an external api to obtain new token</span> <span class="pl-c">// In this example, we can simply return a fixed token to demonstrate how Retryer works</span> <span class="pl-k">private</span> <span class="pl-smi">String</span> <span class="pl-en">getNewToken</span>() { <span class="pl-k">return</span> <span class="pl-s">"newToken"</span>; } <span class="pl-c1">@</span><span class="pl-c1">Override</span> <span class="pl-k">public</span> <span class="pl-smi">Retryer</span> <span class="pl-en">clone</span>() { <span class="pl-k">return</span> <span class="pl-k">new</span> <span class="pl-smi">MyRetryer</span>(<span class="pl-s1">period</span>, <span class="pl-s1">maxAttempts</span>); }}
Retryer负责确定是否应该通过返回 atrue或
false从方法中进行重试。将为每次执行创建continueOrPropagate(RetryableException e); 一个实例,允许您在需要时维护每个请求之间的状态。RetryerClient
如果确定重试不成功,RetryException则抛出最后一次。要抛出导致重试失败的原始原因,请使用该exceptionPropagationPolicy()选项构建您的 Feign 客户端。
如果您需要将错误视为成功并返回结果而不是抛出异常,那么您可以使用ResponseInterceptor.
作为示例,Feign 包含一个简单RedirectionInterceptor的可用于从重定向响应中提取位置标头。
public interface Api { // returns a 302 response @RequestLine("GET /location") String location(); }public class MyApp { public static void main(String[] args) { // Configure the HTTP client to ignore redirection Api api = Feign.builder() .options(new Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, false)) .responseInterceptor(new RedirectionInterceptor()) .target(Api.class, "https://redirect.example.com"); } }
默认情况下,feign 不会收集任何指标。
但是,可以向任何假客户端添加指标收集功能。
指标功能提供了一流的指标 API,用户可以利用该 API 来深入了解请求/响应生命周期。
public class MyApp { public static void main(String[] args) { GitHub github = Feign.builder() .addCapability(new Metrics4Capability()) .target(GitHub.class, "https://api.github.com");github.contributors("OpenFeign", "feign"); // metrics will be available from this point onwards
} }
public class MyApp { public static void main(String[] args) { GitHub github = Feign.builder() .addCapability(new Metrics5Capability()) .target(GitHub.class, "https://api.github.com");github.contributors("OpenFeign", "feign"); // metrics will be available from this point onwards
} }
public class MyApp { public static void main(String[] args) { GitHub github = Feign.builder() .addCapability(new MicrometerCapability()) .target(GitHub.class, "https://api.github.com");github.contributors("OpenFeign", "feign"); // metrics will be available from this point onwards
} }
Feign 的目标接口可能具有静态或默认方法(如果使用 Java 8+)。这些允许 Feign 客户端包含底层 API 未明确定义的逻辑。例如,静态方法可以轻松指定常见的客户端构建配置;默认方法可用于编写查询或定义默认参数。
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);@RequestLine("GET /users/{username}/repos?sort={sort}") List<Repo> repos(@Param("username") String owner, @Param("sort") String sort);
default List<Repo> repos(String owner) { return repos(owner, "full_name"); }
/** * Lists all contributors for all repos owned by a user. */ default List<Contributor> contributors(String user) { MergingContributorList contributors = new MergingContributorList(); for(Repo repo : this.repos(owner)) { contributors.addAll(this.contributors(user, repo.getName())); } return contributors.mergeResult(); }
static GitHub connect() { return Feign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com"); } }
Feign 10.8 引入了一个新的构建器AsyncFeign,允许方法返回CompletableFuture实例。
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo); }public class MyApp { public static void main(String... args) { GitHub github = AsyncFeign.builder() .decoder(new GsonDecoder()) .target(GitHub.class, "https://api.github.com");
<span class="pl-c">// Fetch and print a list of the contributors to this library.</span> <span class="pl-smi">CompletableFuture</span><<span class="pl-smi">List</span><<span class="pl-smi">Contributor</span>>> <span class="pl-s1">contributors</span> = <span class="pl-s1">github</span>.<span class="pl-en">contributors</span>(<span class="pl-s">"OpenFeign"</span>, <span class="pl-s">"feign"</span>); <span class="pl-k">for</span> (<span class="pl-smi">Contributor</span> <span class="pl-s1">contributor</span> : <span class="pl-s1">contributors</span>.<span class="pl-en">get</span>(<span class="pl-c1">1</span>, <span class="pl-smi">TimeUnit</span>.<span class="pl-c1">SECONDS</span>)) { <span class="pl-smi">System</span>.<span class="pl-s1">out</span>.<span class="pl-en">println</span>(<span class="pl-s1">contributor</span>.<span class="pl-s1">login</span> + <span class="pl-s">" ("</span> + <span class="pl-s1">contributor</span>.<span class="pl-s1">contributions</span> + <span class="pl-s">")"</span>); }} }
初始实现包括 2 个异步客户端:
AsyncClient.DefaultAsyncApacheHttp5Client
将所有 feign 库保持在同一版本对于避免不兼容的二进制文件至关重要。使用外部依赖项时,确保仅存在一个版本可能很棘手。
考虑到这一点,feign build 会生成一个名为 的模块,feign-bom该模块锁定所有模块的版本feign-*。
物料清单是一个特殊的 POM 文件,它将已知有效并经过测试可协同工作的依赖项版本分组。这将减少开发人员必须测试不同版本兼容性的痛苦,并减少版本不匹配的机会。
以下是 feign BOM 文件的一个示例。
<project>...
<dependencyManagement> <dependencies> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-bom</artifactId> <version>??feign.version??</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>