Feign は、Retrofit、JAXRS-2.0、および WebSocket からインスピレーションを得た Java から HTTP クライアントへのバインダーです。 Feign の最初の目標は、ReSTful に関係なく、Denominator を HTTP API に均一にバインドする複雑さを軽減することでした。
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 をリファクタリングして、Feign 内でのロギングのための共通のメンタル モデルを提供する SLF4J のようなフレームワークに近づけます。このモデルは Feign 自体によって全体的に使用され、 Logger
どのように使用されるかについてより明確な方向性を提供します。Retry
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 + ")" );
}
}
}
偽のアノテーションは、インターフェースと基礎となるクライアントがどのように動作するかの間のContract
を定義します。 Feign のデフォルトのコントラクトでは、次のアノテーションが定義されています。
注釈 | インターフェースターゲット | 使用法 |
---|---|---|
@RequestLine | 方法 | リクエストのHttpMethod とUriTemplate 定義します。 Expressions 、中括弧{expression} で囲まれた値は、対応する@Param アノテーション付きパラメータを使用して解決されます。 |
@Param | パラメータ | テンプレート変数を定義します。その値は、アノテーション値として指定された名前によって、対応するテンプレートExpression 解決するために使用されます。値が欠落している場合は、バイトコード メソッドのパラメーター名から名前を取得しようとします (コードが-parameters フラグを使用してコンパイルされている場合)。 |
@Headers | メソッド、タイプ | HeaderTemplate を定義します。 UriTemplate のバリエーション。 @Param アノテーション付きの値を使用して、対応するExpressions 解決します。 Type で使用すると、テンプレートはすべてのリクエストに適用されます。 Method で使用すると、テンプレートはアノテーションが付けられたメソッドにのみ適用されます。 |
@QueryMap | パラメータ | クエリ文字列に展開する名前と値のペアのMap (POJO) を定義します。 |
@HeaderMap | パラメータ | 名前と値のペアのMap を定義し、 Http Headers に展開します。 |
@Body | 方法 | UriTemplate やHeaderTemplate と同様に、 @Param 注釈付きの値を使用して対応するExpressions を解決するTemplate を定義します。 |
リクエストラインをオーバーライドする
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 テンプレート - レベル 1 テンプレートの RFC 6570 仕様に従い、次の内容を指定します。
@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 エンコードされていない場合は、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
をキャッチするだけで済みます。
次のように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 とより優れたネットワーク制御が可能になります。
Feign で OkHttp を使用するには、OkHttp モジュールをクラスパスに追加します。次に、OkHttpClient を使用するように Feign を構成します。
public class Example {
public static void main ( String [] args ) {
GitHub github = Feign . builder ()
. client ( new OkHttpClient ())
. target ( GitHub . class , "https://api.github.com" );
}
}
リボンクライアントは、Feign クライアントの URL 解決をオーバーライドし、リボンが提供するスマート ルーティングと復元機能を追加します。
統合では、リボン クライアント名を 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 新しい HTTP/2 クライアントに送信します。
Feign で新しい HTTP/2 クライアントを使用するには、Java SDK 11 を使用します。次に、Http2Client を使用するように Feign を構成します。
GitHub github = Feign . builder ()
. client ( new Http2Client ())
. target ( GitHub . class , "https://api.github.com" );
HystrixFeign は、Hystrix が提供するサーキット ブレーカーのサポートを構成します。
Feign で Hystrix を使用するには、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 など) を簡単に使用できるようになります。
Feign で SLF4J を使用するには、SLF4J モジュールと選択した SLF4J バインディングの両方をクラスパスに追加します。次に、Slf4jLogger を使用するように Feign を構成します。
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" );
}
}
応答を Decoder に渡す前に前処理する必要がある場合は、 mapAndDecode
ビルダー メソッドを使用できます。ユースケースの例は、jsonp のみを提供する API を処理する場合です。場合によっては、選択した Json デコーダーに送信する前に jsonp をラップ解除する必要があります。
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" );
}
}
リクエスト本文をサーバーに送信する最も簡単な方法は、注釈を付けずにString
またはbyte[]
パラメータを持つPOST
メソッドを定義することです。 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 クライアントの構築時にカスタマイズは必要ありません。
ターゲット上の各リクエスト メソッドのヘッダーをカスタマイズするには、RequestInterceptor を使用できます。 RequestInterceptor は Target インスタンス間で共有でき、スレッドセーフであることが期待されます。 RequestInterceptors は、ターゲット上のすべてのリクエスト メソッドに適用されます。
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" );
}
}
インターセプターのもう 1 つの一般的な例は、組み込みの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 );
}
これは、 QueryMapEncoder
を使用して POJO オブジェクトからクエリ パラメーターを生成するためにも使用できます。
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 アノテーションを付ける場合、デフォルトのエンコーダーはリフレクションを使用して、提供されたオブジェクトのフィールドを検査し、オブジェクトの値をクエリ文字列に展開します。 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" );
}
}
2xx 範囲外の HTTP ステータスとなるすべての応答は、 ErrorDecoder
のdecode
メソッドをトリガーし、応答を処理したり、失敗をカスタム例外にラップしたり、追加の処理を実行したりできるようにします。リクエストを再度再試行する場合は、 RetryableException
をスローします。これにより、登録されたRetryer
が呼び出されます。
Feign は、デフォルトで、HTTP メソッドに関係なく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
返すことによって、再試行が必要かどうかを決定する責任があります。 Retryer
インスタンスはClient
実行ごとに作成され、必要に応じて各リクエスト間の状態を維持できるようになります。
再試行が失敗したと判断された場合は、最後の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 を提供します。
メトリクスモジュールに関する注意:
すべてのメトリック統合は別個のモジュールに組み込まれており、
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 では、メソッドがCompletableFuture
インスタンスを返すことを可能にする新しいビルダーAsyncFeign
が導入されています。
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
互換性のないバイナリを避けるためには、すべての偽ライブラリを同じバージョンに保つことが不可欠です。外部依存関係を使用する場合、バージョンが 1 つだけ存在することを確認するのは難しい場合があります。
これを念頭に置いて、feign build はすべてのfeign-*
モジュールのバージョンをロックするfeign-bom
というモジュールを生成します。
部品表は、有効であることがわかっており、連携して動作することがテストされている依存関係バージョンをグループ化する特別な POM ファイルです。これにより、開発者がさまざまなバージョンの互換性をテストする手間が軽減され、バージョンの不一致が発生する可能性が減ります。
以下は、偽の 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
ヘッダーにより2種類のエンコード形式を指定できます。
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
メソッドは、サポートされている 3 つの異なるタイプを使用してphoto
パラメーターを使用しています。
File
ファイルの拡張子を使用してContent-Type
検出します。byte[]
Content-Type
としてapplication/octet-stream
使用します。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 ();
}
}
}
マルチパート応答の解析を処理する Apache commons-fileupload ライブラリを利用する機能については、tf-haotri-pham に感謝します。本体データ部分はバイト配列としてメモリに保持されます。
この機能を使用するには、Decoder のメッセージ コンバーターのリストに SpringManyMultipartFilesReader を含めて、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 ;
}
});
}
}
}