Feign est un classeur client Java vers HTTP inspiré de Retrofit, JAXRS-2.0 et WebSocket. Le premier objectif de Feign était de réduire la complexité de la liaison uniforme de Denominator aux API HTTP, quel que soit ReSTfulness.
Feign utilise des outils comme Jersey et CXF pour écrire des clients Java pour les services ReST ou SOAP. De plus, Feign vous permet d'écrire votre propre code sur des bibliothèques http telles qu'Apache HC. Feign connecte votre code aux API http avec un minimum de surcharge et de code via des décodeurs personnalisables et une gestion des erreurs, qui peuvent être écrites dans n'importe quelle API http basée sur du texte.
Feign fonctionne en traitant les annotations dans une requête modélisée. Les arguments sont appliqués à ces modèles de manière simple avant la sortie. Bien que Feign se limite à prendre en charge les API textuelles, il simplifie considérablement les aspects du système tels que la relecture des requêtes. De plus, Feign facilite le test unitaire de vos conversions en sachant cela.
Feign 10.x et supérieur sont construits sur Java 8 et devraient fonctionner sur Java 9, 10 et 11. Pour ceux qui ont besoin de la compatibilité JDK 6, veuillez utiliser Feign 9.x
Il s'agit d'une carte avec les principales fonctionnalités actuelles fournies par feign :
Rendre les clients API plus faciles
Logger
Logger
pour adhérer plus étroitement à des frameworks tels que SLF4J fournissant un modèle mental commun pour la journalisation dans Feign. Ce modèle sera utilisé par Feign lui-même tout au long et fournira des instructions plus claires sur la manière dont l' Logger
sera utilisé.Retry
la refactorisation de l'APIRetry
pour prendre en charge les conditions fournies par l'utilisateur et mieux contrôler les politiques d'attente. Cela peut entraîner des modifications avec rupture non rétrocompatibles. CompletableFuture
Future
et la gestion de l’exécuteur pour le cycle de vie des demandes/réponses. La mise en œuvre nécessitera des modifications avec rupture non rétrocompatibles . Cependant, cette fonctionnalité est requise avant de pouvoir envisager une exécution réactive.java.util.concurrent.Flow
.La bibliothèque feinte est disponible sur Maven Central.
< dependency >
< groupId >io.github.openfeign</ groupId >
< artifactId >feign-core</ artifactId >
< version >??feign.version??</ version >
</ dependency >
L'utilisation ressemble généralement à ceci, une adaptation de l'exemple canonique 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 + ")" );
}
}
}
Les annotations simulées définissent le Contract
entre l'interface et la manière dont le client sous-jacent doit fonctionner. Le contrat par défaut de Feign définit les annotations suivantes :
Annotation | Cible d'interface | Usage |
---|---|---|
@RequestLine | Méthode | Définit le HttpMethod et UriTemplate pour la requête. Expressions , les valeurs entourées d'accolades {expression} sont résolues à l'aide de leurs paramètres annotés @Param correspondants. |
@Param | Paramètre | Définit une variable de modèle, dont la valeur sera utilisée pour résoudre le modèle Expression correspondant, par le nom fourni comme valeur d'annotation. Si la valeur est manquante, il essaiera d'obtenir le nom à partir du nom du paramètre de la méthode bytecode (si le code a été compilé avec l'indicateur -parameters ). |
@Headers | Méthode, type | Définit un HeaderTemplate ; une variation sur un UriTemplate . qui utilise les valeurs annotées @Param pour résoudre les Expressions correspondantes. Lorsqu'il est utilisé sur un Type , le modèle sera appliqué à chaque requête. Lorsqu'il est utilisé sur un Method , le modèle s'appliquera uniquement à la méthode annotée. |
@QueryMap | Paramètre | Définit une Map de paires nom-valeur, ou POJO, à développer en une chaîne de requête. |
@HeaderMap | Paramètre | Définit une Map de paires nom-valeur, à développer en Http Headers |
@Body | Méthode | Définit un Template , similaire à un UriTemplate et HeaderTemplate , qui utilise les valeurs annotées @Param pour résoudre les Expressions correspondantes. |
Remplacement de la ligne de demande
S'il est nécessaire de cibler une requête sur un hôte différent de celui fourni lors de la création du client Feign, ou si vous souhaitez fournir un hôte cible pour chaque requête, incluez un paramètre
java.net.URI
et Feign utilisera cette valeur. comme cible de la requête.@ RequestLine ( "POST /repos/{owner}/{repo}/issues" ) void createIssue ( URI host , Issue issue , @ Param ( "owner" ) String owner , @ Param ( "repo" ) String repo );
Expressions
simulées représentent des expressions de chaîne simples (niveau 1) telles que définies par le modèle URI - RFC 6570. Expressions
sont développées à l'aide de leurs paramètres de méthode annotés Param
correspondants.
Exemple
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" );
}
}
Les expressions doivent être placées entre accolades {}
et peuvent contenir des modèles d'expressions régulières, séparés par deux points :
pour restreindre les valeurs résolues. L'exemple owner
doit être alphabétique. {owner:[a-zA-Z]*}
Les modèles RequestLine
et QueryMap
suivent la spécification URI Template - RFC 6570 pour les modèles de niveau 1, qui spécifie les éléments suivants :
encoded
via une annotation @Param
.Nous avons également une prise en charge limitée pour les expressions de style de chemin de niveau 3, avec les restrictions suivantes :
Exemples :
{;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
dans l'exemple ci-dessus sont définis comme Matt, Jeff, Susan
, l'URI s'étendra à /repos;owners=Matt;owners=Jeff;owners=Susan
Pour plus d'informations, consultez la RFC 6570, section 3.2.7.
Les expressions non définies sont des expressions dont la valeur est une valeur null
explicite ou dont aucune valeur n'est fournie. Selon le modèle URI - RFC 6570, il est possible de fournir une valeur vide pour une expression. Lorsque Feign résout une expression, il détermine d’abord si la valeur est définie, si c’est le cas, le paramètre de requête restera. Si l'expression n'est pas définie, le paramètre de requête est supprimé. Voir ci-dessous pour une ventilation complète.
Chaîne vide
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
parameters . put ( "param" , "" );
this . demoClient . test ( parameters );
}
Résultat
http://localhost:8080/test?param=
Manquant
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
this . demoClient . test ( parameters );
}
Résultat
http://localhost:8080/test
Indéfini
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
parameters . put ( "param" , null );
this . demoClient . test ( parameters );
}
Résultat
http://localhost:8080/test
Voir Utilisation avancée pour plus d'exemples.
Et les barres obliques ?
/
Les modèles @RequestLine n'encodent pas les barres obliques
/
caractères par défaut. Pour modifier ce comportement, définissez la propriétédecodeSlash
sur@RequestLine
surfalse
.
Et le plus ?
+
Conformément à la spécification d'URI, un signe
+
est autorisé dans les segments de chemin et de requête d'un URI. Cependant, la gestion du symbole sur la requête peut être incohérente. Dans certains systèmes existants, le+
équivaut à un espace. Feign adopte l'approche des systèmes modernes, où un symbole+
ne doit pas représenter un espace et est explicitement codé comme%2B
lorsqu'il est trouvé sur une chaîne de requête.Si vous souhaitez utiliser
+
comme espace, utilisez le littéralcaractère ou encodez la valeur directement en
%20
L'annotation @Param
possède un expander
de propriétés facultatif permettant un contrôle complet sur l'expansion de chaque paramètre. La propriété expander
doit faire référence à une classe qui implémente l'interface Expander
:
public interface Expander {
String expand ( Object value );
}
Le résultat de cette méthode respecte les mêmes règles énoncées ci-dessus. Si le résultat est null
ou une chaîne vide, la valeur est omise. Si la valeur n’est pas codée en PCT, elle le sera. Voir Extension @Param personnalisée pour plus d'exemples.
Les modèles Headers
et HeaderMap
suivent les mêmes règles que l’extension des paramètres de demande avec les modifications suivantes :
Voir En-têtes pour des exemples.
Une note sur les paramètres
@Param
et leurs noms :Toutes les expressions portant le même nom, quelle que soit leur position sur
@RequestLine
,@QueryMap
,@BodyTemplate
ou@Headers
seront résolues avec la même valeur. Dans l'exemple suivant, la valeur decontentType
sera utilisée pour résoudre à la fois l'expression d'en-tête et de chemin :public interface ContentService { @ RequestLine ( "GET /api/documents/{contentType}" ) @ Headers ( "Accept: {contentType}" ) String getDocumentByType ( @ Param ( "contentType" ) String type ); }Gardez cela à l’esprit lors de la conception de vos interfaces.
Les modèles Body
suivent les mêmes règles que l'extension des paramètres de demande avec les modifications suivantes :
Encoder
avant d'être placée sur le corps de la demande.Content-Type
doit être spécifié. Voir Modèles de corps pour des exemples. Feign a plusieurs aspects qui peuvent être personnalisés.
Pour les cas simples, vous pouvez utiliser Feign.builder()
pour construire une interface API avec vos composants personnalisés.
Pour le paramètre de demande, vous pouvez utiliser options(Request.Options options)
sur target()
pour définir connectTimeout, connectTimeoutUnit, readTimeout, readTimeoutUnit, followRedirects.
Par exemple:
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 peut produire plusieurs interfaces API. Ceux-ci sont définis comme Target<T>
(par défaut HardCodedTarget<T>
), qui permettent la découverte dynamique et la décoration des requêtes avant l'exécution.
Par exemple, le modèle suivant peut décorer chaque demande avec l'URL actuelle et le jeton d'authentification du service d'identité.
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 inclut des exemples de clients GitHub et Wikipedia. Le projet du dénominateur peut également être abandonné pour Feign dans la pratique. En particulier, regardez son exemple de démon.
Feign a l'intention de bien fonctionner avec d'autres outils Open Source. Les modules sont les bienvenus pour intégrer vos projets préférés !
Gson comprend un encodeur et un décodeur que vous pouvez utiliser avec une API JSON.
Ajoutez GsonEncoder
et/ou GsonDecoder
à votre Feign.Builder
comme ceci :
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 comprend un encodeur et un décodeur que vous pouvez utiliser avec une API JSON.
Ajoutez JacksonEncoder
et/ou JacksonDecoder
à votre Feign.Builder
comme ceci :
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" );
}
}
Pour le Jackson Jr plus léger, utilisez JacksonJrEncoder
et JacksonJrDecoder
du module Jackson Jr.
Moshi comprend un encodeur et un décodeur que vous pouvez utiliser avec une API JSON. Ajoutez MoshiEncoder
et/ou MoshiDecoder
à votre Feign.Builder
comme ceci :
GitHub github = Feign . builder ()
. encoder ( new MoshiEncoder ())
. decoder ( new MoshiDecoder ())
. target ( GitHub . class , "https://api.github.com" );
SaxDecoder vous permet de décoder XML d'une manière compatible avec les environnements JVM normaux ainsi qu'Android.
Voici un exemple de configuration de l'analyse des réponses 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 comprend un encodeur et un décodeur que vous pouvez utiliser avec une API XML.
Ajoutez JAXBEncoder
et/ou JAXBDecoder
à votre Feign.Builder
comme ceci :
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 comprend un encodeur et un décodeur que vous pouvez utiliser avec une API XML.
Ce module ajoute la prise en charge de l'encodage et du décodage des objets SOAP Body via JAXB et SOAPMessage. Il fournit également des capacités de décodage SOAPFault en les encapsulant dans l' javax.xml.ws.soap.SOAPFaultException
d'origine, de sorte que vous n'aurez besoin que d'intercepter SOAPFaultException
pour gérer SOAPFault.
Ajoutez SOAPEncoder
et/ou SOAPDecoder
à votre Feign.Builder
comme ceci :
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 : vous devrez peut-être également ajouter SOAPErrorDecoder
si des défauts SOAP sont renvoyés en réponse avec des codes http d'erreur (4xx, 5xx, ...)
fastjson2 comprend un encodeur et un décodeur que vous pouvez utiliser avec une API JSON.
Ajoutez Fastjson2Encoder
et/ou Fastjson2Decoder
à votre Feign.Builder
comme ceci :
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 remplace le traitement des annotations pour utiliser à la place les annotations standard fournies par la spécification JAX-RS. Ceci est actuellement ciblé sur la spécification 1.1.
Voici l'exemple ci-dessus réécrit pour utiliser 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 les requêtes http de Feign vers OkHttp, ce qui permet SPDY et un meilleur contrôle du réseau.
Pour utiliser OkHttp avec Feign, ajoutez le module OkHttp à votre chemin de classe. Ensuite, configurez Feign pour utiliser 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 remplace la résolution d'URL du client de Feign, ajoutant des capacités de routage intelligente et de résilience fournies par Ribbon.
L'intégration nécessite que vous transmettiez le nom de votre client ruban comme partie hôte de l'URL, par exemple myAppProd
.
public class Example {
public static void main ( String [] args ) {
MyService api = Feign . builder ()
. client ( RibbonClient . create ())
. target ( MyService . class , "https://myAppProd" );
}
}
Http2Client dirige les requêtes http de Feign vers le nouveau client HTTP/2 Java11 qui implémente HTTP/2.
Pour utiliser le nouveau client HTTP/2 avec Feign, utilisez Java SDK 11. Ensuite, configurez Feign pour utiliser Http2Client :
GitHub github = Feign . builder ()
. client ( new Http2Client ())
. target ( GitHub . class , "https://api.github.com" );
HystrixFeign configure la prise en charge des disjoncteurs fournie par Hystrix.
Pour utiliser Hystrix avec Feign, ajoutez le module Hystrix à votre chemin de classe. Utilisez ensuite le constructeur HystrixFeign
:
public class Example {
public static void main ( String [] args ) {
MyService api = HystrixFeign . builder (). target ( MyService . class , "https://myAppProd" );
}
}
SLF4JModule permet de diriger la journalisation de Feign vers SLF4J, vous permettant d'utiliser facilement un backend de journalisation de votre choix (Logback, Log4J, etc.)
Pour utiliser SLF4J avec Feign, ajoutez à la fois le module SLF4J et une liaison SLF4J de votre choix à votre chemin de classe. Ensuite, configurez Feign pour utiliser 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()
vous permet de spécifier une configuration supplémentaire telle que la façon de décoder une réponse.
Si des méthodes de votre interface renvoient des types autres que Response
, String
, byte[]
ou void
, vous devrez configurer un Decoder
autre que celui par défaut.
Voici comment configurer le décodage JSON (à l'aide de l'extension 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 vous devez prétraiter la réponse avant de la transmettre au décodeur, vous pouvez utiliser la méthode de création mapAndDecode
. Un exemple de cas d'utilisation concerne une API qui ne sert que jsonp, vous devrez peut-être déballer le jsonp avant de l'envoyer au décodeur Json de votre choix :
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 des méthodes de votre interface renvoient le type Stream
, vous devrez configurer un StreamDecoder
.
Voici comment configurer le décodeur Stream sans décodeur délégué :
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" );
}
}
Voici comment configurer le décodeur Stream avec le décodeur délégué :
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" );
}
}
Le moyen le plus simple d'envoyer un corps de requête à un serveur consiste à définir une méthode POST
comportant un paramètre String
ou byte[]
sans aucune annotation. Vous devrez probablement ajouter un en-tête 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 " }" );
}
}
En configurant un Encoder
, vous pouvez envoyer un corps de requête de type sécurisé. Voici un exemple utilisant l'extension 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" ));
}
}
L'annotation @Body
indique un modèle à développer à l'aide de paramètres annotés avec @Param
. Vous devrez probablement ajouter un en-tête 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 prend en charge les en-têtes de paramètres sur les requêtes soit dans le cadre de l'API, soit dans le cadre du client selon le cas d'utilisation.
Dans les cas où des interfaces ou des appels spécifiques doivent toujours avoir certaines valeurs d'en-tête définies, il est logique de définir des en-têtes dans le cadre de l'API.
Les en-têtes statiques peuvent être définis sur une interface ou une méthode API à l'aide de l'annotation @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 );
}
Les méthodes peuvent spécifier du contenu dynamique pour les en-têtes statiques en utilisant l'expansion des variables dans @Headers
.
public interface Api {
@ RequestLine ( "POST /" )
@ Headers ( "X-Ping: {token}" )
void post ( @ Param ( "token" ) String token );
}
Dans les cas où les clés et les valeurs des champs d'en-tête sont dynamiques et où la plage de clés possibles ne peut pas être connue à l'avance et peut varier entre différents appels de méthode dans la même API/client (par exemple, les champs d'en-tête de métadonnées personnalisés tels que "x-amz- meta-*" ou "x-goog-meta-*"), un paramètre Map peut être annoté avec HeaderMap
pour construire une requête qui utilise le contenu de la carte comme paramètres d'en-tête.
public interface Api {
@ RequestLine ( "POST /" )
void post ( @ HeaderMap Map < String , Object > headerMap );
}
Ces approches spécifient les entrées d'en-tête dans le cadre de l'API et ne nécessitent aucune personnalisation lors de la création du client Feign.
Pour personnaliser les en-têtes de chaque méthode de requête sur une cible, un RequestInterceptor peut être utilisé. Les RequestInterceptors peuvent être partagés entre les instances Target et devraient être thread-safe. Les RequestInterceptors sont appliqués à toutes les méthodes de requête sur une cible.
Si vous avez besoin d'une personnalisation par méthode, une cible personnalisée est requise, car un RequestInterceptor n'a pas accès aux métadonnées de la méthode actuelle.
Pour un exemple de définition d’en-têtes à l’aide d’un RequestInterceptor
, consultez la section Request Interceptors
.
Les en-têtes peuvent être définis dans le cadre d’un Target
personnalisé.
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 ));
}
}
Ces approches dépendent du RequestInterceptor
ou Target
personnalisé défini sur le client Feign lors de sa construction et peuvent être utilisées comme moyen de définir des en-têtes sur tous les appels d'API pour chaque client. Cela peut être utile pour effectuer des tâches telles que définir un jeton d'authentification dans l'en-tête de toutes les requêtes API par client. Les méthodes sont exécutées lorsque l'appel API est effectué sur le thread qui appelle l'appel API, ce qui permet aux en-têtes d'être définis dynamiquement au moment de l'appel et d'une manière spécifique au contexte - par exemple, le stockage local du thread peut être utilisé pour définir différentes valeurs d'en-tête en fonction du thread appelant, ce qui peut être utile pour des choses telles que la définition d'identifiants de trace spécifiques au thread pour les requêtes.
Pour spécifier l'en-tête Content-Length: 0
lors d'une requête avec un corps vide, la propriété système sun.net.http.allowRestrictedHeaders
doit être définie sur true
Sinon, l’en-tête Content-Length
ne sera pas ajouté.
Dans de nombreux cas, les API d'un service suivent les mêmes conventions. Feign prend en charge ce modèle via des interfaces à héritage unique.
Prenons l'exemple :
interface BaseAPI {
@ RequestLine ( "GET /health" )
String health ();
@ RequestLine ( "GET /all" )
List < Entity > all ();
}
Vous pouvez définir et cibler une API spécifique, en héritant des méthodes de base.
interface CustomAPI extends BaseAPI {
@ RequestLine ( "GET /custom" )
String custom ();
}
Dans de nombreux cas, les représentations des ressources sont également cohérentes. Pour cette raison, les paramètres de type sont pris en charge sur l’interface API de 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 > { }
Vous pouvez enregistrer les messages http entrant et sortant de la cible en configurant un Logger
. Voici la façon la plus simple de procéder :
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" );
}
}
Une note sur JavaLogger : évitez d'utiliser le constructeur
JavaLogger()
par défaut - il a été marqué comme obsolète et sera bientôt supprimé.
Le SLF4JLogger (voir ci-dessus) peut également être intéressant.
Pour filtrer les informations sensibles telles que l'autorisation ou les jetons, remplacez les méthodes shouldLogRequestHeader
ou shouldLogResponseHeader
.
Lorsque vous devez modifier toutes les requêtes, quelle que soit leur cible, vous souhaiterez configurer un RequestInterceptor
. Par exemple, si vous agissez en tant qu'intermédiaire, vous souhaiterez peut-être propager l'en-tête 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" );
}
}
Un autre exemple courant d'intercepteur serait l'authentification, comme l'utilisation du BasicAuthRequestInterceptor
intégré.
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" );
}
}
Les paramètres annotés avec Param
se développent en fonction de leur toString
. En spécifiant un Param.Expander
personnalisé, les utilisateurs peuvent contrôler ce comportement, par exemple le formatage des dates.
public interface Api {
@ RequestLine ( "GET /?since={date}" ) Result list ( @ Param ( value = "date" , expander = DateToMillis . class ) Date date );
}
Un paramètre Map peut être annoté avec QueryMap
pour construire une requête qui utilise le contenu de la carte comme paramètres de requête.
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap Map < String , Object > queryMap );
}
Cela peut également être utilisé pour générer les paramètres de requête à partir d'un objet POJO à l'aide d'un QueryMapEncoder
.
public interface Api {
@ RequestLine ( "GET /find" )
V find ( @ QueryMap CustomPojo customPojo );
}
Lorsqu'elle est utilisée de cette manière, sans spécifier de QueryMapEncoder
personnalisé, la carte de requête sera générée en utilisant les noms de variables membres comme noms de paramètres de requête. Vous pouvez annoter un champ spécifique de CustomPojo
avec l'annotation @Param
pour spécifier un nom différent pour le paramètre de requête. Le POJO suivant générera les paramètres de requête de "/find?name={name}&number={number}®ion_id={regionId}" (l'ordre des paramètres de requête inclus n'est pas garanti, et comme d'habitude, si une valeur est nulle, elle sera exclu).
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 ;
}
}
Pour configurer un QueryMapEncoder
personnalisé :
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. queryMapEncoder ( new MyCustomQueryMapEncoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Lors de l'annotation d'objets avec @QueryMap, l'encodeur par défaut utilise la réflexion pour inspecter les champs des objets fournis afin de développer les valeurs des objets dans une chaîne de requête. Si vous préférez que la chaîne de requête soit créée à l'aide des méthodes getter et setter, telles que définies dans l'API Java Beans, veuillez utiliser 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 vous avez besoin de plus de contrôle sur la gestion des réponses inattendues, les instances Feign peuvent enregistrer un ErrorDecoder
personnalisé via le générateur.
public class Example {
public static void main ( String [] args ) {
MyApi myApi = Feign . builder ()
. errorDecoder ( new MyErrorDecoder ())
. target ( MyApi . class , "https://api.hostname.com" );
}
}
Toutes les réponses qui aboutissent à un statut HTTP non compris dans la plage 2xx déclencheront la méthode decode
de ErrorDecoder
, vous permettant de gérer la réponse, d'envelopper l'échec dans une exception personnalisée ou d'effectuer tout traitement supplémentaire. Si vous souhaitez réessayer la demande, lancez une RetryableException
. Cela invoquera le Retryer
enregistré.
Feign, par défaut, réessayera automatiquement IOException
, quelle que soit la méthode HTTP, les traitant comme des exceptions transitoires liées au réseau et toute RetryableException
levée à partir d'un ErrorDecoder
. Pour personnaliser ce comportement, enregistrez une instance Retryer
personnalisée via le générateur.
L'exemple suivant montre comment actualiser le jeton et réessayer avec ErrorDecoder
et Retryer
lors de la réception d'une réponse 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 );
}
}
Les Retryer
sont chargés de déterminer si une nouvelle tentative doit avoir lieu en renvoyant un true
ou false
à partir de la méthode continueOrPropagate(RetryableException e);
Une instance Retryer
sera créée pour chaque exécution Client
, vous permettant de maintenir l'état entre chaque requête si vous le souhaitez.
Si la nouvelle tentative s’avère infructueuse, la dernière RetryException
sera levée. Pour identifier la cause initiale qui a conduit à la nouvelle tentative infructueuse, créez votre client Feign avec l'option exceptionPropagationPolicy()
.
Si vous devez traiter ce qui serait autrement une erreur comme un succès et renvoyer un résultat plutôt que de lever une exception, vous pouvez utiliser un ResponseInterceptor
.
À titre d'exemple, Feign inclut un simple RedirectionInterceptor
qui peut être utilisé pour extraire l'en-tête d'emplacement des réponses de redirection.
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" );
}
}
Par défaut, feign ne collectera aucune métrique.
Mais il est possible d'ajouter des capacités de collecte de métriques à n'importe quel client simulé.
Les capacités métriques fournissent une API métriques de première classe que les utilisateurs peuvent exploiter pour obtenir un aperçu du cycle de vie des requêtes/réponses.
Une note sur les modules de métriques :
Toutes les intégrations métriques sont construites dans des modules séparés et ne sont pas disponibles dans le module
feign-core
. Vous devrez les ajouter à vos dépendances.
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
}
}
Les interfaces ciblées par Feign peuvent avoir des méthodes statiques ou par défaut (si vous utilisez Java 8+). Ceux-ci permettent aux clients Feign de contenir une logique qui n'est pas expressément définie par l'API sous-jacente. Par exemple, les méthodes statiques facilitent la spécification des configurations de build client courantes ; les méthodes par défaut peuvent être utilisées pour composer des requêtes ou définir des paramètres par défaut.
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 introduit un nouveau générateur AsyncFeign
qui permet aux méthodes de renvoyer des instances 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 mise en œuvre initiale inclut 2 clients asynchrones :
AsyncClient.Default
AsyncApacheHttp5Client
Conserver toutes les bibliothèques feint sur la même version est essentiel pour éviter les binaires incompatibles. Lors de la consommation de dépendances externes, il peut être difficile de s'assurer qu'une seule version est présente.
Dans cet esprit, feign build génère un module appelé feign-bom
qui verrouille les versions de tous les modules feign-*
.
La nomenclature est un fichier POM spécial qui regroupe les versions de dépendance connues pour être valides et testées pour fonctionner ensemble. Cela réduira la difficulté pour les développeurs de devoir tester la compatibilité des différentes versions et réduira les risques d'incompatibilités de versions.
Voici un exemple de ce à quoi ressemble un fichier de nomenclature simulée.
< 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 >
Ce module ajoute la prise en charge de l'encodage des formulaires application/x-www-form-urlencoded et multipart/form-data .
Incluez la dépendance à votre application :
Maven :
< dependencies >
...
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form</ artifactId >
< version >4.0.0</ version >
</ dependency >
...
</ dependencies >
Graduation :
compile ' io.github.openfeign.form:feign-form:4.0.0 '
L'extension feign-form
dépend d' OpenFeign
et de ses versions concrètes :
feign-form
antérieures à la 3.5.0 fonctionnent avec les versions OpenFeign
9.* ;feign-form
, le module fonctionne avec les versions OpenFeign
10.1.0 et supérieures.IMPORTANT : il n'y a pas de compatibilité ascendante et aucune garantie que les versions de
feign-form
après 3.5.0 fonctionnent avecOpenFeign
avant 10.* .OpenFeign
a été refactorisé dans la 10e version, donc la meilleure approche consiste à utiliser les versions les plus récentesOpenFeign
etfeign-form
.
Remarques :
spring-cloud-openfeign utilise OpenFeign
9.* jusqu'à la v2.0.3.RELEASE et utilise 10.* après. Quoi qu'il en soit, la dépendance a déjà une version feign-form
appropriée, voir la dépendance pom, vous n'avez donc pas besoin de la spécifier séparément ;
spring-cloud-starter-feign
est une dépendance obsolète et elle utilise toujours les versions 9.* d' OpenFeign
.
Ajoutez FormEncoder
à votre Feign.Builder
comme ceci :
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ())
. target ( SomeApi . class , "http://api.some.org" );
De plus, vous pouvez décorer l'encodeur existant, par exemple JsonEncoder comme ceci :
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ( new JacksonEncoder ()))
. target ( SomeApi . class , "http://api.some.org" );
Et utilisez-les ensemble :
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 );
}
Vous pouvez spécifier deux types de formulaires de codage par en-tête 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 ;
}
}
Dans l'exemple ci-dessus, la méthode sendPhoto
utilise le paramètre photo
en utilisant trois types différents pris en charge.
File
utilisera l'extension du fichier pour détecter le Content-Type
;byte[]
utilisera application/octet-stream
comme Content-Type
;FormData
utilisera le Content-Type
et fileName
de FormData
; FormData
est un objet personnalisé qui encapsule un byte[]
et définit un Content-Type
et fileName
comme ceci :
FormData formData = new FormData ( "image/png" , "filename.png" , myDataAsByteArray );
someApi . sendPhoto ( true , formData );
Vous pouvez également utiliser Form Encoder avec Spring MultipartFile
et @FeignClient
.
Incluez les dépendances dans le fichier pom.xml de votre projet :
< 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, si vous n'avez pas besoin de l'encodeur standard 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 ();
}
}
}
Merci à tf-haotri-pham pour sa fonctionnalité, qui utilise la bibliothèque Apache commons-fileupload, qui gère l'analyse de la réponse en plusieurs parties. Les parties de données corporelles sont conservées sous forme de tableaux d’octets en mémoire.
Pour utiliser cette fonctionnalité, incluez SpringManyMultipartFilesReader dans la liste des convertisseurs de messages pour le Decoder et demandez au client Feign de renvoyer un tableau 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 ;
}
});
}
}
}