Exemple d'un client HTTP déclaratif de Micronaut en langage Kotlin

Destiné à la création de microservices sur la machine virtuelle Java, le framework Micronaut dispose de fonctionnalités impressionnantes facilitant l'activité du développeur.
L'une d'elles est notamment la possibilité de construire des clients HTTP réactifs, de manière déclarative, afin d'appeler un autre microservice Micronaut par exemple, ou bien de consommer une API externe.
C'est ce nous allons voir, au travers d'un exemple d'un tel client, dans une application Micronaut développée en langage Kotlin, pour consommer l'API SWAPI (The Star Wars API), et dont le code source est disponible ici : declarative-http-client.

Définition de l'interface du client HTTP

Lorsque l'on parle d'un client HTTP déclaratif, c'est qu'il suffit d'utiliser l'annotation @Client du package io.micronaut.http.client sur une interface ou une classe abstraite pour définir un client dont l'implémentation sera automatiquement prise en charge par Micronaut au moment de la compilation ; d'autres annotations correspondants aux différents verbes HTTP viennent aussi décorer les méthodes du type pour indiquer les types de requête à effectuer lors de l'invocation de ces méthodes.

Pour commencer simplement, voyons comment interroger l'API SWAPI pour obtenir des informations sur un personnage de la célèbre saga ; cela s'effectue par l'envoi d'une requête HTTP de type GET sur l'URI https://swapi.co/api/people/{id}, où {id} représente l'identifiant de la ressourse dont on veut obtenir une représentation.

Dans le code ci-après, l'annotation @Client placée sur l'interface SwApiClient permet de définir l'URL de base de l'API SWAPI à invoquer, tandis que sur la méthode getCharacterUsingMap, on trouve une annotation @Get permettant de spécifier une URL relative : cela permettra, lors de l'appel à la méthode, d'effectuer au final une requête de type GET vers la ressource cible.
Il y a ici deux choses à noter :

  • nous utilisons ici une URL relative paramétrée avec {id}, afin de transmettre l'id de la ressource à partir du paramètre de même nom passé dans la méthode ;
  • le type du retour de la méthode, devant être un type supporté par Micronaut, spécifie sous quel type la ressource demandée devra être retournée ; en l'occurence, la représentation JSON du personnage sera convertie en un objet de type Map<String,*>, encapsulé dans le type réactif Single.
1
2
3
4
5
6
@Client("https://swapi.co/api/")
interface SwApiClient {

    @Get("/people/{id}")
    fun getCharacterUsingMap(id: String): Single<Map<String,*>>
}

Utilisation du client HTTP

Pour utiliser le client HTTP ainsi défini par une interface, un moyen possible d'en obtenir une instance (plus précisément une instance du type implémentant l'interface), est celui de l'injection que Micronaut résout à la compilation.

Dans le code source du projet d'exemple Micronaut (declarative-http-client), une instance du client est obtenue par le contexte d'application (méthode main de l'objet Application), puis l'on invoque la méthode getCharacterUsingMap en lui passant la valeur 1 qui est l'id pour Luke Skywalker :

1
2
3
val client = applicationCtx.createBean(SwApiClient::class.java)
client.getCharacterUsingMap(id)
                .subscribe { m -> println(m) }

On se contente dans le code ci-dessus d'afficher la valeur présente dans l'objet Single, par l'expression lambda passée à la méthode subscribe ; lors de l'exécution, la console affiche les informations de la ressource Luke Skywalker, sous la forme d'une Map (transformée en chaîne de caractères) :

{name=Luke Skywalker, height=172, mass=77, hair_color=blond, skin_color=fair, eye_color=blue, birth_year=19BBY, gender=male, homeworld=https://swapi.co/api/planets/1/, films=[https://swapi.co/api/films/2/, https://swapi.co/api/films/6/, https://swapi.co/api/films/3/, https://swapi.co/api/films/1/, https://swapi.co/api/films/7/], species=[https://swapi.co/api/species/1/], vehicles=[https://swapi.co/api/vehicles/14/, https://swapi.co/api/vehicles/30/], starships=[https://swapi.co/api/starships/12/, https://swapi.co/api/starships/22/], created=2014-12-09T13:50:51.644000Z, edited=2014-12-20T21:17:56.891000Z, url=https://swapi.co/api/people/1/}

Cette Map correspond à l'objet JSON suivant retourné par l'API SWAPI :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
    "name": "Luke Skywalker", 
    "height": "172", 
    "mass": "77", 
    "hair_color": "blond", 
    "skin_color": "fair", 
    "eye_color": "blue", 
    "birth_year": "19BBY", 
    "gender": "male", 
    "homeworld": "https://swapi.co/api/planets/1/", 
    "films": [
        "https://swapi.co/api/films/2/", 
        "https://swapi.co/api/films/6/", 
        "https://swapi.co/api/films/3/", 
        "https://swapi.co/api/films/1/", 
        "https://swapi.co/api/films/7/"
    ], 
    "species": [
        "https://swapi.co/api/species/1/"
    ], 
    "vehicles": [
        "https://swapi.co/api/vehicles/14/", 
        "https://swapi.co/api/vehicles/30/"
    ], 
    "starships": [
        "https://swapi.co/api/starships/12/", 
        "https://swapi.co/api/starships/22/"
    ], 
    "created": "2014-12-09T13:50:51.644000Z", 
    "edited": "2014-12-20T21:17:56.891000Z", 
    "url": "https://swapi.co/api/people/1/"
}

Plus loin avec de la liaison de données

Pour des besoins de traitement, il serait plus clair de manipuler des types prédéfinis plutôt que des Maps ; c'est aussi prévu par Micronaut !
Définissons une méthode d'interface presque identique à la précédente, nommée getCharacter, mais retournant le type Single<Character>, où Character est une classe de données personnalisée représentant un personnage :

1
2
3
4
5
6
7
8
9
@Client("https://swapi.co/api/")
interface SwApiClient {

    @Get("/people/{id}")
    fun getCharacterUsingMap(id: String): Single<Map<String,*>>

    @Get("/people/{id}")
    fun getCharacter(id: String): Single<Character>    
}

Nous définissions la classe Character avec les propriétés qui nous intéressent, ici name, gender et birth_year :

1
2
data class Character(var name: String = "", var gender: String = "",
    var birth_year: String = "")

Character est une classe data pour laquelle on a défini une valeur par défaut pour chacun des paramètres du constructeur, de sorte que le mécanisme de liaison de données puisse fonctionner correctement (c'est comme si on avait défini un constructeur par défaut pour Character).

Ainsi dans le lambda passé à la méthode subscribe, on dispose maintenant d'un objet de type Character correctement initialisé avec les valeurs JSON correspondant aux propriétés de la classe :

1
2
3
val client = applicationCtx.createBean(SwApiClient::class.java)
client.getCharacter(id)
                .subscribe { c -> println(c) }

Intéressons-nous maintenant à l'ensemble des personnages de la saga, qui peut s'obtenir grâce à l'URL https://swapi.co/api/people/ de l'API SWAPI ; un appel vers cette URL avec une commande HTTP GET permet d'obtenir la première page contenant les représentations des 10 premiers personnages sur un total de 87, dans la propriété JSON results.
Par ailleurs, la structure JSON retournée comporte également les propriétés next et previous permettant à l'appelant de naviguer vers les différentes pages de l'ensemble de données.

Ajoutons une troisième méthode, getPeople, à notre interface SwApiClient, afin de permettre l'interrogation de l'ensemble des personnages, pour une page donnée :

1
2
3
4
5
6
7
8
9
10
11
12
@Client("https://swapi.co/api/")
interface SwApiClient {

    @Get("/people/{id}")
    fun getCharacterUsingMap(id: String): Single<Map<String,*>>

    @Get("/people/{id}")
    fun getCharacter(id: String): Single<Character>

    @Get("/people/?page={pageNumber}")
    fun getPeople(pageNumber: Int = 1): Single<PeopleResponse>
}

La classe de données PeopleResponse est définie comme suit :

1
2
data class PeopleResponse(var count: Int = 0, var next: String? = "",
    var results: List<Character>? = null)

Notez que cette classe de données comporte en particulier les propriétés next et results :

  • next, qui permettra de récupérer l'URL de la page suivante ;
  • results, qui est de type List<Character>, et contiendra la liste des personnages de la page visitée.

Ainsi, la liaison de données s'appliquera aussi bien pour la structure d'une page, que pour les résultats qu'elle retournera, sous la forme d'une liste de Character.
A titre d'exemple, le code source du projet declarative-http-client, contient du code qui parcourt l'ensemble des pages pour obtenir la totalité des personnages sous la forme d'une liste.

Documentation et guide

Pour en savoir plus sur les clients HTTP déclaratifs de Micronaut, reportez-vous à la section Declarative HTTP Clients with @Client du guide utilisateur du framework.
Je vous recommande également de lire le guide Micronaut HTTP Client accessible depuis la page Guides, et disponible dans différents langages.