L'antenne de la guilde fonctionne désormais correctement.
Elle interroge la taverne principale, récupère les aventuriers, envoie les nouveaux arrivants au registre, et le tout repose sur un client typesafe généré automatiquement par SmallRye GraphQL à partir de quelques interfaces Java.
Mais deux questions sont restées sans réponse dans l'article précédent.
La première : si le schéma GraphQL de la taverne existe déjà, pourquoi écrire les modèles Java à la main ? SmallRye GraphQL propose un générateur expérimental capable de produire ces classes automatiquement depuis le schéma distant. La seconde : le client typesafe convient parfaitement quand les opérations sont connues à l'avance, mais que se passe-t-il quand le messager doit improviser, quand la requête ne peut pas être figée dans une interface Java ?
Ce sont ces deux chemins que nous allons explorer.
Générer le client depuis le schéma : la promesse du générateur
Jusqu'ici, l'antenne de la guilde recopiait patiemment les modèles de la taverne principale.
Elle relevait chaque champ qui l'intéressait, créait ses classes Java à la main, et s'assurait que les noms correspondaient bien aux types GraphQL distants.
Un travail de scribe appliqué, mais fastidieux, et surtout fragile : si la taverne fait évoluer son registre, l'antenne doit s'en apercevoir seule et corriger ses propres copies sans se tromper.
Dans l'écosystème Quarkus, SmallRye GraphQL propose une réponse à ce problème sous la forme d'un générateur de code.
L'idée est séduisante : on dépose le schéma de la taverne dans les ressources du projet, on déclare les requêtes que l'antenne souhaite exécuter, et le générateur produit automatiquement les classes Java correspondantes à la compilation. Plus de recopie manuelle, plus de risque de désalignement entre le registre de la taverne et les modèles de l'antenne.
Pour l'équiper, deux ajouts suffisent. D'abord la dépendance :
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-graphql-client-generator</artifactId>
<version>3.0.0.Beta1</version>
</dependency>
Le module pour la génération de client
Puis la configuration du compilateur Maven pour activer le processeur d'annotations :
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
<annotationProcessorPaths>
<path>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-graphql-client-generator</artifactId>
<version>3.0.0.Beta1</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
On active l'annotation processor du module
Le schéma de la taverne, récupéré depuis /graphql/schema.graphql, est déposé dans les ressources du projet.
L'antenne déclare ensuite ses requêtes directement sur une interface, en GraphQL pur, sans avoir à écrire les modèles Java à la main :
@GraphQLSchema("resource:schema.graphql")
@GraphQLQuery("""
query Aventuriers {
aventuriers {
id
nom
classe
niveau
quetes {
id
titre
difficulte
recompenseOr
}
}
}
""")
@GraphQLQuery("""
query Aventurier($id: Int) {
aventurier(id: $id) {
id
nom
classe
niveau
quetes {
id
titre
difficulte
recompenseOr
}
}
}
""")
public interface TavernQuery {
}
Notre schéma
Le générateur lit le schéma, analyse les requêtes déclarées, et produit à la compilation les classes Java correspondantes.
Sur le papier, c'est exactement ce dont l'antenne avait besoin : un scribe automatique qui recopie fidèlement le registre de la taverne sans intervention humaine.
public interface TavernQueryApi {
@Query("aventuriers") @NonNull List<@NonNull AventurierResponse> Aventuriers();
@Query("aventurier") AventurierResponse Aventurier(Integer id);
}
public class AventurierResponse {
@NonNull String classe;
@NonNull Integer id;
@NonNull Integer niveau;
@NonNull String nom;
@NonNull List<@NonNull QueteResponse> quetes;
}Une partie des classes générées
Mais en pratique, le scribe a ses lacunes. Lors de nos tests, le générateur n'a produit les types imbriqués qu'à moitié : la classe Quete ne contenait que son champ id, les autres champs ayant tout simplement disparu en chemin.
Ce n'est pas un cas limite obscur : c'est le comportement de base sur des objets GraphQL imbriqués, qui sont pourtant au cœur de la plupart des API réelles.
public class QueteResponse {
@NonNull Integer id;
}La raison de ces lacunes devient claire en creusant un peu : le module smallrye-graphql-client-generator n'a pas reçu la moindre évolution depuis cinq ans. En ouvrant une issue pour signaler le problème des types imbriqués, la réponse de l'équipe a été directe, ils reconnaissent eux-mêmes que le générateur est resté sans attention, et évoquent vaguement la possibilité de le ressusciter un jour.
L'issue ouverte sur ce problème
Une intention sympathique, mais qui ne résout rien aujourd'hui.
Le client dynamique : quand le messager doit improviser
Le client typesafe repose sur une hypothèse forte : les opérations sont connues à l'avance.
L'interface Java déclare les Query et les Mutation disponibles, le framework génère les requêtes correspondantes, et les messagers partent toujours avec des instructions précises gravées dans le parchemin.
Mais un soir, un aventurier inhabituel pousse la porte de l'antenne.
Ce n'est pas un guerrier venu consulter la liste des quêtes, ni un mage cherchant les récompenses en or. C'est un cartographe itinérant qui ne sait pas encore exactement ce dont il a besoin.
Il veut explorer le registre librement, composer sa propre demande selon ce qu'il découvre en chemin, choisir les champs qui l'intéressent au dernier moment, sans que personne n'ait pu anticiper sa requête à l'avance.
Face à lui, le client typesafe est désarmé. Une interface Java figée ne peut pas répondre à une question qui change de forme à chaque visite. Il faut un messager différent, capable d'improviser sa mission plutôt que de suivre un script : c'est le client dynamique.
@Inject
@GraphQLClient("tavern-dynamic")
DynamicGraphQLClient dynamicClient;Plutôt que de s'appuyer sur une interface annotée, ce messager construit ses requêtes à la volée, champ par champ, selon les besoins du moment :
Document query = document(
operation(
field("aventuriers",
field("nom"),
field("niveau"),
field("quetes",
field("titre"),
field("recompenseOr")
)
)
)
);
Response response = dynamicClient.executeSync(query);
return response.getList(Map.class, "aventuriers");
La requête n'est plus gravée dans le marbre avant le départ. Elle se compose au moment où le cartographe formule sa demande, s'adapte à ce qu'il cherche, et peut prendre une forme complètement différente à sa prochaine visite.
L'antenne répond sans avoir eu besoin de prévoir chaque combinaison possible.
Lorsque le messager ne peut pas non plus se permettre d'attendre la réponse de la taverne avant de continuer sa route, il peut partir en éclaireur et récupérer les informations de façon asynchrone :
Uni<Response> response = dynamicClient.execute(query);
Mais cette liberté a un prix. En abandonnant l'interface Java typesafe, on perd le filet de sécurité que le compilateur offrait.
Il ne peut plus vérifier que les champs demandés existent réellement dans le registre de la taverne, ni que les types correspondent. Si la taverne renomme une colonne ou restructure ses aventuriers, le messager ne s'en apercevra qu'en revenant bredouille, à l'exécution, sans que personne ne l'ait prévenu au départ.
C'est pourquoi le client dynamique n'est pas destiné à remplacer le client typesafe dans le quotidien de l'antenne.
Il coexiste avec lui, disponible pour les situations où la rigidité d'une interface Java devient un obstacle plutôt qu'une protection : exploration d'un schéma inconnu, construction d'outils génériques, ou requêtes qui doivent vraiment se composer à la volée selon des paramètres imprévisibles.
Pour tout le reste, le messager avec ses instructions écrites reste le choix le plus sûr.
Conclusion
L'antenne de la guilde dispose maintenant d'une vision complète de ce que SmallRye GraphQL Client peut offrir à une taverne qui cherche à grandir.
Le client typesafe couvre la grande majorité des missions quotidiennes : les opérations sont connues, les modèles Java sont solides, et les messagers partent toujours avec des instructions claires. Le générateur expérimental pousse cette logique encore plus loin en confiant le travail de recopie du registre à un outil automatique, au prix d'une certaine prudence sur sa stabilité actuelle. Et le client dynamique garde une place de choix pour les visiteurs inhabituels, ceux dont les demandes ne pouvaient pas être anticipées et qui ont besoin d'un messager capable de s'adapter sur le chemin.
Dans un royaume bien organisé, chaque type de messager connaît son rôle. C'est ce qui distingue une antenne qui tient la route d'une échoppe qui improvise à chaque nouvelle demande.
Tout le code relatif à cet article est consultable ici :