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客户端更轻松
Logger
API 重构Logger
API,使其更接近 SLF4J 等框架,为 Feign 中的日志记录提供通用的思维模型。 Feign 本身将自始至终使用该模型,并为如何使用Logger
提供更清晰的指导。Retry
API 重构Retry
API 以支持用户提供的条件并更好地控制回退策略。这可能会导致不向后兼容的重大更改CompletableFuture
支持异步执行Future
链接和执行器管理。实施将需要非向后兼容的重大更改。然而,在考虑响应式执行之前,需要此功能。java.util.concurrent.Flow
的本机实现。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" );
// Fetch and print a list of the contributors to this library.
List < Contributor > contributors = github . contributors ( "OpenFeign" , "feign" );
for ( Contributor contributor : contributors ) {
System . out . println ( contributor . login + " (" + contributor . contributions + ")" );
}
}
}
Feign 注释定义了接口之间的Contract
以及底层客户端应该如何工作。 Feign的默认合约定义了以下注解:
注解 | 接口目标 | 用法 |
---|---|---|
@RequestLine | 方法 | 定义请求的HttpMethod 和UriTemplate 。 Expressions ,用大括号{expression} 括起来的值使用其相应的@Param 注解参数进行解析。 |
@Param | 范围 | 定义一个模板变量,其值将用于解析相应的模板Expression ,其名称作为注释值提供。如果值丢失,它将尝试从字节码方法参数名称中获取名称(如果代码是使用-parameters 标志编译的)。 |
@Headers | 方法、类型 | 定义一个HeaderTemplate ; UriTemplate 的变体。使用@Param 注解值来解析相应的Expressions 。当用于Type 时,模板将应用于每个请求。当用于Method 时,模板将仅应用于带注释的方法。 |
@QueryMap | 范围 | 定义名称-值对(或 POJO)的Map ,以扩展为查询字符串。 |
@HeaderMap | 范围 | 定义名称-值对的Map ,以扩展为Http Headers |
@Body | 方法 | 定义一个Template ,类似于UriTemplate 和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 );
Feign Expressions
表示由 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" );
/* The owner and repository parameters will be used to expand the owner and repo expressions
* 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 模板规范,该规范指定以下内容:
@Param
注释标记为encoded
,则均经过 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 模板默认不编码斜杠
/
字符。要更改此行为,请将@RequestLine
上的decodeSlash
属性设置为false
。
加号呢?
+
根据 URI 规范,URI 的路径段和查询段中都允许使用
+
号,但是,查询中符号的处理可能不一致。在某些遗留系统中,+
相当于空格。 Feign 采用现代系统的方法,其中+
符号不应代表空格,并且在查询字符串中找到时显式编码为%2B
。如果您想使用
+
作为空格,则使用文字字符或直接将值编码为
%20
@Param
注释有一个可选的属性expander
允许完全控制单个参数的扩展。 expander
属性必须引用实现Expander
接口的类:
public interface Expander {
String expand ( Object value );
}
该方法的结果遵循上述相同的规则。如果结果为null
或空字符串,则省略该值。如果该值不是 pct 编码的,则它将是。有关更多示例,请参阅自定义 @Param 扩展。
Headers
和HeaderMap
模板遵循与请求参数扩展相同的规则,但有以下更改:
有关示例,请参阅标头。
关于
@Param
参数及其名称的注释:所有具有相同名称的表达式,无论它们在
@RequestLine
、@QueryMap
、@BodyTemplate
或@Headers
上的位置如何,都将解析为相同的值。在以下示例中,contentType
的值将用于解析标头和路径表达式:public interface ContentService { @ RequestLine ( "GET /api/documents/{contentType}" ) @ Headers ( "Accept: {contentType}" ) String getDocumentByType ( @ Param ( "contentType" ) String type ); }设计界面时请记住这一点。
Body
模板遵循与请求参数扩展相同的规则,但有以下更改:
Encoder
传递。Content-Type
标头。有关示例,请参阅正文模板。Feign 有几个方面可以定制。
对于简单的情况,您可以使用Feign.builder()
与您的自定义组件构建 API 接口。
对于请求设置,您可以在target()
上使用options(Request.Options options)
来设置 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 刮取。特别是,看看它的示例守护进程。
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 包装到原始javax.xml.ws.soap.SOAPFaultException
中来提供 SOAPFault 解码功能,这样您只需捕获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" );
}
}
注意:如果 SOAP 错误返回并带有错误 http 代码(4xx、5xx、...),您可能还需要添加SOAPErrorDecoder
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" );
}
}
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" );
client . login ( new Credentials ( "denominator" , "secret" ));
}
}
@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 的一部分是有意义的。
可以使用@Headers
注释在 api 接口或方法上设置静态标头。
@ 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 );
@ Override
public Request apply ( RequestTemplate input ) {
TokenIdAndPublicURL urlAndToken = provider . get ();
if ( input . url (). indexOf ( "http" ) != 0 ) {
input . insert ( 0 , urlAndToken . publicURL );
}
input . header ( "X-Auth-Token" , urlAndToken . tokenId );
input . header ( "X-Request-ID" , requestIdProvider . get ());
return input . request ();
}
}
public class Example {
public static void main ( String [] args ) {
Bank bank = Feign . builder ()
. target ( new DynamicAuthTokenTarget ( Bank . class , provider , requestIdProvider ));
}
}
这些方法取决于构建 Feign 客户端时设置的自定义RequestInterceptor
或Target
,并且可以用作在每个客户端的所有 api 调用上设置标头的方法。这对于执行一些操作非常有用,例如在每个客户端的所有 api 请求的标头中设置身份验证令牌。当在调用 api 调用的线程上进行 api 调用时,这些方法就会运行,这允许在调用时以上下文特定的方式动态设置标头——例如,线程本地存储可用于根据调用线程设置不同的标头值,这对于为请求设置特定于线程的跟踪标识符等事情很有用。
要在发出空正文请求时指定Content-Length: 0
标头,系统属性sun.net.http.allowRestrictedHeaders
应设置为true
如果不是,则不会添加Content-Length
标头。
在许多情况下,服务的 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
来记录进出目标的 http 消息。这是最简单的方法:
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 );
}
可以使用QueryMap
对 Map 参数进行注释,以构造使用映射内容作为其查询参数的查询。
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap Map < String , Object > queryMap );
}
这也可以用于使用QueryMapEncoder
从 POJO 对象生成查询参数。
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap CustomPojo customPojo );
}
当以这种方式使用时,如果不指定自定义QueryMapEncoder
,则将使用成员变量名称作为查询参数名称来生成查询映射。您可以使用@Param
注释来注释CustomPojo
的特定字段,以为查询参数指定不同的名称。以下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 范围内的响应都将触发ErrorDecoder
的decode
方法,允许您处理响应、将失败包装到自定义异常中或执行任何其他处理。如果您想再次重试请求,请抛出RetryableException
。这将调用注册的Retryer
。
默认情况下,无论 HTTP 方法如何,Feign 都会自动重试IOException
,将它们视为与网络相关的瞬态异常,以及从ErrorDecoder
抛出的任何RetryableException
。要自定义此行为,请通过构建器注册自定义Retryer
实例。
以下示例演示如何在收到 401 响应时刷新令牌并使用ErrorDecoder
和Retryer
重试。
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" );
var contributors = github . contributors ( "foo" , "bar" , "invalid_token" );
for ( var contributor : contributors ) {
System . out . println ( contributor . login + " " + contributor . contributions );
}
}
static class MyErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default ();
@ Override
public Exception decode ( String methodKey , Response response ) {
// wrapper 401 to RetryableException in order to retry
if ( response . status () == 401 ) {
return new RetryableException ( response . status (), response . reason (), response . request (). httpMethod (), null , response . request ());
}
return defaultErrorDecoder . decode ( methodKey , response );
}
}
static class MyRetryer implements Retryer {
private final long period ;
private final int maxAttempts ;
private int attempt = 1 ;
public MyRetryer ( long period , int maxAttempts ) {
this . period = period ;
this . maxAttempts = maxAttempts ;
}
@ Override
public void continueOrPropagate ( RetryableException e ) {
if (++ attempt > maxAttempts ) {
throw e ;
}
if ( e . status () == 401 ) {
// remove Authorization first, otherwise Feign will add a new Authorization header
// cause github responses a 400 bad request
e . request (). requestTemplate (). removeHeader ( "Authorization" );
e . request (). requestTemplate (). header ( "Authorization" , "Bearer " + getNewToken ());
try {
Thread . sleep ( period );
} catch ( InterruptedException ex ) {
throw e ;
}
} else {
throw e ;
}
}
// Access an external api to obtain new token
// In this example, we can simply return a fixed token to demonstrate how Retryer works
private String getNewToken () {
return "newToken" ;
}
@ Override
public Retryer clone () {
return new MyRetryer ( period , maxAttempts );
}
}
Retryer
负责通过从方法continueOrPropagate(RetryableException e);
返回true
或false
来确定是否应进行重试;将为每个Client
执行创建一个Retryer
实例,允许您根据需要维护每个请求之间的状态。
如果确定重试不成功,则会抛出最后一个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 来深入了解请求/响应生命周期。
关于 Metrics 模块的注释:
所有度量集成都构建在单独的模块中,并且在
feign-core
模块中不可用。您需要将它们添加到您的依赖项中。
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" );
}
}
CompletableFuture
异步执行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" );
// Fetch and print a list of the contributors to this library.
CompletableFuture < List < Contributor >> contributors = github . contributors ( "OpenFeign" , "feign" );
for ( Contributor contributor : contributors . get ( 1 , TimeUnit . SECONDS )) {
System . out . println ( contributor . login + " (" + contributor . contributions + ")" );
}
}
}
初始实现包括 2 个异步客户端:
AsyncClient.Default
AsyncApacheHttp5Client
将所有 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 >
该模块添加了对application/x-www-form-urlencoded和multipart/form-data表单编码的支持。
包含对您的应用程序的依赖项:
行长:
< dependencies >
...
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form</ artifactId >
< version >4.0.0</ version >
</ dependency >
...
</ dependencies >
摇篮:
compile ' io.github.openfeign.form:feign-form:4.0.0 '
feign-form
扩展依赖于OpenFeign
及其具体版本:
feign-form
版本都适用于OpenFeign
9.*版本;feign-form
的3.5.0版本开始,该模块可与OpenFeign
10.1.0及更高版本一起使用。重要提示:没有向后兼容性,也不能保证3.5.0之后的
feign-form
版本可以与10.*之前的OpenFeign
一起使用。OpenFeign
在第 10 版中进行了重构,因此最好的方法是使用最新的OpenFeign
和feign-form
版本。
笔记:
spring-cloud-openfeign 在v2.0.3.RELEASE之前使用OpenFeign
9.* ,之后使用10.* 。不管怎样,依赖已经有合适的feign-form
版本,参见依赖 pom,所以不需要单独指定;
spring-cloud-starter-feign
是一个已弃用的依赖项,它始终使用OpenFeign
的9.*版本。
将FormEncoder
添加到您的Feign.Builder
中,如下所示:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ())
. target ( SomeApi . class , "http://api.some.org" );
此外,您可以装饰现有的编码器,例如 JsonEncoder,如下所示:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ( new JacksonEncoder ()))
. target ( SomeApi . class , "http://api.some.org" );
并将它们一起使用:
interface SomeApi {
@ RequestLine ( "POST /json" )
@ Headers ( "Content-Type: application/json" )
void json ( Dto dto );
@ RequestLine ( "POST /form" )
@ Headers ( "Content-Type: application/x-www-form-urlencoded" )
void from ( @ Param ( "field1" ) String field1 , @ Param ( "field2" ) String [] values );
}
您可以通过Content-Type
标头指定两种类型的编码形式。
interface SomeApi {
@ RequestLine ( "POST /authorization" )
@ Headers ( "Content-Type: application/x-www-form-urlencoded" )
void authorization ( @ Param ( "email" ) String email , @ Param ( "password" ) String password );
// Group all parameters within a POJO
@ RequestLine ( "POST /user" )
@ Headers ( "Content-Type: application/x-www-form-urlencoded" )
void addUser ( User user );
class User {
Integer id ;
String name ;
}
}
interface SomeApi {
// File parameter
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( @ Param ( "is_public" ) Boolean isPublic , @ Param ( "photo" ) File photo );
// byte[] parameter
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( @ Param ( "is_public" ) Boolean isPublic , @ Param ( "photo" ) byte [] photo );
// FormData parameter
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( @ Param ( "is_public" ) Boolean isPublic , @ Param ( "photo" ) FormData photo );
// Group all parameters within a POJO
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( MyPojo pojo );
class MyPojo {
@ FormProperty ( "is_public" )
Boolean isPublic ;
File photo ;
}
}
在上面的示例中, sendPhoto
方法使用photo
参数,该参数使用三种不同的受支持类型。
File
将使用 File 的扩展名来检测Content-Type
;byte[]
将使用application/octet-stream
作为Content-Type
;FormData
将使用FormData
的Content-Type
和fileName
; FormData
是自定义对象,它包装了byte[]
并定义了Content-Type
和fileName
,如下所示:
FormData formData = new FormData ( "image/png" , "filename.png" , myDataAsByteArray );
someApi . sendPhoto ( true , formData );
您还可以将 Form Encoder 与 Spring MultipartFile
和@FeignClient
一起使用。
将依赖项包含到项目的 pom.xml 文件中:
< dependencies >
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form</ artifactId >
< version >4.0.0</ version >
</ dependency >
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form-spring</ artifactId >
< version >4.0.0</ version >
</ dependency >
</ dependencies >
@ FeignClient (
name = "file-upload-service" ,
configuration = FileUploadServiceClient . MultipartSupportConfig . class
)
public interface FileUploadServiceClient extends IFileUploadServiceClient {
public class MultipartSupportConfig {
@ Autowired
private ObjectFactory < HttpMessageConverters > messageConverters ;
@ Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder ( new SpringEncoder ( messageConverters ));
}
}
}
或者,如果您不需要 Spring 的标准编码器:
@ FeignClient (
name = "file-upload-service" ,
configuration = FileUploadServiceClient . MultipartSupportConfig . class
)
public interface FileUploadServiceClient extends IFileUploadServiceClient {
public class MultipartSupportConfig {
@ Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder ();
}
}
}
感谢 tf-haotri-pham 的功能,它利用 Apache commons-fileupload 库来处理多部分响应的解析。主体数据部分作为字节数组保存在内存中。
要使用此功能,请将 SpringManyMultipartFilesReader 包含在 Decoder 的消息转换器列表中,并让 Feign 客户端返回一个 MultipartFile 数组:
@ FeignClient (
name = "${feign.name}" ,
url = "${feign.url}"
configuration = DownloadClient . ClientConfiguration . class
)
public interface DownloadClient {
@ RequestMapping ( "/multipart/download/{fileId}" )
MultipartFile [] download ( @ PathVariable ( "fileId" ) String fileId );
class ClientConfiguration {
@ Autowired
private ObjectFactory < HttpMessageConverters > messageConverters ;
@ Bean
public Decoder feignDecoder () {
List < HttpMessageConverter <?>> springConverters =
messageConverters . getObject (). getConverters ();
List < HttpMessageConverter <?>> decoderConverters =
new ArrayList < HttpMessageConverter <?>>( springConverters . size () + 1 );
decoderConverters . addAll ( springConverters );
decoderConverters . add ( new SpringManyMultipartFilesReader ( 4096 ));
HttpMessageConverters httpMessageConverters = new HttpMessageConverters ( decoderConverters );
return new SpringDecoder ( new ObjectFactory < HttpMessageConverters >() {
@ Override
public HttpMessageConverters getObject () {
return httpMessageConverters ;
}
});
}
}
}