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。
這是 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 定義的簡單字串表達式( Expressions
1) 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。
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 ;
}
});
}
}
}