Feign, encore un client HTTP ?

Depuis les prémices de Java, il est possible de requêter sur le protocole HTTP, soit de manière native avec le package java.net, soit avec l’un des nombreux clients qui ont vu le jour. Certaines librairies sont éprouvées depuis quelques temps déjà. On peut ainsi parler de :

  • Apache HttpClient,
  • Jersey,
  • RestTemplate (Spring),
  • CXF,
  • JAXRS,
  • Google Http Client,
  • Unirest,
  • Restlet,
  • etc.

À l’origine développé et utilisé par Netflix OSS depuis quelques années, Feign est aujourd’hui proposé par OpenFeign comme librairie open-source. Quelles sont ses possibilités et comment l’utiliser ?

Spring Cloud propose une définition complète du client HTTP :

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign.

Le but de cette libraire est donc très clairement de simplifier et minimiser l’écriture du requêtage HTTP tout en s’inscrivant dans la lignée des “Convention Over Configuration”.

Un premier exemple : GET

N.B. : une des agences Ippon étant basée à Bordeaux, la thématique de notre Api s’organise autour du vin 🙂

public interface WineAPI {
    @RequestLine("GET /wines")
    List getWineDesignations();
}

public static void main(String … args) {
    WineAPI api = Feign.builder()
                       .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

    List wineDesignation = api.getWineDesignations();
}

Plutôt simple, non ? Une interface est déclarée avec une méthode annotée @RequestLine qui définit le verbe HTTP et le contexte à utiliser. Il suffit ensuite de créer le client Feign en ciblant la précédente interface et l’URL à appeler. Comme nous pouvons le voir, la déclaration du client repose sur une fluent interface afin de favoriser la déclaration/lecture/…

POST et paramètre de contexte

public interface WineAPI {
    @RequestLine("GET /wines/{wineId}")
    Wine getWine(@Param("wineId") long wineId);

    @Headers("Content-Type: application/json")
    @RequestLine("POST /wines")
    void create(Wine wine);
}

public static void main(String … args) {
    WineAPI api = Feign.builder()
                       .encoder(new JacksonEncoder())
                       .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

    Wine wine = api.getWine(1);

    Wine chateauPessacLeognan = new Wine();
    api.create(chateauPessacLeognan);
}

L’exemple propose de récupérer un vin par son identifiant renseigné dans le contexte, puis d’enregistrer un nouveau vin via une requête POST. Trois nouvelles choses :

  • l’utilisation de l’annotation @Param pour renseigner un paramètre dans le contexte,
  • @Header pour définir le content-type,
  • l’affectation d’un “encoder” au niveau du client Feign afin de spécifier la sérialisation à effectuer.

Le principe est identique pour tous les verbes HTTP.

Encodeurs et décodeurs

Si votre interface retourne autre chose que Response, String, byte[] ou void, il est obligatoire de configurer un décodeur ou encodeur. De façon native, Feign en propose plusieurs couvrant majoritairement les usages actuels. On peut citer :

  • Gson : GsonEncoder/GsonDecoder
WineAPI api = Feign.builder()
                   .encoder(new GsonEncoder())
                   .decoder(new GsonDecoder())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");
  • Jackson : JacksonEncoder/JacksonDecoder
WineAPI api = Feign.builder()
                   .encoder(new JacksonEncoder())
                   .decoder(new JacksonDecoder())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");
  • JAXB : JAXBEncoder/JAXBDecoder
WineAPI api = Feign.builder()
                   .encoder(new JAXBEncoder())
                   .decoder(new JAXBDecoder())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");
  • Sax : SAXDecoder
WineAPI api = Feign.builder()
                   .decoder(SAXDecoder.builder()
                                      .registerContentHandler(SomeHandler.class)
                                      .build())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Si votre bonheur ne se trouve pas dans cette liste, il est toujours possible d’en créer un spécifique en surchargeant la classe DefaultEncoder.

ErrorDecoder

Dans le cas d’erreurs, Feign envoie une unique exception de type FeignException. Il est cependant toujours conseillé de gérer les différents types d’erreur. Il suffit simplement ici de créer une classe implémentant ErrorDecoder et de l’affecter au builder :

public class WineErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() >= 400 && response.status() <= 499) { return new WineClientException(response.status(), response.reason()); } if (response.status() >= 500 && response.status() <= 599) {
            return new WineServerException(response.status(), response.reason());
        }
        return errorStatus(methodKey, response);
    }
}

L’affectation s’effectue au niveau du client Feign :

WineAPI api = Feign.builder()
                   .errorDecoder(new WineErrorDecoder())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Headers

Feign propose différentes méthodes pour spécifier les headers HTTP :

  • Au niveau de l’API avec l’annotation @Headers sur l’interface.
  • Sur une méthode spécifique avec là encore l’annotation @Headers.
  • De manière dynamique en ajoutant l’annotation @HeaderMap sur le paramètre typé Map<String, Object> de la méthode.
@Headers(“Accept-Charset: utf-8“)
public interface WineAPI {

    @RequestLine("GET /wines/{wineId}")
    Wine getWine(@Param("wineId") long wineId);

    @Headers("Content-Type: application/json")
    @RequestLine("POST /wines")
    void create(Wine wine);

    @RequestLine("GET /wines/white")
    void getWhiteWines(@HeaderMap Map<String, Object> headerMap);
}

Logs HTTP

Par défaut, les logs HTTP ne sont pas récupérés. Il est toutefois possible de les logger. Avec SLF4J (à importer comme module complémentaire) :

WineAPI api = Feign.builder()
                   .logger(new Slf4jLogger())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Ou avec JavaLogger :

WineAPI api = Feign.builder()
                   .logger(new Logger.JavaLogger().appendToFile("logs/http.log"))
                   .logLevel(Logger.Level.FULL)
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Il existe quatre niveaux de logs HTTP :

  • Logger.Level.BASIC

feign.Logger - [WineAPI#getWineDesignations] ---> GET http://api.wine.bordeaux.ippon.fr/wines HTTP/1.1 feign.Logger - [WineAPI#getWineDesignations] <--- HTTP/1.1 200 OK (59ms)

  • Logger.Level.HEADERS (BASIC + Headers)

feign.Logger - [WineAPI#getWineDesignations] ---> GET http://api.wine.bordeaux.ippon.fr/wines HTTP/1.1 feign.Logger - [WineAPI#getWineDesignations] ---> END HTTP (0-byte body) feign.Logger - [WineAPI#getWineDesignations] <--- HTTP/1.1 200 OK (49ms) feign.Logger - [WineAPI#getWineDesignations] Server: Jetty(8.y.z-SNAPSHOT) feign.Logger - [WineAPI#getWineDesignations] Content-Length: 6431 feign.Logger - [WineAPI#getWineDesignations] Content-Type: application/json feign.Logger - [WineAPI#getWineDesignations] <--- END HTTP (6431-byte body)

  • Logger.Level.FULL (HEADERS + contenu de la réponse)

feign.Logger - [WineAPI#getWineDesignations] ---> GET http://api.wine.bordeaux.ippon.fr/wines HTTP/1.1 feign.Logger - [WineAPI#getWineDesignations] ---> END HTTP (0-byte body) feign.Logger - [WineAPI#getWineDesignations] <--- HTTP/1.1 200 OK (48ms) feign.Logger - [WineAPI#getWineDesignations] Server: Jetty(8.y.z-SNAPSHOT) feign.Logger - [WineAPI#getWineDesignations] Content-Length: 6431 feign.Logger - [WineAPI#getWineDesignations] Content-Type: application/json feign.Logger - [WineAPI#getWineDesignations] feign.Logger - [WineAPI#getWineDesignations] ["bordeaux", "bourgogne", "graves", "jurançon"] feign.Logger - [WineAPI#getWineDesignations] <--- END HTTP (6431-byte body)

  • Logger.Level.NONE

Aucun log HTTP n’est tracé...

Intercepteur de requêtes

Dans le cas où toutes les requêtes doivent être modifiées, peu importe la cible, Feign propose d’utiliser des intercepteurs de requêtes. L’exemple ci-dessous ajoute la date du jour comme Header à chaque requête :

WineAPI api = Feign.builder()
                   .requestInterceptor(template -> template.header (“Date”, Instant.now.string()))
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Un autre exemple courant est l’authentification avec la classe feign.auth.BasicAuthRequestInterceptor :

WineAPI api = Feign.builder()
                   .requestInterceptor(new BasicAuthRequestInterceptor(username, password))
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

L’intégration avec …

Un autre but de Feign, tout aussi important, est de s’intégrer facilement avec Netflix OSS et d’autres librairies Open Source (Spring Source en est un exemple).

Hystrix

Cette librairie Netflix OSS permet d’utiliser le pattern Circuit Breaker. Le module HystrixFeign englobe ainsi les requêtes HTTP dans le Hystrix et configure ainsi le Circuit Breaker. Après avoir intégré le module dans le classpath, il reste à configurer Feign pour utiliser feign.hystrix.HystrixInvocationHandler :

WineAPI api = HystrixFeign.builder()
                          .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Ribbon

Ribbon sert de Load Balancer et intervient comme Routeur Dynamique. Il est généralement couplé avec Eureka pour la découverte des services. Son intégration, en tant que module Feign, permet de surcharger la résolution d’URL et de profiter des caractéristiques de Ribbon.

Routage dynamique

WineAPI api = Feign.builder()
                   .client(new RibbonClient())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Load balancing

WineAPI api = Feign.builder()
                   .target(LoadBalancingTarget.create(WineAPI.class, "https://api.wine.bordeaux.ippon.fr"));

Spring Cloud

Spring Cloud Netflix est basé, comme son nom l’indique, sur les composants Netflix OSS. Reposant sur les principes d’Auto Configuration et Convention over Configuration, l’utilisation de Feign est d’autant plus simplifiée. Plus besoin de définir le client, il est configuré directement par Spring.

@FeignClient(name = "wine-api", url = "https://api.wine.bordeaux.ippon.fr")
public interface WineAPI {
    @RequestLine("GET /wines")
    List getWineDesignations();
}

@Service
public class WineService {
    @Inject
    private WineAPI api;

    public void doSomething(){
        List wines = api.getWineDesignations();
    }
}

Les autres paramètres du client Feign

Options

Le builder Feign propose de spécifier des options. Actuellement, les options par défaut se focalisent sur les timeout de connexion ou de lecture. L’implémentation par défaut des options est la suivante :

public static class Options {

    private final int connectTimeoutMillis;
    private final int readTimeoutMillis;

    public Options(int connectTimeoutMillis, int readTimeoutMillis) {
        this.connectTimeoutMillis = connectTimeoutMillis;
        this.readTimeoutMillis = readTimeoutMillis;
    }

    public Options() {
        this(10 * 1000, 60 * 1000);
    }
}

Il est ainsi possible de surcharger cette classe pour modifier les valeurs par défaut. Il suffit ensuite de les fixer dans le builder :

WineAPI api = Feign.builder()
                   .options(new WineApiOptions())
                   .target(WineAPI.class, "https://api.wine.bordeaux.ippon.fr");

Contract

Il est possible de définir un contrat sur le client. Des nouvelles annotations ainsi que d’autres comportements peuvent être définis à travers ce dernier. Par défaut, le contrat utilisé est la classe feign.contract.Default. Cette classe s'occupe de parser et valider les différents paramètres, interfaces et annotations définis sur la classe cible (paramètre de la méthode target). Dans notre exemple, l’interface WineAPI sera vérifiée ainsi que toutes les méthodes définies.

Il est encore une fois possible de définir le comportement d’un nouveau contrat ou alors d'utiliser le module JAXRS et sa classe feign.jaxrs.JAXRSContract.

Retryer

Un retryer par défaut est configuré dans le client Feign. Couplé avec l’ErrorDecoder par défaut, il est utilisé uniquement si le header de la réponse possède la propriété “retry-after”.

Au final

Vous connaissez à présent les principales caractéristiques de ce client HTTP qu'est Feign. Simple d’utilisation, sa maîtrise en est d’autant plus rapide. Son intégration avec d’autres librairies est grandement facilitée. Bref, Feign possède les principales caractéristiques pour en faire l’un des principaux clients HTTP.

“You can’t FEIGN ignorance now”.

Sources

https://github.com/OpenFeign/feign
http://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html#spring-cloud-feign
https://dzone.com/articles/the-netflix-stack-using-spring-boot-part-3-feign