Feign es un enlazador de cliente de Java a HTTP inspirado en Retrofit, JAXRS-2.0 y WebSocket. El primer objetivo de Feign fue reducir la complejidad de vincular Denominator de manera uniforme a las API HTTP, independientemente de ReSTfulness.
Feign utiliza herramientas como Jersey y CXF para escribir clientes Java para servicios ReST o SOAP. Además, Feign le permite escribir su propio código sobre bibliotecas http como Apache HC. Feign conecta su código a las API http con una sobrecarga y código mínimos a través de decodificadores personalizables y manejo de errores, que se pueden escribir en cualquier API http basada en texto.
Feign funciona procesando anotaciones en una solicitud con plantilla. Los argumentos se aplican a estas plantillas de forma sencilla antes de generarlos. Aunque Feign se limita a admitir API basadas en texto, simplifica drásticamente aspectos del sistema como la reproducción de solicitudes. Además, Feign facilita la prueba unitaria de sus conversiones sabiendo esto.
Feign 10.x y superiores están construidos en Java 8 y deberían funcionar en Java 9, 10 y 11. Para aquellos que necesitan compatibilidad con JDK 6, utilice Feign 9.x
Este es un mapa con características clave actuales proporcionadas por fingir:
Hacer que los clientes API sean más fáciles
Logger
Logger
para adherirse más a marcos como SLF4J, proporcionando un modelo mental común para iniciar sesión en Feign. Este modelo será utilizado por el propio Feign en todo momento y proporcionará instrucciones más claras sobre cómo se utilizará el Logger
.Retry
la refactorización de APIRetry
para admitir las condiciones proporcionadas por el usuario y un mejor control sobre las políticas de retroceso. Esto puede resultar en cambios importantes no compatibles con versiones anteriores. CompletableFuture
Future
y la gestión de ejecutores para el ciclo de vida de solicitud/respuesta. La implementación requerirá cambios importantes no compatibles con versiones anteriores . Sin embargo, esta característica es necesaria antes de que se pueda considerar la ejecución reactiva.java.util.concurrent.Flow
.La biblioteca simulada está disponible en Maven Central.
< dependency >
< groupId >io.github.openfeign</ groupId >
< artifactId >feign-core</ artifactId >
< version >??feign.version??</ version >
</ dependency >
El uso normalmente se ve así, una adaptación del ejemplo canónico 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 + ")" );
}
}
}
Las anotaciones falsas definen el Contract
entre la interfaz y cómo debería funcionar el cliente subyacente. El contrato predeterminado de Feign define las siguientes anotaciones:
Anotación | Destino de la interfaz | Uso |
---|---|---|
@RequestLine | Método | Define HttpMethod y UriTemplate para la solicitud. Expressions , los valores entre llaves {expression} se resuelven utilizando sus correspondientes parámetros anotados @Param . |
@Param | Parámetro | Define una variable de plantilla, cuyo valor se utilizará para resolver la Expression de plantilla correspondiente, por el nombre proporcionado como valor de anotación. Si falta un valor, intentará obtener el nombre del nombre del parámetro del método de código de bytes (si el código se compiló con el indicador -parameters ). |
@Headers | Método, tipo | Define un HeaderTemplate ; una variación de UriTemplate . que utiliza valores anotados @Param para resolver las Expressions correspondientes. Cuando se utiliza en un Type , la plantilla se aplicará a cada solicitud. Cuando se usa en un Method , la plantilla se aplicará solo al método anotado. |
@QueryMap | Parámetro | Define un Map de pares nombre-valor, o POJO, para expandirse en una cadena de consulta. |
@HeaderMap | Parámetro | Define un Map de pares nombre-valor, para expandirlo a Http Headers |
@Body | Método | Define una Template , similar a UriTemplate y HeaderTemplate , que utiliza valores anotados @Param para resolver las Expressions correspondientes. |
Anulación de la línea de solicitud
Si es necesario dirigir una solicitud a un host diferente al proporcionado cuando se creó el cliente Feign, o si desea proporcionar un host de destino para cada solicitud, incluya un parámetro
java.net.URI
y Feign usará ese valor. como destino de la solicitud.@ RequestLine ( "POST /repos/{owner}/{repo}/issues" ) void createIssue ( URI host , Issue issue , @ Param ( "owner" ) String owner , @ Param ( "repo" ) String repo );
Expressions
simuladas representan expresiones de cadena simples (Nivel 1) según lo definido por la plantilla URI - RFC 6570. Expressions
se expanden utilizando sus parámetros de método anotados Param
correspondientes.
Ejemplo
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" );
}
}
Las expresiones deben estar entre llaves {}
y pueden contener patrones de expresión regulares, separados por dos puntos :
para restringir los valores resueltos. owner
del ejemplo debe ser alfabético. {owner:[a-zA-Z]*}
Las plantillas RequestLine
y QueryMap
siguen la plantilla URI: especificación RFC 6570 para plantillas de nivel 1, que especifica lo siguiente:
encoded
mediante una anotación @Param
.También tenemos soporte limitado para el nivel 3, expresiones de estilo de ruta, con las siguientes restricciones:
Ejemplos:
{;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 ;
}
}
Si owners
en el ejemplo anterior se definen como Matt, Jeff, Susan
, la uri se expandirá a /repos;owners=Matt;owners=Jeff;owners=Susan
Para obtener más información, consulte RFC 6570, Sección 3.2.7.
Las expresiones no definidas son expresiones en las que el valor de la expresión es un null
explícito o no se proporciona ningún valor. Según la plantilla de URI: RFC 6570, es posible proporcionar un valor vacío para una expresión. Cuando Feign resuelve una expresión, primero determina si el valor está definido; si lo está, el parámetro de consulta permanecerá. Si la expresión no está definida, se elimina el parámetro de consulta. Consulte a continuación para obtener un desglose completo.
Cadena vacía
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
parameters . put ( "param" , "" );
this . demoClient . test ( parameters );
}
Resultado
http://localhost:8080/test?param=
Desaparecido
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 avanzado para ver más ejemplos.
¿Qué pasa con las barras?
/
Las plantillas @RequestLine no codifican barras
/
caracteres de forma predeterminada. Para cambiar este comportamiento, establezca la propiedaddecodeSlash
en@RequestLine
enfalse
.
¿Qué pasa con el plus?
+
Según la especificación de URI, se permite un signo
+
tanto en la ruta como en los segmentos de consulta de un URI; sin embargo, el manejo del símbolo en la consulta puede ser inconsistente. En algunos sistemas heredados, el+
equivale al espacio a. Feign adopta el enfoque de los sistemas modernos, donde un símbolo+
no debe representar un espacio y se codifica explícitamente como%2B
cuando se encuentra en una cadena de consulta.Si desea utilizar
+
como espacio, utilice el literalcarácter o codificar el valor directamente como
%20
La anotación @Param
tiene un expander
de propiedades opcional que permite un control completo sobre la expansión del parámetro individual. La propiedad expander
debe hacer referencia a una clase que implemente la interfaz Expander
:
public interface Expander {
String expand ( Object value );
}
El resultado de este método sigue las mismas reglas expuestas anteriormente. Si el resultado es null
o una cadena vacía, el valor se omite. Si el valor no está codificado en pct, lo estará. Consulte Expansión @Param personalizada para ver más ejemplos.
Las plantillas Headers
y HeaderMap
siguen las mismas reglas que la expansión de parámetros de solicitud con las siguientes modificaciones:
Consulte Encabezados para ver ejemplos.
Una nota sobre los parámetros
@Param
y sus nombres :Todas las expresiones con el mismo nombre, independientemente de su posición en
@RequestLine
,@QueryMap
,@BodyTemplate
o@Headers
se resolverán con el mismo valor. En el siguiente ejemplo, el valor decontentType
se utilizará para resolver tanto el encabezado como la expresión de ruta:public interface ContentService { @ RequestLine ( "GET /api/documents/{contentType}" ) @ Headers ( "Accept: {contentType}" ) String getDocumentByType ( @ Param ( "contentType" ) String type ); }Tenga esto en cuenta al diseñar sus interfaces.
Las plantillas Body
siguen las mismas reglas que Solicitar expansión de parámetros con las siguientes modificaciones:
Encoder
antes de colocarse en el cuerpo de la solicitud.Content-Type
. Consulte Plantillas de cuerpo para ver ejemplos. Feign tiene varios aspectos que se pueden personalizar.
Para casos simples, puede usar Feign.builder()
para construir una interfaz API con sus componentes personalizados.
Para la configuración de solicitudes, puede usar options(Request.Options options)
en target()
para configurar connectTimeout, connectTimeoutUnit, readTimeout, readTimeoutUnit, followRedirects.
Por ejemplo:
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 puede producir múltiples interfaces API. Estos se definen como Target<T>
( HardCodedTarget<T>
predeterminado), que permite el descubrimiento dinámico y la decoración de solicitudes antes de su ejecución.
Por ejemplo, el siguiente patrón podría decorar cada solicitud con la URL actual y el token de autenticación del servicio de identidad.
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 incluye ejemplos de clientes de GitHub y Wikipedia. El proyecto denominador también se puede eliminar para Feign en la práctica. En particular, mire su demonio de ejemplo.
Feign tiene la intención de funcionar bien con otras herramientas de código abierto. ¡Los módulos pueden integrarse con sus proyectos favoritos!
Gson incluye un codificador y decodificador que puede usar con una API JSON.
Agregue GsonEncoder
y/o GsonDecoder
a su Feign.Builder
así:
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 incluye un codificador y decodificador que puede usar con una API JSON.
Agregue JacksonEncoder
y/o JacksonDecoder
a su Feign.Builder
así:
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 el Jackson Jr más liviano, use JacksonJrEncoder
y JacksonJrDecoder
del módulo Jackson Jr.
Moshi incluye un codificador y decodificador que puedes usar con una API JSON. Añade MoshiEncoder
y/o MoshiDecoder
a tu Feign.Builder
así:
GitHub github = Feign . builder ()
. encoder ( new MoshiEncoder ())
. decoder ( new MoshiDecoder ())
. target ( GitHub . class , "https://api.github.com" );
SaxDecoder le permite decodificar XML de una manera que sea compatible con entornos JVM normales y también con Android.
A continuación se muestra un ejemplo de cómo configurar el análisis de respuesta de 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 incluye un codificador y decodificador que puede utilizar con una API XML.
Agregue JAXBEncoder
y/o JAXBDecoder
a su Feign.Builder
así:
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 incluye un codificador y un decodificador que puede utilizar con una API XML.
Este módulo agrega soporte para codificar y decodificar objetos SOAP Body a través de JAXB y SOAPMessage. También proporciona capacidades de decodificación SOAPFault envolviéndolas en el javax.xml.ws.soap.SOAPFaultException
original, de modo que solo necesitará capturar SOAPFaultException
para poder manejar SOAPFault.
Agregue SOAPEncoder
y/o SOAPDecoder
a su Feign.Builder
así:
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: es posible que también necesite agregar SOAPErrorDecoder
si se devuelven fallas SOAP en respuesta con códigos http de error (4xx, 5xx, ...)
fastjson2 incluye un codificador y decodificador que puede usar con una API JSON.
Agregue Fastjson2Encoder
y/o Fastjson2Decoder
a su Feign.Builder
así:
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 anula el procesamiento de anotaciones para utilizar las estándar proporcionadas por la especificación JAX-RS. Actualmente, esto está dirigido a la especificación 1.1.
Aquí está el ejemplo anterior 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 dirige las solicitudes http de Feign a OkHttp, lo que permite SPDY y un mejor control de la red.
Para usar OkHttp con Feign, agregue el módulo OkHttp a su classpath. Luego, configure Feign para usar 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 anula la resolución de URL del cliente de Feign, agregando capacidades de resiliencia y enrutamiento inteligente proporcionadas por Ribbon.
La integración requiere que pase el nombre de su cliente de cinta como parte del host de la URL, por ejemplo myAppProd
.
public class Example {
public static void main ( String [] args ) {
MyService api = Feign . builder ()
. client ( RibbonClient . create ())
. target ( MyService . class , "https://myAppProd" );
}
}
Http2Client dirige las solicitudes http de Feign a Java11 Nuevo cliente HTTP/2 que implementa HTTP/2.
Para usar el nuevo cliente HTTP/2 con Feign, use Java SDK 11. Luego, configure Feign para usar Http2Client:
GitHub github = Feign . builder ()
. client ( new Http2Client ())
. target ( GitHub . class , "https://api.github.com" );
HystrixFeign configura el soporte del disyuntor proporcionado por Hystrix.
Para usar Hystrix con Feign, agregue el módulo Hystrix a su classpath. Luego use el constructor HystrixFeign
:
public class Example {
public static void main ( String [] args ) {
MyService api = HystrixFeign . builder (). target ( MyService . class , "https://myAppProd" );
}
}
SLF4JModule permite dirigir el registro de Feign a SLF4J, lo que le permite usar fácilmente un servidor de registro de su elección (Logback, Log4J, etc.)
Para usar SLF4J con Feign, agregue el módulo SLF4J y un enlace SLF4J de su elección a su classpath. Luego, configure Feign para usar 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()
le permite especificar configuraciones adicionales, como cómo decodificar una respuesta.
Si algún método en su interfaz devuelve tipos además de Response
, String
, byte[]
o void
, deberá configurar un Decoder
no predeterminado.
A continuación se explica cómo configurar la decodificación JSON (usando la extensión 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" );
}
}
Si necesita preprocesar la respuesta antes de entregársela al Decoder, puede utilizar el método de creación mapAndDecode
. Un caso de uso de ejemplo es tratar con una API que solo sirve jsonp; tal vez necesites desenvolver el jsonp antes de enviarlo al decodificador Json de tu elección:
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" );
}
}
Si algún método en su interfaz devuelve el tipo Stream
, deberá configurar un StreamDecoder
.
A continuación se explica cómo configurar el decodificador Stream sin 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" );
}
}
A continuación se explica cómo configurar el decodificador Stream con el 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" );
}
}
La forma más sencilla de enviar el cuerpo de una solicitud a un servidor es definir un método POST
que tenga un parámetro String
o byte[]
sin ninguna anotación. Probablemente necesitarás agregar un encabezado 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 " }" );
}
}
Al configurar un Encoder
, puede enviar un cuerpo de solicitud con seguridad de tipos. A continuación se muestra un ejemplo que utiliza la extensión 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" ));
}
}
La anotación @Body
indica una plantilla para expandir usando parámetros anotados con @Param
. Probablemente necesitarás agregar un encabezado 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 admite encabezados de configuración en solicitudes, ya sea como parte de la API o como parte del cliente, según el caso de uso.
En los casos en los que interfaces o llamadas específicas siempre deben tener ciertos valores de encabezado establecidos, tiene sentido definir encabezados como parte de la API.
Los encabezados estáticos se pueden configurar en una interfaz o método API utilizando la anotación @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 );
}
Los métodos pueden especificar contenido dinámico para encabezados estáticos usando expansión variable en @Headers
.
public interface Api {
@ RequestLine ( "POST /" )
@ Headers ( "X-Ping: {token}" )
void post ( @ Param ( "token" ) String token );
}
En los casos en los que tanto las claves como los valores del campo de encabezado son dinámicos y el rango de claves posibles no se puede conocer de antemano y puede variar entre diferentes llamadas a métodos en la misma API/cliente (por ejemplo, campos de encabezado de metadatos personalizados como "x-amz- meta-*" o "x-goog-meta-*"), se puede anotar un parámetro de mapa con HeaderMap
para construir una consulta que utilice el contenido del mapa como parámetros de encabezado.
public interface Api {
@ RequestLine ( "POST /" )
void post ( @ HeaderMap Map < String , Object > headerMap );
}
Estos enfoques especifican entradas de encabezado como parte de la API y no requieren ninguna personalización al crear el cliente Feign.
Para personalizar los encabezados de cada método de solicitud en un Target, se puede utilizar un RequestInterceptor. Los RequestInterceptores se pueden compartir entre instancias de Target y se espera que sean seguros para subprocesos. Los RequestInterceptors se aplican a todos los métodos de solicitud en un Target.
Si necesita una personalización por método, se requiere un objetivo personalizado, ya que RequestInterceptor no tiene acceso a los metadatos del método actual.
Para ver un ejemplo de cómo configurar encabezados usando RequestInterceptor
, consulte la sección Request Interceptors
.
Los encabezados se pueden configurar como parte de un 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 ));
}
}
Estos enfoques dependen de que el RequestInterceptor
o Target
personalizado se establezca en el cliente Feign cuando se crea y se pueden usar como una forma de configurar encabezados en todas las llamadas a la API por cliente. Esto puede resultar útil para hacer cosas como configurar un token de autenticación en el encabezado de todas las solicitudes de API por cliente. Los métodos se ejecutan cuando la llamada a la API se realiza en el subproceso que invoca la llamada a la API, lo que permite que los encabezados se establezcan dinámicamente en el momento de la llamada y de una manera específica del contexto; por ejemplo, el almacenamiento local del subproceso se puede utilizar para establezca diferentes valores de encabezado según el hilo que invoca, lo que puede ser útil para cosas como configurar identificadores de seguimiento específicos del hilo para solicitudes.
Para especificar Content-Length: 0
encabezado al realizar una solicitud con cuerpo vacío, la propiedad del sistema sun.net.http.allowRestrictedHeaders
debe establecerse en true
De lo contrario, no se agregará el encabezado Content-Length
.
En muchos casos, las API de un servicio siguen las mismas convenciones. Feign admite este patrón a través de interfaces de herencia única.
Considere el ejemplo:
interface BaseAPI {
@ RequestLine ( "GET /health" )
String health ();
@ RequestLine ( "GET /all" )
List < Entity > all ();
}
Puede definir y apuntar a una API específica, heredando los métodos base.
interface CustomAPI extends BaseAPI {
@ RequestLine ( "GET /custom" )
String custom ();
}
En muchos casos, las representaciones de recursos también son consistentes. Por este motivo, los parámetros de tipo se admiten en la interfaz 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 > { }
Puede registrar los mensajes http que van y vienen del destino configurando un Logger
. Esta es la forma más sencilla de hacerlo:
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" );
}
}
Una nota sobre JavaLogger : evite el uso del constructor
JavaLogger()
predeterminado; se marcó como obsoleto y se eliminará pronto.
El SLF4JLogger (ver arriba) también puede ser de interés.
Para filtrar información confidencial como autorización o tokens, los métodos de anulación shouldLogRequestHeader
o shouldLogResponseHeader
.
Cuando necesite cambiar todas las solicitudes, independientemente de su objetivo, querrá configurar un RequestInterceptor
. Por ejemplo, si actúa como intermediario, es posible que desee propagar el encabezado 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" );
}
}
Otro ejemplo común de interceptor sería la autenticación, como el uso del 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" );
}
}
Los parámetros anotados con Param
se expanden según su toString
. Al especificar un Param.Expander
personalizado, los usuarios pueden controlar este comportamiento, por ejemplo, formatear fechas.
public interface Api {
@ RequestLine ( "GET /?since={date}" ) Result list ( @ Param ( value = "date" , expander = DateToMillis . class ) Date date );
}
Se puede anotar un parámetro de mapa con QueryMap
para construir una consulta que utilice el contenido del mapa como parámetros de consulta.
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap Map < String , Object > queryMap );
}
Esto también se puede usar para generar los parámetros de consulta desde un objeto POJO usando un QueryMapEncoder
.
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap CustomPojo customPojo );
}
Cuando se usa de esta manera, sin especificar un QueryMapEncoder
personalizado, el mapa de consulta se generará utilizando nombres de variables miembro como nombres de parámetros de consulta. Puede anotar un campo específico de CustomPojo
con la anotación @Param
para especificar un nombre diferente al parámetro de consulta. El siguiente POJO generará parámetros de consulta de "/find?name={name}&number={number}®ion_id={regionId}" (el orden de los parámetros de consulta incluidos no está garantizado y, como de costumbre, si algún valor es nulo, será excluido).
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 un 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" );
}
}
Al anotar objetos con @QueryMap, el codificador predeterminado utiliza la reflexión para inspeccionar los campos de los objetos proporcionados para expandir los valores de los objetos en una cadena de consulta. Si prefiere que la cadena de consulta se cree utilizando los métodos getter y setter, como se define en la API de Java Beans, utilice BeanQueryMapEncoder.
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. queryMapEncoder ( new BeanQueryMapEncoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Si necesita más control sobre el manejo de respuestas inesperadas, las instancias de Feign pueden registrar un ErrorDecoder
personalizado a través del constructor.
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. errorDecoder ( new MyErrorDecoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Todas las respuestas que resulten en un estado HTTP que no esté en el rango 2xx activarán el método decode
de ErrorDecoder
, lo que le permitirá manejar la respuesta, incluir el error en una excepción personalizada o realizar cualquier procesamiento adicional. Si desea volver a intentar la solicitud, inicie una RetryableException
. Esto invocará al Retryer
registrado.
Feign, de forma predeterminada, reintentará automáticamente IOException
, independientemente del método HTTP, tratándolas como excepciones transitorias relacionadas con la red y cualquier RetryableException
lanzada desde un ErrorDecoder
. Para personalizar este comportamiento, registre una instancia Retryer
personalizada a través del constructor.
El siguiente ejemplo muestra cómo actualizar el token y volver a intentarlo con ErrorDecoder
y Retryer
cuando se recibe una respuesta 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 );
}
}
Los Retryer
son responsables de determinar si debe ocurrir un reintento al devolver true
o false
del método continueOrPropagate(RetryableException e);
Se creará una instancia Retryer
para cada ejecución Client
, lo que le permitirá mantener el estado entre cada solicitud si lo desea.
Si se determina que el reintento no tuvo éxito, se generará la última RetryException
. Para eliminar la causa original que provocó el reintento fallido, cree su cliente Feign con la opciónExceptionPropagationPolicy exceptionPropagationPolicy()
.
Si necesita tratar lo que de otro modo sería un error como un éxito y devolver un resultado en lugar de generar una excepción, entonces puede utilizar un ResponseInterceptor
.
Como ejemplo, Feign incluye un RedirectionInterceptor
simple que se puede usar para extraer el encabezado de ubicación de las respuestas de redireccionamiento.
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" );
}
}
De forma predeterminada, fingir no recopilará ninguna métrica.
Pero es posible agregar capacidades de recopilación de métricas a cualquier cliente falso.
Metric Capabilities proporciona una API de métricas de primera clase que los usuarios pueden aprovechar para obtener información sobre el ciclo de vida de solicitud/respuesta.
Una nota sobre los módulos de métricas :
Todas las integraciones métricas están integradas en módulos separados y no están disponibles en el módulo
feign-core
. Deberá agregarlos a sus dependencias.
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
}
}
Las interfaces a las que apunta Feign pueden tener métodos estáticos o predeterminados (si se usa Java 8+). Esto permite a los clientes de Feign contener lógica que no está expresamente definida por la API subyacente. Por ejemplo, los métodos estáticos facilitan la especificación de configuraciones de compilación de clientes comunes; Los métodos predeterminados se pueden utilizar para redactar consultas o definir parámetros predeterminados.
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 presenta un nuevo constructor AsyncFeign
que permite que los métodos devuelvan instancias 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 + ")" );
}
}
}
La implementación inicial incluye 2 clientes asíncronos:
AsyncClient.Default
AsyncApacheHttp5Client
Mantener todas las bibliotecas simuladas en la misma versión es esencial para evitar binarios incompatibles. Al consumir dependencias externas, puede resultar complicado asegurarse de que solo haya una versión presente.
Con eso en mente, fing build genera un módulo llamado feign-bom
que bloquea las versiones para todos los módulos feign-*
.
La lista de materiales es un archivo POM especial que agrupa versiones de dependencia que se sabe que son válidas y se ha probado que funcionan juntas. Esto reducirá la molestia de los desarrolladores de tener que probar la compatibilidad de diferentes versiones y reducirá las posibilidades de que las versiones no coincidan.
Aquí hay un ejemplo de cómo se ve el archivo BOM fingido.
< 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 agrega soporte para codificar formularios application/x-www-form-urlencoded y multipart/form-data .
Incluya la dependencia de su aplicación:
experto :
< dependencies >
...
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form</ artifactId >
< version >4.0.0</ version >
</ dependency >
...
</ dependencies >
Gradle :
compile ' io.github.openfeign.form:feign-form:4.0.0 '
La extensión feign-form
depende de OpenFeign
y sus versiones concretas :
feign-form
anteriores a 3.5.0 funcionan con las versiones OpenFeign
9.* ;feign-form
, el módulo funciona con las versiones OpenFeign
10.1.0 y superiores.IMPORTANTE: no hay compatibilidad con versiones anteriores ni garantía de que las versiones de
feign-form
posteriores a la 3.5.0 funcionen conOpenFeign
anteriores a la 10.* .OpenFeign
fue refactorizado en la décima versión, por lo que el mejor enfoque es utilizar las versiones más recientesOpenFeign
yfeign-form
.
Notas:
spring-cloud-openfeign usa OpenFeign
9.* hasta v2.0.3.RELEASE y usa 10.* después. De todos modos, la dependencia ya tiene una versión feign-form
adecuada, consulte el pom de dependencia, por lo que no es necesario especificarla por separado;
spring-cloud-starter-feign
es una dependencia obsoleta y siempre usa las versiones 9.* de OpenFeign
.
Agregue FormEncoder
a su Feign.Builder
así:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ())
. target ( SomeApi . class , "http://api.some.org" );
Además, puedes decorar el codificador existente, por ejemplo JsonEncoder, así:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ( new JacksonEncoder ()))
. target ( SomeApi . class , "http://api.some.org" );
Y úsalos 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 );
}
Puede especificar dos tipos de formularios de codificación por encabezado 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 ;
}
}
En el ejemplo anterior, el método sendPhoto
utiliza el parámetro photo
utilizando tres tipos diferentes admitidos.
File
utilizará la extensión del archivo para detectar el Content-Type
;byte[]
utilizará application/octet-stream
como Content-Type
;FormData
utilizará el Content-Type
y fileName
de FormData
; FormData
es un objeto personalizado que envuelve un byte[]
y define un Content-Type
y fileName
como este:
FormData formData = new FormData ( "image/png" , "filename.png" , myDataAsByteArray );
someApi . sendPhoto ( true , formData );
También puedes usar Form Encoder con Spring MultipartFile
y @FeignClient
.
Incluya las dependencias del archivo pom.xml de su proyecto:
< 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 ));
}
}
}
O, si no necesita el codificador estándar de 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 ();
}
}
}
Gracias a tf-haotri-pham por su función, que utiliza la biblioteca Apache commons-fileupload, que maneja el análisis de la respuesta multiparte. Las partes de datos del cuerpo se mantienen como matrices de bytes en la memoria.
Para usar esta función, incluya SpringManyMultipartFilesReader en la lista de convertidores de mensajes para Decoder y haga que el cliente Feign devuelva una 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 ;
}
});
}
}
}