Spring : Consommer une API avec WebClient
Tu as vu dans une précédente quête comment parser un fichier de données JSON avec la bibliothèque Jackson.
La suite logique est d'appeler une API afin d'en analyser le contenu.
Objectifs
- Savoir émettre une requête à une API avec WebClient
- Interpréter les données retournées par l'API
Spring WebFlux
En Java, il existe de nombreuses bibliothèques permettant d'appeler une API, par exemple :
-
OkHttp
-
Jersey
-
Retrofit
-
Volley
À cela s'ajoutent les bibliothèques spécifiques à Spring :
-
RestTemplate
-
WebClient
1. Spring WebFlux
La version 5 de Spring inclut la bibliothèque Spring WebFlux qui permet d'apporter les fonctionnalités liées à la programmation réactive.
La programmation réactive est un paradigme de programmation qui tient compte du fait que l'action est asynchrone, à savoir non-bloquante.
Lors d'un long processus (par exemple : charger et afficher la liste de tous les utilisateurs d'un site), on peut comparer deux paradigmes :
-
la programmation impérative : le programme va attendre la fin de la tâche avant de poursuivre son exécution
-
la programmation réactive : le programme va poursuivre son exécution (il sera donc possible d'effectuer d'autres actions) et il sera prévenu de la fin de la tâche (ou de l'avancée, voire de son échec).
2. WebClient
Comme ici tu vas travailler avec Spring Boot, tu vas découvrir la bibliothèque WebClient. La bibliothèque d'origine, RestTemplate, est dépréciée et sera abandonnée à la version 7 de Spring.
La bibliothèque WebClient est contenue dans la brique Spring Reactive Web : il faudra donc l'ajouter à Spring Initializr à la création d'un projet (tu peux chercher le terme "webflux").

Tu peux aussi l'ajouter à un projet déjà existant en modifiant le fichier pom.xml :
<dependencies>
<!-- previous dependencies... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
Arrêter la lecture de cette ressource (en français) avant la section "Let's code"
Lis cette ressource (en français) en entier, même si tu ne comprends pas tout. Elle est complexe, mais elle introduit très bien la notion de programmation réactive !
Appeler une API
Appeler une API avec WebClient dans un projet Spring Boot possédant la bibliothèque Spring WebFlux est facile (si, vraiment, tu peux me croire !).
1. Récupérer un JSON
La première étape consiste à initialiser un objet WebClient en lui fournissant une url d'API :
String url = "https://api.chucknorris.io/jokes/random";
WebClient webClient = WebClient.create(url);
Ensuite on crée un objet Mono en demandant à notre objet WebClient de récupérer la réponse vers une chaîne de caractères.
Mono<String> call = webClient.get()
.retrieve()
.bodyToMono(String.class);
Attention, à l'heure actuelle
callne contient pas le résultat de l'appel à l'API : c'est l'appel lui-même !
Spring WebFlux utilise la bibliothèque Reactor qui donne accès aux objets Mono et Flux. Ces classes permettent d'accéder à des données de façon réactive :
-
Monopermet de fournir un unique résultat -
Fluxpermet de fournir un ensemble de résultats
Pour récupérer la réponse, tu auras deux possibilités :
-
soit en asynchrone, avec
subscribe: on s'inscrit à un traitement et on est prévenu quand il est terminé -
soit en synchrone, avec
block: on bloque le programme pour attendre la fin du traitement
C'est l'option synchrone block que tu vas suivre avec cette quête (pas d'inquiétude, tu découvriras l'option asynchrone subscribe dans une future quête) :
String response = call.block();
Ici response contient le JSON issu de l'appel de l'API, dans notre cas :
{
"categories": [
"science"
],
"created_at": "2016-05-01 10:51:41.584544",
"icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
"id": "jtibnb-mtuyluzqrwtodzw",
"updated_at": "2016-05-01 10:51:41.584544",
"url": "https://api.chucknorris.io/jokes/jtibnb-mtuyluzqrwtodzw",
"value": "Chuck Norris can divide by zero."
}
Voici le code complet d'un appel d'API et de la récupération du résultat (tu vois que c'était simple !) :
String url = "https://api.chucknorris.io/jokes/random";
WebClient webClient = WebClient.create(url);
Mono<String> call = webClient.get()
.retrieve()
.bodyToMono(String.class);
String response = call.block();
Passage de paramètres
1. Paramètres de requête
Certaines routes d'API s'attendent à avoir des paramètres pour fonctionner : https://api.chucknorris.io/jokes/search.
En l'état, ta requête ne marche pas (erreur 400) : elle s'attend à avoir un paramètre "query", par exemple : https://api.chucknorris.io/jokes/search?query=zero.
Afin d'appeler une url avec un (ou plusieurs) paramètre(s), le début de l'opération ne change pas :
String url = "https://api.chucknorris.io/jokes/search";
WebClient webClient = WebClient.create(url);
Tu remarques que l'on appelle l'url sans les paramètres.
On crée là-aussi un objet Mono en demandant à notre objet WebClient de récupérer la réponse vers une chaîne de caractères.
Sauf que cette fois, tu vas modifier la construction de l'url avec un UriBuilder et lui préciser les paramètres à ajouter avec la méthode queryParam :
Mono<String> call = webClient.get()
.uri(uriBuilder-> uriBuilder
.queryParam("query", "zero")
.build())
.retrieve()
.bodyToMono(String.class);
Si tu te demandes ce que signifie la flèche
->, on appelle cela un lambda : il permet de redéfinir la méthode abstraite d'une interface fonctionnelle. Quoi, tu comprends encore moins ? Pas de souci, on verra ça plus tard !
Voici le code complet qui permet de récupérer le JSON du résultat de l'appel à l'API :
String url = "https://api.chucknorris.io/jokes/search";
WebClient webClient = WebClient.create(url);
Mono<String> call = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("query", "zero")
.build())
.retrieve()
.bodyToMono(String.class);
String response = call.block();
Si tu avais plusieurs paramètres, il te suffirait de les enchaîner. Par exemple :
uriBuilder
.queryParam("name", "Eric Cartman")
.queryParam("age", 10)
.queryParam("psychopath", true)
.build()
L'url construite aura comme paramètres
?name=Eric%20Cartman&age=10&psychopath=true
2. Variables de chemin
Sur le même principe, certaines API s'attendent à avoir des path variables renseignées, par exemple : http://hp-api.herokuapp.com/api/characters/house/{house}.
En l'état, si la variable {house} n'a pas de valeur, une erreur apparaîtra (Cannot GET /api/characters/house/).
Si l'on précise une valeur, le résultat s'affichera correctement : http://hp-api.herokuapp.com/api/characters/house/gryffindor.
Comme pour les paramètres d'url, tu vas devoir utiliser un UriBuilder. Cette fois tu devras préciser les path variables dans la méthode path et les valeurs de ces variables iront dans la méthode build :
String url = "http://hp-api.herokuapp.com/api/characters";
WebClient webClient = WebClient.create(url);
Mono<String> call = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/house/{house}")
.build("gryffindor"))
.retrieve()
.bodyToMono(String.class);
String response = call.block();
Si ton url possède plusieurs path variables, il faudra toutes les définir dans path et passer autant de valeurs que nécessaire dans build :
uriBuilder -> uriBuilder
.path("/name/{name}/age/{age}/psychopath/{psychopath}")
.build("Eric Cartman", 10, true)
L'url construite aura comme path variables
/name/Eric%20Cartman/age/10/psychopath/true
Garde cette ressource sous le coude, elle n'est pas à lire en détail maintenant
Interpréter le JSON
C'est bien sympa d'appeler une API et d'en récupérer la réponse en JSON, mais encore faut-il interpréter ce résultat !
Il te serait possible d'utiliser des outils internes à Spring Boot pour parser le JSON. Cependant, tu as déjà vu comment parser du JSON avec la bibliothèque Jackson (sinon arrête cette quête tout de suite et va faire l'autre !).
L'information qu'il te manque, c'est comment parser un JSON à partir d'une chaîne de caractères, plutôt qu'à partir d'un fichier.
Imagine que tu as déjà récupéré String response lors de l'appel de l'API :
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode root = objectMapper.readTree(response);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Les blocs
tryetcatchservent à intercepter des erreurs. Ici, par exemple, la chaîne de caractères récupérée n'est peut-être pas formatée correctement.
Si une classe contenant les données nécessaires existe, il est aussi possible de récupérer les informations directement au format voulu.
Pour le JSON suivant :
{
"categories": [
"science"
],
"created_at": "2016-05-01 10:51:41.584544",
"icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
"id": "jtibnb-mtuyluzqrwtodzw",
"updated_at": "2016-05-01 10:51:41.584544",
"url": "https://api.chucknorris.io/jokes/jtibnb-mtuyluzqrwtodzw",
"value": "Chuck Norris can divide by zero."
}
La classe suivante a été créée :
public class ChuckNorris {
private String id;
private String value;
private String url;
@JsonProperty("icon_url")
private String iconUrl;
private String[] categories;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
// TODO : add an empty constructor
// TODO : add getters and setters
}
Pour que ce soit plus lisible, le constructeur est vide et les getters/setters n'apparaissent pas dans le code de l'exemple. Cependant, il faut qu'ils soient présents pour que le processus de mapping fonctionne. Si tu te demandes ce que viennent faire ces
@JsonProperty, relis la quête sur Jackson ;)
Il faut utiliser la méthode readValue, en lui précisant le type vers lequel convertir le JSON : ici ChuckNorris.class.
ObjectMapper objectMapper = new ObjectMapper();
ChuckNorris fact = null;
try {
fact = objectMapper.readValue(response, ChuckNorris.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
Attention : pour convertir un JSON directement en objet, il est nécessaire de renseigner tous les attributs du JSON dans la classe de destination. Si tu enlevais les attributs createdAt et updatedAt de la classe ChuckNorris, le mapping automatique ne fonctionnerait plus !
Challenge
Star Wars API
Fais un fork du dépôt suivant : https://github.com/WildCodeSchool/spring-swapi.
Tu reprends le développement d'un site de fans de Star Wars afin de lui ajouter des fonctionnalités. À l'heure actuelle, il est possible de rechercher un personnage grâce à son identifiant.
L'API utilisée est accessible à cette adresse : https://swapi.dev/.
Tu peux déjà tester le site et voir comment tout cela fonctionne.
Ta mission est d'ajouter la recherche des planètes par identifiant. Par exemple, l'url de l'API pour la planète 1 sera : https://swapi.dev/api/planets/1/.
Le "/" à la fin de l'url est important !
Critères de validation
- Lors de l'appel du formulaire de recherche de planète, le nom et le nombre d'habitants de la planète apparaissent correctement.
- Les données sont bien cherchées de l'API.
- Une conversion du JSON vers le type
Planeta bien été effectuée.