Mktd#1 Feign Vs Retrofit : 1 Prise En Main
Introduction
Nous avons organisé à Toulouse début juillet, le premier MonkeyTechDays chez HarryCow sur la thématique Feign vs Retrofit. Un MKTD consiste à comparer, apprendre, sous forme de défis, une ou plusieurs technologies sur une journée.
Ce premier événement était l’occasion d’approfondir les technologies de clients REST en Java. Nous avons donc étudié Feign, une librairie initiée par Netflix et Retrofit qui est écrite par Square. Ces deux API permettent d’écrire des clients REST en Java de façon plus élégante qu’avec les solutions plus classiques comme JAX-RS client, Spring Rest Template, …
Pour aider les équipes à tester les technologies, nous avions au préalable déployé plusieurs services REST sur le thème des singes. Le code source de cette journée est disponible à l’adresse : https://github.com/monkeytechdays
Défi 0 : Constitution des équipes
Ce défi n’avait rien de technique, mais nous permet de constituer des équipes équitables pour chaque technologie, en finissant nos cafés, croissants, ... L’équipe Feign étant dirigée par Igor et l’équipe Retrofit par Emmanuel.
Défi 1: Prise en main
Ce premier défi consiste à une mise en bouche pour découvrir ces technologies.
Le principe d’utilisation de Feign et Retrofit consiste à créer une interface décrivant le service REST, puis l’API se charge de créer une instance de cette interface. Dans ce premier défi, il suffisait de compléter les interfaces correspondant aux services REST pour faire passer des tests unitaires.
Voici les deux interfaces retournant du JSON :
public interface MonkeyApi {
Page<Monkey> getMonkeys(int page);
Monkey getMonkeyByName(String name);
Monkey createMonkey(Monkey monkey);
void deleteMonkey(String id);
}
public interface MonkeyRaceApi {
List<MonkeyRace> getMonkeyRaces();
}
et celle dont le service retourne du XML :
public interface MonkeyStatsApi {
MonkeyStatistics getMonkeyStats();
}
Voir le code sous GitHub
Pour réussir ce défi, il faut donc faire:
- un GET et décoder le JSON de la réponse,
- un GET avec un paramètre de requête,
- un GET avec un paramètre dans le path de la requête,
- un POST avec un encodage en JSON du corps de la requête,
- un DELETE,
- un GET et décoder le XML de la réponse.
Feign
La documentation de Feign se trouve dans le README.md sous GitHub. La documentation des extensions se trouve aussi dans des fichiers README.md de ces extensions.
Bien que Feign supporte Java 6 par défaut, nous avons codé avec Java 8.
Dépendances
Pour commencer à utiliser Feign, il faut bien sûr ajouter les dépendances nécessaires pour ce défi:
<!-- Feign -->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
<version>8.17.0</version>
</dependency>
<!-- Feign: encode/decode JSON with GSON -->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-gson</artifactId>
<version>8.17.0</version>
</dependency>
<!-- Feign: encode/decode XML with JAXB -->
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-jaxb</artifactId>
<version>8.17.0</version>
</dependency>
Nous recommandons bien sûr l’utilisation d’une propriété maven pour définir la version de Feign utilisée.
Configuration des interfaces
Ensuite il faut annoter les interfaces pour que Feign fasse les requêtes HTTP correspondantes aux méthodes de ces interfaces. Feign apporte ses propres annotations pour décrire les requêtes HTTP:
@RequestLine
: permet de définir la première ligne de la requête HTTP: le verbe HTTP (GET, POST, PUT, DELETE, …) et le chemin, on y précise aussi les paramètres de la requête. On peut utiliser la notation{name}
pour définir une partie variable de la requête (paramètre ou chemin)@Param
: cette annotation permet de faire le lien entre une variable définie dans les autres annotations (@RequestLine
,@Headers
, …) et le paramètre de la méthode. Il faut préciser le nom de la variable dans l’annotation.@Headers
: permet d’ajouter une en-tête HTTP, comme pour@RequestLine
on peut utiliser la notation{name}
pour définir une valeur variable dans l’en-tête HTTP. Cette annotation, peut être mise sur l’interface, ou sur une méthode de cette interface. Pas d’annotation pour le corps d’une requête POST ou PUT, le paramètre sans annotation sera converti dans le corps de la requête.
Ce qui nous donne ceci :
public interface MonkeyRaceApi {
@RequestLine("GET /races")
List<MonkeyRace> getMonkeyRaces();
}
@Headers("Content-Type: application/json")
public interface MonkeyApi {
@RequestLine("GET ?page={page}")
Page<Monkey> getMonkeys(@Param("page") int page);
@RequestLine("GET /{name}")
Monkey getMonkeyByName(@Param("name") String name);
@RequestLine("POST ")
Monkey createMonkey(Monkey monkey);
@RequestLine("DELETE /{id}")
void deleteMonkey(@Param("id") String id);
}
public interface MonkeyStatsApi {
@RequestLine("GET /stats")
MonkeyStatistics getMonkeyStats();
}
Construction des instances
Pour la dernière étape, on utilise l’API fluent builder de Feign pour créer l’instance de ces interfaces. C’est ici que l’on va faire intervenir les encodeurs/décodeurs ajoutés dans nos dépendances Maven plus tôt :
static MonkeyRaceApi buildRaceApi(String url) {
return Feign.builder()
// Decode JSON from response body
.decoder(new GsonDecoder())
.target(MonkeyRaceApi.class, url);
}
static MonkeyApi buildMonkeyApi(String url) {
return Feign.builder()
// Decode JSON from response body
.decoder(new GsonDecoder())
// Encode JSON for request body
.encoder(new GsonEncoder())
.target(MonkeyApi.class, url + "/monkeys");
}
static MonkeyStatsApi buildStatsApi(String url) {
// Create JAXB context factory
JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
.withMarshallerJAXBEncoding("UTF-8")
.build();
return Feign.builder()
// Decode XML from response body
.decoder(new JAXBDecoder(jaxbFactory))
.target(MonkeyStatsApi.class, url);
}
Feign va concaténer l'URL avec le chemin défini, dans le chemin de l'annotation @RequestLine
, ceci permet facilement de rajouter un préfixe pour les services si on le souhaite.
Bilan
Peu de points négatifs pour Feign dans cet exercice:
- pour faire fonctionner le décodage XML il a fallu triturer un peu l’objet pour que JAXB deserialize correctement le XML. Mais c’est plus un problème lié à JAXB et au XML de façon plus générale,
- les messages d’erreurs ne sont parfois pas simples à décrypter, mais avec un peu de pratique et une connaissance basique du protocole HTTP, ça n’est pas vraiment un problème. Un problème classique est le fait d’oublier le verbe HTTP dans l’annotation
@RequestLine
.
Beaucoup de côtés positifs ici:
- simple et proche du HTTP,
- très léger, il n’y a pas de dépendances transitives pour le
feign-core
, - il y a un bon support de Java 8, par exemple, les méthodes
static
etdefault
des interfaces de Java 8 sont supportées.
Quelques remarques:
- il y a d’autres annotations
@Body
,@HeaderMap
,@QueryMap
qui existent, - on peut configurer la façon dont les variables (
@Param
) sont converties en String via lesExpander
. - pour définir un chemin racine à toutes nos méthodes dans l’interface, on peut l’ajouter dans l’URL utilisée par le builder.
Il n’y a pas de magie dans Feign : il n'utilise que ce qui existe déjà dans le JDK : java.net.HttpURLConnection
, java.lang.reflect.Proxy
, java.lang.reflect.InvocationHandler
, …
Retrofit
La première étape consiste à rajouter les dépendances de Rétrofit
<! -- Dépendance de rétrofit -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>${retrofit.version}</version>
</dependency>
<! -- Converter Jackson pour gérer le Json -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>${retrofit.version}</version>
</dependency>
<!-- Converter Simple Xml pour gérer le xml -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-simplexml</artifactId>
<version>${retrofit.version}</version>
</dependency>
Ensuite, il nous faut rajouter les annotations spécifiques à Retrofit sur l’interface.
Les règles du jeu étant de ne pas changer la signature de l’interface, nous avons dû ajouter une autre interface utilisée par la CallFactory
par défaut de Retrofit.
public interface MonkeyRaceService {
@GET("races")
Call<List<MonkeyRace>> getMonkeyRaces();
}
public interface MonkeyService {
@GET("monkeys")
Call<Page<Monkey>> getMonkeys();
@GET("monkeys/{name}")
Call<Monkey> getMonkeyByName(@Path("name") String name);
@POST("monkeys")
Call<Monkey> create(@Body Monkey monkey);
@DELETE("monkeys/{id}")
Call<ResponseBody> delete(@Path("id") String monkeyId);
}
public interface MonkeyStatsService {
@GET("/stats")
Call<MonkeyStatistics> getMonkeyStats();
}
Ensuite, nous implémentons les interfaces MonkeyApi
, MonkeyRaceApi
, MonkeyStatsApi
en utilisant les interfaces spécifiques pour Retrofit.
public class RetrofitMonkeyApi implements MonkeyApi, RetrofitApi {
private MonkeyService monkeyService;
public void setBaseUrl(String baseUrl) {
monkeyService = createRetrofit(baseUrl, false)
.create(MonkeyService.class);
}
@Override
public Page<Monkey> getMonkeys(int page) {
return executeCall(monkeyService::getMonkeys);
}
@Override
public Monkey getMonkeyByName(String name) {
return executeCall(() -> monkeyService.getMonkeyByName(name));
}
@Override
public Monkey createMonkey(Monkey monkey) {
return executeCall(() -> monkeyService.create(monkey));
}
@Override
public void deleteMonkey(String id) {
executeCall(() -> monkeyService.delete(id));
}
}
public interface RetrofitApi {
default Retrofit createRetrofit(String baseUrl, boolean useXml) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(baseUrl);
if (useXml) {
builder.addConverterFactory(SimpleXmlConverterFactory.create());
}
builder.addConverterFactory(JacksonConverterFactory.create());
return builder.build();
}
default <T> T executeCall(Supplier<Call<T>> supplier) {
try {
Call<T> call = supplier.get();
return call.execute().body();
} catch (IOException e) {
return null;
}
}
}
Bilan
Points négatifs
Nous trouvons dommage qu’il n’y ait pas nativement une CallFactory
permettant de faire de façon synchrone un appel retournant notre objet métier sans avoir besoin de passer par l’objet Call
à la manière dont cela est géré avec Feign.
Il est aussi possible de faire notre propre CallAdapterFactory
.
Voici un exemple tiré du code source des tests de Retrofit :
static class DirectCallIOException extends RuntimeException {
DirectCallIOException(String message, IOException e) {
super(message, e);
}
}
static class DirectCallAdapterFactory extends CallAdapter.Factory {
@Override
public CallAdapter<?> get(final Type returnType, Annotation[] annotations, Retrofit retrofit) {
return new CallAdapter<Object>() {
@Override public Type responseType() {
return returnType;
}
@Override public Object adapt(Call call) {
try {
return call.execute().body();
} catch (IOException e) {
throw new DirectCallIOException(e.getMessage(), e);
}
}
};
}
}
Cela nous obligerait quand même à traiter les exceptions de type DirectCallIOException
.
Autre point que nous trouvons dommage lors de cet exercice est le fait que nous devons catcher les IOException
qui peuvent se produire lors de l’appel.
Peut être manque-t-il a Retrofit une gestion des exceptions comme Feign peut l’avoir.
Nous verrons cela plus en détail dans l’exercice suivant.
Dernier point 'négatif', Retrofit ayant besoin de plusieurs dépendances pour fonctionner : OkHttp et d'au moins 1 converter, la taille de l’exécutable généré est sensiblement plus grosse que celle de l’exécutable de Feign (1.5Mo contre 0.5Mo).
Points Positifs
Retrofit reste simple à utiliser. Le fait que les principaux converters soient disponibles est une très bonne chose.
Retrofit a ses propres annotations, évitant ainsi les erreurs de typo, ce qui est une très bonne chose. Bien que Feign se soit améliorée sur les messages d’erreurs, nous trouvons préférable le choix fait par l’équipe de Retrofit sur cette partie.