Refit et Retrofit

J’aime bien les belles librairies. Une belle librairie expose une API simple et auto-descriptive ; on peut la comprendre en lisant un minimum de documentation. Dans l’idéal, s’inspirer de quelques exemples suffit pour l’utiliser. Une belle librairie fait souvent une seule chose, mais elle le fait bien… Accessoirement, une belle librairie peut aussi s’avérer utile.

Je suis tombé sur une bien belle librairie il y a quelques jours, en me promenant sur le Web : Refit.

  • Refit ne sert qu’à une seule chose : créer rapidement un client .NET fortement typé autour d’une API REST;
  • Refit a une API simple, elle tient en une classe et une méthode (et quelques attributs) :
    1
    var myService = RestService.For<T>(url);
  • Enfin, une fois que l’on a compris l’exemple de base, on a tout compris (il restera bien sûr des subtilités à saisir, mais l’essentiel tient en quelques lignes). D’ailleurs, la documentation complète de Refit tient dans le Readme.md sur Github.

Et Retrofit alors ? C’est la source d’inspiration de Refit, son pendant Java, et son ancêtre.

Dans les deux parties qui suivent, je vais vous montrer comment implémenter le même client avec les deux librairies. Nous utiliserons pour l’exemple l’API de Github, permettant de transformer du Markdown en HTML (documentation ici). Cette API est simple : elle prend en entrée un objet JSON à 3 champs (dont l’un contient notre texte en Markdown), et renvoie directement la traduction en HTML (pas de wrapping Json pour la réponse).

Refit : le client C#

Refit s’installe via nuget. Donc, après avoir créé un nouveau projet (.NET 4.5 minimum), un passage par le Nuget Package Manager de Visual Studio ou, en ligne de commande :

1
Install-Package refit

Une fois cette formalité accomplie, la première chose à faire est de créer une interface qui représentera notre service. Cette interface disposera de méthodes représentant les APIs à invoquer, et enfin, les méthodes décorées avec des attributs indiquant le verbe HTTP à utiliser. Dans notre cas, cela donne quelque chose de ce genre :

1
2
3
4
5
6
[Headers("User-Agent: HelloRefit")]
public interface IGitHubApi
{
    [Post("/markdown")]
    Task<string> Render([Body] RenderRequest request);
}

Difficile de faire plus simple, et pourtant, il y a quelques détails à noter :

  • L’attribut Headers au-dessus de l’interface, indique à Refit qu’il doit ajouter le Header HTTP précisé lors de l’envoi de la requête. Ici, nous en avons besoin car Github n’aime pas les requêtes sans User-Agent ;
  • Post est un attribut fourni par Refit qui parle de lui-même, on trouvera aussi Get, Put, PatchDelete et Head. L’url passée en paramètre est relative à une url de base, qui sera précisée lors de l’utilisation du client ;
  • Body, toujours fourni par Refit, indique que le paramètre (une fois sérialisé) doit être passé dans le corps de la requête ;
  • La documentation Github nous dit que l’on récupère dans la réponse une chaîne contenant le HTML. Mais Refit nous oblige à renvoyer des Task<T>. En effet, le client produit par Refit n’exposera que des méthodes asynchrones awaitables. Ceci explique d’ailleurs la dépendance à .NET 4.5 minimum ;
  • RenderRequest est une classe que nous devons écrire, elle représente l’objet JSON qui sera envoyé à l’API Github. voir ci-dessous !!
    Là encore, ce n’était pas très compliqué : on définit des propriétés qui seront mappées en JSON grâce à l’attribut JsonProperty. Ici, ce n’est pas Refit qui fournit l’attribut, mais Newtonsoft.Json aka Json.NET (le standard de facto de sérialisation JSON en .NET). Refit dépend de Json.NET.
1
2
3
4
5
6
7
8
9
public class RenderRequest
{
    [JsonProperty("text")]
    public string Text { get; set; }
    [JsonProperty("mode")]
    public string Mode { get; set; }
    [JsonProperty("context")]
    public string Context { get; set; }
}

Que nous reste-t-il à faire avant de pouvoir utiliser notre client ? Implémenter l’interface ? Même pas. C’est là que la magie de Refit opère : nous pouvons dès à présent coder nos appels. Refit s’occupera de générer une classe à la compilation !

En avant pour le code utilisant notre client tout neuf :

1
2
3
4
5
6
7
var api = RestService.For<IGitHubApi>(
string html = await api.Render(new RenderRequest()
{
    Text = "**Hello**, Refit!",
    Mode = "markdown"
});

Sur la première lignes, nous instancions notre client : on demande simplement à Refit de nous fournir une instance de l’interface que nous avons définie plus haut (concrètement, Refit nous passe une instance de la classe qu’il a générée). On précise aussi l’url de base des APIs Github.

La deuxième ligne est l’appel proprement dit : on fournit un objet pour la requête, ici, on passe le texte en markdown et un mode de traduction (Github reconnait un autre mode de traduction : gfm). Comme notre interface renvoie un Task<string>, on peut faire un await sur l’appel, et récupérer le résultat de l’appel de manière asynchrone. On obtient bien :

1
<strong>Hello</strong>, Refit!

Enfin, last but not least, Refit fonctionne, pour à peu près toutes les plateformes vers lesquelles on peut compiler du C# (Desktop, Windows Phone, WinRT, mais aussi Android et iOS via Xamarin).

Retrofit : le client Java

On prend la même et on recommence, mais en Java…

Afin de faire fonctionner notre exemple, nous devons référencer deux librairies : Retrofit (évidemment) et un convertisseur JSON. En effet, Retrofit impose moins de choses que Refit et on peut, par exemple, choisir le format de sérialisation. Le revers de la médaille est qu’il faut être explicite :

1
2
3
4
5
6
7
8
9
10
<dependency>
 <groupId>com.squareup.retrofit</groupId>
 <artifactId>retrofit</artifactId>
 <version>2.0.0-beta2</version>
</dependency>
<dependency>
 <groupId>com.squareup.retrofit</groupId>
 <artifactId>converter-gson</artifactId>
 <version>2.0.0-beta2</version>
</dependency>

On notera que j’utilise ici une version beta. La version 2.0.0 devrait être finalisée par l’auteur d’ici quelques mois. Elle n’est pas compatible avec la version 1 (pas mal de refactoring de l’API).

Une fois ces dépendances référencées, on peut rentrer dans le vif du sujet et reproduire les étapes du chapitre sur Refit. Tout d’abord l’interface représentant le service :

1
2
3
4
5
public interface GitHubApi {
 @Headers("User-Agent: HelloRetrofit")
 @POST("/markdown")
 Call<ResponseBody> render(@Body RenderRequest request);
}

On voit que la version C# est une traduction presque mot à mot de la version Java. La seule différence importante est le type de retour de la méthode :

  • Call<T> : les réponses doivent toujours être de ce type. En Java, il n’y a pas de mot clé ni de pattern intégré pour gérer les appels asynchrones. La solution de Refit est de proposer le type Call<T> ; il s’agit d’une classe fournie par la librairie sur laquelle on pourra appeler diverses méthodes pour effectivement effectuer l’appel (en synchrone ou asynchrone) ;
  • ResponseBody : lors de mes premiers essais, je renvoyais un Call<String> (par analogie avec le Task<string> de C#). Cela ne fonctionne pas. En effet, Refit considère que la réponse doit aussi être en JSON, et essaie donc d’interpréter la chaîne renvoyée par Github qui, rappelons-le, contient du HTML … Évidemment, cela se passe assez mal. Je passerai sur le hack que j’avais mis en place consistant à fournir mon propre convertisseur (JSON pour les requêtes et rien du tout pour les réponses). La bonne solution a été trouvée par mon collègue de Clermont Julien Millau : en renvoyant un type BodyResponse (de la librairie okhttp des mêmes auteurs), Refit n’essaiera pas de désérialiser, et on peut récupérer la réponse brute.

Je ne m’étendrai pas sur la classe RenderRequest, et vous ferai grâce des getters/setters (l’annotation @SerializedName est issue de la librairie gson de Google utilisée par le convertisseur JSON de Retrofit) :

1
2
3
4
5
6
@SerializedName("text")
private String text;
@SerializedName("mode")
private String mode;
@SerializedName("context")
private String context;

L’utilisation de la librairie est plus intéressante (et exhibe plus de différences avec la version C#). Tout d’abord, l’initialisation :

1
2
3
4
5
6
Retrofit retrofit = new Retrofit.Builder()
 .addConverterFactory(GsonConverterFactory.create())
 .build();
GitHubApi api = retrofit.create(GitHubApi.class);

Il y a un peu plus de travail qu’en C# ; c’est dû, comme nous l’avons déjà dit, au fait que Retrofit est beaucoup plus paramétrable. Ainsi, on doit préciser explicitement que la sérialisation doit se faire en JSON.

N’oublions pas de construire l’objet de la requête :

1
2
3
RenderRequest request = new RenderRequest();
request.setText("**Hello**, Refit!");
request.setMode("markdown");

Et enfin, voici l’appel :

1
2
3
4
5
String html = api
    .render(request)
    .execute()     
    .body()
    .string();

Si on analyse l’enchaînement des méthodes :

  • render : l’appel de la méthode définie dans l’interface ; renvoie un objet Call<ResponseBody>;
  • execute : méthode de Call<T> effectuant l’invocation synchrone (l’appel asynchrone est similaire mais prend un callback). Renvoie un objet Response<T>. Dans notre cas, T = ResponseBody;
  • body : méthode de Response<T> renvoyant le corps de la réponse (ici un ResponseBody);
  • string : méthode de ResponseBody renvoyant la charge utile sous forme de chaîne ; c’est enfin la traduction en HTML renvoyée par Github.

Cela semble beaucoup de travail par rapport à la version C#, mais cette chaîne d’appel n’est pas si compliquée à taper (merci la complétion), et elle permet un contrôle beaucoup plus fin qu’avec Refit ; en particulier, le fait de passer par un objet Response<T> permet d’accéder aux éventuelles erreurs.

Retrofit fonctionne pour Java standard et Android.

Conclusion

Les deux librairies sont très similaires et servent bien le même besoin, d’une manière plus directe et moins paramétrable pour Refit, un peu plus complexe mais bien plus flexible pour Retrofit. Je suis tenté d’y voir une métaphore des philosophies des deux plateformes. Il est aussi intéressant de noter que sous une apparence similaire, les deux implémentations sont très différentes : Refit génère du code qui est compilé dans l’assembly final alors que Retrofit utilise des proxys dynamiques créés au runtime. Mais pas trop de spoil : l’examen de ce qu’on trouve sous le capot des deux librairies fera l’objet d’une suite à cet article.

Nous sommes loin d’avoir exploré toutes les possibilités de Refit et Retrofit. En effet, j’aurais pu parler des autres méthodes HTTP, du multipart, des formulaires… mais cet article est déjà bien assez long ; et puis il faut bien que la documentation écrite par les auteurs serve à quelque chose.

Enfin, comme je préfère écrire du code que du texte dans l’éditeur de WordPress, j’ai posé l’exemple d’utilisation des deux librairies sur Github. C’est ici : https://github.com/SopraSteriaTech/RefitRetrofitDemos

Leave a Reply

Your email address will not be published. Required fields are marked *