Feign é um fichário de cliente Java para HTTP inspirado em Retrofit, JAXRS-2.0 e WebSocket. O primeiro objetivo de Feign era reduzir a complexidade de vincular o Denominator uniformemente às APIs HTTP, independentemente do ReSTfulness.
Feign usa ferramentas como Jersey e CXF para escrever clientes Java para serviços ReST ou SOAP. Além disso, Feign permite que você escreva seu próprio código em bibliotecas http como Apache HC. Feign conecta seu código a APIs http com sobrecarga mínima e código por meio de decodificadores personalizáveis e tratamento de erros, que podem ser gravados em qualquer API http baseada em texto.
Feign funciona processando anotações em uma solicitação padronizada. Os argumentos são aplicados a esses modelos de maneira direta antes da saída. Embora o Feign esteja limitado ao suporte a APIs baseadas em texto, ele simplifica drasticamente aspectos do sistema, como a reprodução de solicitações. Além disso, o Feign facilita o teste de unidade de suas conversões sabendo disso.
Feign 10.xe superior são construídos em Java 8 e devem funcionar em Java 9, 10 e 11. Para aqueles que precisam de compatibilidade com JDK 6, use Feign 9.x
Este é um mapa com os principais recursos atuais fornecidos pela feign:
Facilitando os clientes da API
Logger
Logger
para aderir mais a estruturas como SLF4J, fornecendo um modelo mental comum para registro no Feign. Este modelo será usado pela própria Feign e fornecerá uma orientação mais clara sobre como o Logger
será usado.Retry
a refatoração da APIRetry
para oferecer suporte às condições fornecidas pelo usuário e melhor controle sobre as políticas de retirada. Isso pode resultar em alterações significativas não compatíveis com versões anteriores CompletableFuture
Future
e gerenciamento de executores para o ciclo de vida de solicitação/resposta. A implementação exigirá alterações significativas não compatíveis com versões anteriores . No entanto, esse recurso é necessário antes que a execução reativa possa ser considerada.java.util.concurrent.Flow
.A biblioteca fingida está disponível no Maven Central.
< dependency >
< groupId >io.github.openfeign</ groupId >
< artifactId >feign-core</ artifactId >
< version >??feign.version??</ version >
</ dependency >
O uso normalmente se parece com isto, uma adaptação da amostra canônica 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 + ")" );
}
}
}
As anotações Feign definem o Contract
entre a interface e como o cliente subjacente deve funcionar. O contrato padrão da Feign define as seguintes anotações:
Anotação | Alvo de interface | Uso |
---|---|---|
@RequestLine | Método | Define o HttpMethod e UriTemplate para solicitação. Expressions , valores entre chaves {expression} são resolvidos usando seus parâmetros anotados @Param correspondentes. |
@Param | Parâmetro | Define uma variável de template, cujo valor será usado para resolver o template Expression , pelo nome fornecido como valor de anotação. Se o valor estiver faltando, ele tentará obter o nome do nome do parâmetro do método bytecode (se o código foi compilado com o sinalizador -parameters ). |
@Headers | Método, Tipo | Define um HeaderTemplate ; uma variação de um UriTemplate . que usa valores anotados @Param para resolver as Expressions correspondentes. Quando usado em Type , o modelo será aplicado a todas as solicitações. Quando usado em um Method , o modelo será aplicado apenas ao método anotado. |
@QueryMap | Parâmetro | Define um Map de pares nome-valor, ou POJO, para expandir em uma string de consulta. |
@HeaderMap | Parâmetro | Define um Map de pares nome-valor, para expandir em Http Headers |
@Body | Método | Define um Template , semelhante a UriTemplate e HeaderTemplate , que usa valores anotados @Param para resolver as Expressions correspondentes. |
Substituindo a linha de solicitação
Se houver necessidade de direcionar uma solicitação para um host diferente daquele fornecido quando o cliente Feign foi criado, ou se você quiser fornecer um host de destino para cada solicitação, inclua um parâmetro
java.net.URI
e Feign usará esse valor como o destino da solicitação.@ RequestLine ( "POST /repos/{owner}/{repo}/issues" ) void createIssue ( URI host , Issue issue , @ Param ( "owner" ) String owner , @ Param ( "repo" ) String repo );
Expressions
Feign representam Expressões de String Simples (Nível 1) conforme definido pelo Modelo URI - RFC 6570. Expressions
são expandidas usando seus parâmetros de método anotados Param
correspondentes.
Exemplo
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" );
}
}
As expressões devem ser colocadas entre chaves {}
e podem conter padrões de expressão regular, separados por dois pontos :
para restringir valores resolvidos. owner
do exemplo deve estar em ordem alfabética. {owner:[a-zA-Z]*}
Os modelos RequestLine
e QueryMap
seguem a especificação URI Template - RFC 6570 para modelos de Nível 1, que especifica o seguinte:
encoded
por meio de uma anotação @Param
.Também temos suporte limitado para expressões de estilo de caminho de nível 3, com as seguintes restrições:
Exemplos:
{;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 ;
}
}
Se owners
no exemplo acima forem definidos como Matt, Jeff, Susan
, o uri será expandido para /repos;owners=Matt;owners=Jeff;owners=Susan
Para obter mais informações, consulte RFC 6570, Seção 3.2.7
Expressões indefinidas são expressões em que o valor da expressão é um null
explícito ou nenhum valor é fornecido. De acordo com o modelo URI - RFC 6570, é possível fornecer um valor vazio para uma expressão. Quando Feign resolve uma expressão, ele primeiro determina se o valor está definido; se estiver, o parâmetro de consulta permanecerá. Se a expressão for indefinida, o parâmetro de consulta será removido. Veja abaixo uma análise completa.
String vazia
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
parameters . put ( "param" , "" );
this . demoClient . test ( parameters );
}
Resultado
http://localhost:8080/test?param=
Ausente
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
this . demoClient . test ( parameters );
}
Resultado
http://localhost:8080/test
Indefinido
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
parameters . put ( "param" , null );
this . demoClient . test ( parameters );
}
Resultado
http://localhost:8080/test
Consulte Uso avançado para obter mais exemplos.
E quanto às barras?
/
Os modelos @RequestLine não codificam barras
/
caracteres por padrão. Para alterar esse comportamento, defina a propriedadedecodeSlash
no@RequestLine
comofalse
.
E mais?
+
De acordo com a especificação do URI, um sinal
+
é permitido nos segmentos de caminho e de consulta de um URI; no entanto, o tratamento do símbolo na consulta pode ser inconsistente. Em alguns sistemas legados, o+
é equivalente ao espaço a. Feign adota a abordagem dos sistemas modernos, onde um símbolo+
não deve representar um espaço e é explicitamente codificado como%2B
quando encontrado em uma string de consulta.Se você deseja usar
+
como espaço, use o literalcaractere ou codifique o valor diretamente como
%20
A anotação @Param
possui um expander
de propriedades opcional que permite controle completo sobre a expansão do parâmetro individual. A propriedade expander
deve fazer referência a uma classe que implementa a interface Expander
:
public interface Expander {
String expand ( Object value );
}
O resultado deste método segue as mesmas regras mencionadas acima. Se o resultado for null
ou uma sequência vazia, o valor será omitido. Se o valor não estiver codificado em pct, será. Consulte Expansão @Param personalizada para obter mais exemplos.
Os modelos Headers
e HeaderMap
seguem as mesmas regras da Expansão de Parâmetros de Solicitação com as seguintes alterações:
Consulte Cabeçalhos para obter exemplos.
Uma nota sobre os parâmetros
@Param
e seus nomes :Todas as expressões com o mesmo nome, independentemente de sua posição em
@RequestLine
,@QueryMap
,@BodyTemplate
ou@Headers
serão resolvidas com o mesmo valor. No exemplo a seguir, o valor decontentType
será usado para resolver o cabeçalho e a expressão de caminho:public interface ContentService { @ RequestLine ( "GET /api/documents/{contentType}" ) @ Headers ( "Accept: {contentType}" ) String getDocumentByType ( @ Param ( "contentType" ) String type ); }Tenha isso em mente ao projetar suas interfaces.
Os modelos Body
seguem as mesmas regras da Expansão de Parâmetros de Solicitação com as seguintes alterações:
Encoder
antes de ser colocado no corpo da solicitação.Content-Type
deve ser especificado. Veja modelos de corpo para exemplos. Feign possui vários aspectos que podem ser customizados.
Para casos simples, você pode usar Feign.builder()
para construir uma interface API com seus componentes personalizados.
Para configuração de solicitação, você pode usar options(Request.Options options)
em target()
para definir connectTimeout, connectTimeoutUnit, readTimeout, readTimeoutUnit, followRedirects.
Por exemplo:
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 pode produzir várias interfaces API. Eles são definidos como Target<T>
(padrão HardCodedTarget<T>
), que permite a descoberta dinâmica e a decoração de solicitações antes da execução.
Por exemplo, o padrão a seguir pode decorar cada solicitação com a URL atual e o token de autenticação do serviço de identidade.
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 inclui exemplos de clientes GitHub e Wikipedia. O projeto do denominador também pode ser eliminado para Feign na prática. Particularmente, veja seu daemon de exemplo.
A Feign pretende funcionar bem com outras ferramentas de código aberto. Módulos são bem-vindos para integração com seus projetos favoritos!
Gson inclui um codificador e decodificador que você pode usar com uma API JSON.
Adicione GsonEncoder
e/ou GsonDecoder
ao seu Feign.Builder
assim:
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 inclui um codificador e um decodificador que você pode usar com uma API JSON.
Adicione JacksonEncoder
e/ou JacksonDecoder
ao seu Feign.Builder
assim:
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" );
}
}
Para o Jackson Jr mais leve, use JacksonJrEncoder
e JacksonJrDecoder
do Módulo Jackson Jr.
Moshi inclui um codificador e decodificador que você pode usar com uma API JSON. Adicione MoshiEncoder
e/ou MoshiDecoder
ao seu Feign.Builder
assim:
GitHub github = Feign . builder ()
. encoder ( new MoshiEncoder ())
. decoder ( new MoshiDecoder ())
. target ( GitHub . class , "https://api.github.com" );
SaxDecoder permite decodificar XML de uma forma compatível com JVM normal e também com ambientes Android.
Aqui está um exemplo de como configurar a análise de resposta do 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 inclui um codificador e um decodificador que você pode usar com uma API XML.
Adicione JAXBEncoder
e/ou JAXBDecoder
ao seu Feign.Builder
assim:
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 inclui um codificador e um decodificador que você pode usar com uma API XML.
Este módulo adiciona suporte para codificação e decodificação de objetos SOAP Body via JAXB e SOAPMessage. Ele também fornece recursos de decodificação SOAPFault agrupando-os no javax.xml.ws.soap.SOAPFaultException
original, de modo que você só precisará capturar SOAPFaultException
para lidar com SOAPFault.
Adicione SOAPEncoder
e/ou SOAPDecoder
ao seu Feign.Builder
assim:
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" );
}
}
NB: você também pode precisar adicionar SOAPErrorDecoder
se falhas SOAP forem retornadas em resposta com códigos http de erro (4xx, 5xx, ...)
fastjson2 inclui um codificador e decodificador que você pode usar com uma API JSON.
Adicione Fastjson2Encoder
e/ou Fastjson2Decoder
ao seu Feign.Builder
assim:
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 substitui o processamento de anotações para usar os padrão fornecidos pela especificação JAX-RS. Atualmente, isso está direcionado à especificação 1.1.
Aqui está o exemplo acima reescrito para usar 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 direciona as solicitações http de Feign para OkHttp, o que permite SPDY e melhor controle de rede.
Para usar OkHttp com Feign, adicione o módulo OkHttp ao seu classpath. Em seguida, configure o Feign para usar o 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 substitui a resolução de URL do cliente Feign, adicionando roteamento inteligente e recursos de resiliência fornecidos pelo Ribbon.
A integração exige que você passe o nome do cliente da faixa de opções como parte do host da URL, por exemplo myAppProd
.
public class Example {
public static void main ( String [] args ) {
MyService api = Feign . builder ()
. client ( RibbonClient . create ())
. target ( MyService . class , "https://myAppProd" );
}
}
Http2Client direciona as solicitações http de Feign para Java11 New HTTP/2 Client que implementa HTTP/2.
Para usar o novo cliente HTTP/2 com Feign, use Java SDK 11. Em seguida, configure o Feign para usar o Http2Client:
GitHub github = Feign . builder ()
. client ( new Http2Client ())
. target ( GitHub . class , "https://api.github.com" );
HystrixFeign configura suporte de disjuntor fornecido pela Hystrix.
Para usar Hystrix com Feign, adicione o módulo Hystrix ao seu classpath. Em seguida, use o construtor HystrixFeign
:
public class Example {
public static void main ( String [] args ) {
MyService api = HystrixFeign . builder (). target ( MyService . class , "https://myAppProd" );
}
}
SLF4JModule permite direcionar o log do Feign para o SLF4J, permitindo que você use facilmente um backend de log de sua escolha (Logback, Log4J, etc.)
Para usar SLF4J com Feign, adicione o módulo SLF4J e uma ligação SLF4J de sua escolha ao seu classpath. Em seguida, configure o Feign para usar o 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()
permite especificar configurações adicionais, como decodificar uma resposta.
Se algum método em sua interface retornar tipos além de Response
, String
, byte[]
ou void
, você precisará configurar um Decoder
não padrão.
Veja como configurar a decodificação JSON (usando a extensão 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" );
}
}
Se precisar pré-processar a resposta antes de fornecê-la ao decodificador, você pode usar o método construtor mapAndDecode
. Um exemplo de caso de uso é lidar com uma API que atende apenas jsonp. Talvez você precise desembrulhar o jsonp antes de enviá-lo para o decodificador Json de sua escolha:
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" );
}
}
Se algum método em sua interface retornar o tipo Stream
, você precisará configurar um StreamDecoder
.
Veja como configurar o decodificador Stream sem decodificador delegado:
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" );
}
}
Veja como configurar o decodificador Stream com decodificador delegado:
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" );
}
}
A maneira mais simples de enviar um corpo de solicitação a um servidor é definir um método POST
que possua um parâmetro String
ou byte[]
sem nenhuma anotação. Provavelmente você precisará adicionar um cabeçalho 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 " }" );
}
}
Ao configurar um Encoder
, você pode enviar um corpo de solicitação com segurança de tipo. Aqui está um exemplo usando a extensão 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" ));
}
}
A anotação @Body
indica um modelo para expandir usando parâmetros anotados com @Param
. Provavelmente você precisará adicionar um cabeçalho 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 suporta cabeçalhos de configurações em solicitações como parte da API ou como parte do cliente, dependendo do caso de uso.
Nos casos em que interfaces ou chamadas específicas devem sempre ter determinados valores de cabeçalho definidos, faz sentido definir cabeçalhos como parte da API.
Cabeçalhos estáticos podem ser definidos em uma interface ou método API usando a anotação @Headers
.
@ Headers ( "Accept: application/json" )
interface BaseApi < V > {
@ Headers ( "Content-Type: application/json" )
@ RequestLine ( "PUT /api/{key}" )
void put ( @ Param ( "key" ) String key , V value );
}
Os métodos podem especificar conteúdo dinâmico para cabeçalhos estáticos usando expansão de variável em @Headers
.
public interface Api {
@ RequestLine ( "POST /" )
@ Headers ( "X-Ping: {token}" )
void post ( @ Param ( "token" ) String token );
}
Nos casos em que as chaves e os valores do campo de cabeçalho são dinâmicos e o intervalo de chaves possíveis não pode ser conhecido antecipadamente e pode variar entre diferentes chamadas de método na mesma API/cliente (por exemplo, campos de cabeçalho de metadados personalizados, como "x-amz- meta-*" ou "x-goog-meta-*"), um parâmetro Map pode ser anotado com HeaderMap
para construir uma consulta que usa o conteúdo do mapa como seus parâmetros de cabeçalho.
public interface Api {
@ RequestLine ( "POST /" )
void post ( @ HeaderMap Map < String , Object > headerMap );
}
Essas abordagens especificam entradas de cabeçalho como parte da API e não requerem nenhuma customização ao construir o cliente Feign.
Para personalizar cabeçalhos para cada método de solicitação em um Target, um RequestInterceptor pode ser usado. RequestInterceptors podem ser compartilhados entre instâncias do Target e devem ser thread-safe. RequestInterceptors são aplicados a todos os métodos de solicitação em um Target.
Se você precisar de personalização por método, um Target personalizado será necessário, pois um RequestInterceptor não tem acesso aos metadados do método atual.
Para obter um exemplo de configuração de cabeçalhos usando um RequestInterceptor
, consulte a seção Request Interceptors
.
Os cabeçalhos podem ser definidos como parte de um Target
personalizado.
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 ));
}
}
Essas abordagens dependem do RequestInterceptor
ou Target
personalizado definido no cliente Feign quando ele é construído e podem ser usadas como uma forma de definir cabeçalhos em todas as chamadas de API por cliente. Isso pode ser útil para fazer coisas como definir um token de autenticação no cabeçalho de todas as solicitações de API por cliente. Os métodos são executados quando a chamada da API é feita no thread que invoca a chamada da API, o que permite que os cabeçalhos sejam definidos dinamicamente no momento da chamada e de uma maneira específica do contexto - por exemplo, o armazenamento local do thread pode ser usado para defina valores de cabeçalho diferentes dependendo do thread de chamada, o que pode ser útil para coisas como definir identificadores de rastreamento específicos do thread para solicitações.
Para especificar o cabeçalho Content-Length: 0
ao fazer uma solicitação com corpo vazio, a propriedade do sistema sun.net.http.allowRestrictedHeaders
deve ser definida como true
Caso contrário, o cabeçalho Content-Length
não será adicionado.
Em muitos casos, as APIs de um serviço seguem as mesmas convenções. Feign suporta esse padrão por meio de interfaces de herança única.
Considere o exemplo:
interface BaseAPI {
@ RequestLine ( "GET /health" )
String health ();
@ RequestLine ( "GET /all" )
List < Entity > all ();
}
Você pode definir e direcionar uma API específica, herdando os métodos base.
interface CustomAPI extends BaseAPI {
@ RequestLine ( "GET /custom" )
String custom ();
}
Em muitos casos, as representações de recursos também são consistentes. Por esse motivo, os parâmetros de tipo são suportados na interface API base.
@ 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 > { }
Você pode registrar as mensagens HTTP de e para o destino configurando um Logger
. Esta é a maneira mais fácil de fazer isso:
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" );
}
}
Uma observação sobre JavaLogger : Evite usar o construtor
JavaLogger()
padrão - ele foi marcado como obsoleto e será removido em breve.
O SLF4JLogger (veja acima) também pode ser interessante.
Para filtrar informações confidenciais, como autorização ou métodos de substituição de tokens shouldLogRequestHeader
ou shouldLogResponseHeader
.
Quando você precisar alterar todas as solicitações, independentemente do destino, configure um RequestInterceptor
. Por exemplo, se você estiver agindo como intermediário, talvez queira propagar o cabeçalho 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" );
}
}
Outro exemplo comum de interceptador seria a autenticação, como o uso do BasicAuthRequestInterceptor
integrado.
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" );
}
}
Os parâmetros anotados com Param
se expandem com base em seu toString
. Ao especificar um Param.Expander
personalizado, os usuários podem controlar esse comportamento, por exemplo, formatando datas.
public interface Api {
@ RequestLine ( "GET /?since={date}" ) Result list ( @ Param ( value = "date" , expander = DateToMillis . class ) Date date );
}
Um parâmetro Map pode ser anotado com QueryMap
para construir uma consulta que usa o conteúdo do mapa como seus parâmetros de consulta.
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap Map < String , Object > queryMap );
}
Isso também pode ser usado para gerar os parâmetros de consulta de um objeto POJO usando um QueryMapEncoder
.
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap CustomPojo customPojo );
}
Quando usado dessa maneira, sem especificar um QueryMapEncoder
customizado, o mapa de consulta será gerado usando nomes de variáveis de membro como nomes de parâmetros de consulta. Você pode anotar um campo específico de CustomPojo
com a anotação @Param
para especificar um nome diferente para o parâmetro de consulta. O seguinte POJO irá gerar parâmetros de consulta de "/find?name={name}&number={number}®ion_id={regionId}" (a ordem dos parâmetros de consulta incluídos não é garantida e, como de costume, se algum valor for nulo, será deixado de fora).
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 ;
}
}
Para configurar um QueryMapEncoder
personalizado:
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. queryMapEncoder ( new MyCustomQueryMapEncoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Ao anotar objetos com @QueryMap, o codificador padrão usa reflexão para inspecionar objetos fornecidos. Campos para expandir os valores dos objetos em uma string de consulta. Se você preferir que a string de consulta seja construída usando métodos getter e setter, conforme definido na API Java Beans, use BeanQueryMapEncoder
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. queryMapEncoder ( new BeanQueryMapEncoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Se você precisar de mais controle sobre o tratamento de respostas inesperadas, as instâncias Feign podem registrar um ErrorDecoder
personalizado por meio do construtor.
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. errorDecoder ( new MyErrorDecoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Todas as respostas que resultem em um status HTTP fora do intervalo 2xx acionarão o método decode
do ErrorDecoder
, permitindo que você manipule a resposta, envolva a falha em uma exceção personalizada ou execute qualquer processamento adicional. Se você quiser tentar novamente a solicitação, lance um RetryableException
. Isso invocará o Retryer
registrado.
Feign, por padrão, tentará automaticamente IOException
s, independentemente do método HTTP, tratando-os como exceções transitórias relacionadas à rede e qualquer RetryableException
lançada de um ErrorDecoder
. Para customizar esse comportamento, registre uma instância customizada Retryer
por meio do construtor.
O exemplo a seguir mostra como atualizar o token e tentar novamente com ErrorDecoder
e Retryer
quando receber uma resposta 401.
public class Example {
public static void main ( String [] args ) {
var github = Feign . builder ()
. decoder ( new GsonDecoder ())
. retryer ( new MyRetryer ( 100 , 3 ))
. errorDecoder ( new MyErrorDecoder ())
. target ( Github . class , "https://api.github.com" );
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
s são responsáveis por determinar se uma nova tentativa deve ocorrer retornando um true
ou false
do método continueOrPropagate(RetryableException e);
Uma instância Retryer
será criada para cada execução Client
, permitindo manter o estado entre cada solicitação, se desejado.
Se a nova tentativa for considerada malsucedida, a última RetryException
será lançada. Para eliminar a causa original que levou à nova tentativa malsucedida, crie seu cliente Feign com a opção exceptionPropagationPolicy()
.
Se você precisar tratar o que de outra forma seria um erro como um sucesso e retornar um resultado em vez de lançar uma exceção, você poderá usar um ResponseInterceptor
.
Como exemplo, Feign inclui um RedirectionInterceptor
simples que pode ser usado para extrair o cabeçalho de localização das respostas de redirecionamento.
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" );
}
}
Por padrão, feign não coletará nenhuma métrica.
Porém, é possível adicionar recursos de coleta de métricas a qualquer cliente falso.
Os Metric Capabilities fornecem uma API de métricas de primeira classe que os usuários podem aproveitar para obter insights sobre o ciclo de vida de solicitação/resposta.
Uma nota sobre módulos de métricas :
Todas as integrações métricas são construídas em módulos separados e não estão disponíveis no módulo
feign-core
. Você precisará adicioná-los às suas dependências.
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
}
}
As interfaces alvo do Feign podem ter métodos estáticos ou padrão (se estiver usando Java 8+). Isso permite que os clientes Feign contenham lógica que não é expressamente definida pela API subjacente. Por exemplo, os métodos estáticos facilitam a especificação de configurações comuns de compilação do cliente; métodos padrão podem ser usados para compor consultas ou definir parâmetros padrão.
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
O Feign 10.8 introduz um novo construtor AsyncFeign
que permite que métodos retornem instâncias 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 + ")" );
}
}
}
A implementação inicial inclui 2 clientes assíncronos:
AsyncClient.Default
AsyncApacheHttp5Client
Manter todas as bibliotecas falsas na mesma versão é essencial para evitar binários incompatíveis. Ao consumir dependências externas, pode ser complicado garantir que apenas uma versão esteja presente.
Com isso em mente, o feign build gera um módulo chamado feign-bom
que bloqueia as versões de todos os módulos feign-*
.
A lista de materiais é um arquivo POM especial que agrupa versões de dependências conhecidas por serem válidas e testadas para funcionarem juntas. Isso reduzirá a dor dos desenvolvedores em ter que testar a compatibilidade de diferentes versões e reduzirá as chances de incompatibilidades de versões.
Aqui está um exemplo da aparência de um arquivo BOM falso.
< 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 >
Este módulo adiciona suporte para codificação de formulários application/x-www-form-urlencoded e multipart/form-data .
Inclua a dependência em seu aplicativo:
Maven :
< dependencies >
...
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form</ artifactId >
< version >4.0.0</ version >
</ dependency >
...
</ dependencies >
Gradil :
compile ' io.github.openfeign.form:feign-form:4.0.0 '
A extensão feign-form
depende do OpenFeign
e de suas versões concretas :
feign-form
anteriores a 3.5.0 funcionam com versões OpenFeign
9.* ;feign-form
, o módulo funciona com versões OpenFeign
10.1.0 e superiores.IMPORTANTE: não há compatibilidade com versões anteriores e nenhuma garantia de que as versões do
feign-form
após 3.5.0 funcionem comOpenFeign
antes de 10.* .OpenFeign
foi refatorado na 10ª versão, então a melhor abordagem é usar as versões mais recentesOpenFeign
efeign-form
.
Notas:
spring-cloud-openfeign usa OpenFeign
9.* até v2.0.3.RELEASE e usa 10.* depois. De qualquer forma, a dependência já possui uma versão feign-form
adequada, consulte a dependência pom, portanto você não precisa especificá-la separadamente;
spring-cloud-starter-feign
é uma dependência obsoleta e sempre usa as versões 9.* do OpenFeign
.
Adicione FormEncoder
ao seu Feign.Builder
assim:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ())
. target ( SomeApi . class , "http://api.some.org" );
Além disso, você pode decorar o codificador existente, por exemplo JsonEncoder assim:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ( new JacksonEncoder ()))
. target ( SomeApi . class , "http://api.some.org" );
E use-os juntos:
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 );
}
Você pode especificar dois tipos de formulários de codificação por cabeçalho 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 ;
}
}
No exemplo acima, o método sendPhoto
usa o parâmetro photo
usando três tipos diferentes suportados.
File
usará a extensão do File para detectar o Content-Type
;byte[]
usará application/octet-stream
como Content-Type
;FormData
usará o Content-Type
e fileName
do FormData
; FormData
é um objeto personalizado que envolve um byte[]
e define um Content-Type
e fileName
como este:
FormData formData = new FormData ( "image/png" , "filename.png" , myDataAsByteArray );
someApi . sendPhoto ( true , formData );
Você também pode usar o Form Encoder com Spring MultipartFile
e @FeignClient
.
Inclua as dependências no arquivo pom.xml do seu projeto:
< 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 ));
}
}
}
Ou, se você não precisar do codificador padrão do 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 ();
}
}
}
Obrigado ao tf-haotri-pham por seu recurso, que faz uso da biblioteca Apache commons-fileupload, que lida com a análise da resposta multipart. As partes dos dados do corpo são mantidas como matrizes de bytes na memória.
Para usar esse recurso, inclua SpringManyMultipartFilesReader na lista de conversores de mensagens para o Decoder e faça com que o cliente Feign retorne uma matriz de 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 ;
}
});
}
}
}