Chapitre Développement avec Quarkus
Utilisation de la programmation réactive
Nous allons modifier notre code existant pour utiliser la programmation réactive.
Objectifs
Nous allons donc modifier les portions de code suivantes :
- convertir les contrôleurs du frontend en ressources asynchrones
- modifier le client REST qui retourne le message de bienvenue pour retourner des réponses asynchrones
- modifier le contrôleur du formulaire d'adoption
- modifier les contrôleurs du backend pour retourner des réponses asynchrones
Architecture de notre application.
Un peu d'information concernant le framework Mutiny.
La classe Uni
est une interface qui représente une valeur asynchrone. Elle provient du framework Jakarta EE et la bibliothèque Mutiny. Vous pouvez trouver de la documentation sur ce framework ici.
Mutiny propose deux types à la fois événementiels et paresseux:
-
Un
Uni
émet un seul événement (un élément ou un échec). Les unités sont pratiques pour représenter des actions asynchrones qui renvoient un résultat 0 ou 1. Un bon exemple est le résultat de l'envoi d'un message à une file d'attente de courtier de messages. -
Un
Multi
émet plusieurs événements (n éléments, 1 échec ou 1 achèvement). Les Multis peuvent représenter des flux d'éléments, potentiellement illimités. Un bon exemple est la réception de messages d'une file d'attente de courtier de messages.
Modification de la ressource qui retourne la page d'accueil
Nous allons commencer en ouvrant la classe java HomePage.java
.
Nous allons rechercher la méthode qui définit le code qui retourne la page d'accueil.
Identifiez la ligne suivante :
@Blocking
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get() {
Nous allons modifier la ligne comme suit :
@GET
@Produces(MediaType.TEXT_HTML)
public Uni<TemplateInstance> get() {
Nous devons également produire une valeur asynchrone à partir du template HTML.
Avant :
return index.data("motd", welcomeMessage.message());
Après :
return Uni.createFrom().item(() -> index.data("motd", welcomeMessage.message()));
Modification du client REST qui gère la communication
Nous allons modifier la classe CommunicationMessageClient.java
.
Recherchonz la méthode qui définit le code qui retourne le message de bienvenue.
Identifiez la ligne suivante :
@GET
@Produces(MediaType.APPLICATION_JSON)
WelcomeMessage getWelcomeMessage();
Nous allons modifier la ligne comme suit :
@GET
@Produces(MediaType.APPLICATION_JSON)
Uni<WelcomeMessage> getWelcomeMessage();
Nous devons également modifier le code qui injecte la donnée dans le template Qute. En effet, nous appelions la méthode message()
pour récupérer le message de bienvenue depuis la classe WelcomeMessage
.
Avant :
return Uni.createFrom().item(() -> index.data("motd", welcomeMessage.message()));
Nous devons convertir la promesse dans le cas oû le client REST retourne avec succès la valeur du message de bienvenue.
Pour transformer une promesse en une autre promesse, il est nécessaire d'utiliser la méthode transform()
. Cette méthode est disponible dans la bibliothèque Mutiny via onItem()
qui permet de configurer ce qui est nécessaire de se produire quand l'objet Uni
observé émet un élément.
Après :
Uni<String> welcomeMessage = CommunicationMessageClient.getWelcomeMessage().onItem().transform(msg ->msg.message());
return Uni.createFrom().item(() -> index.data("motd", welcomeMessage));
Modification de contrôleur qui gère les adoptions
Modification de la ressource qui retourne le formulaire d'adoption
Nous allons modifier la méthode qui retourne le formulaire d'aoption.
Identifiez les lignes suivantes :
@Blocking
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get() {
return adoptionForm.instance();
}
Nous allons modiier ces lignes pour retourner un Uni
et supprimer l'annotation qui indique que la méthode est bloquante.
Après :
@GET
@Produces(MediaType.TEXT_HTML)
public Uni<TemplateInstance> get() {
return Uni.createFrom().item(() -> adoptionForm.instance());
}
Modification de la ressource qui gère la soumission d'une adoption
Identifiez la ligne suivante dans le contrôleur AdoptionForm.java
.
@POST
@Path("/submit")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response addMonster(
@FormParam("name") String name,
@FormParam("description") String description,
@FormParam("price") Integer price,
@FormParam("age") Integer age,
@FormParam("location") String location,
@Context UriInfo uriInfo) {
Nous allons également retourner un objet Uni
pour gérer la réponse. Il faut toutefois encapsuler le code qui gère la soumission dans le callback de notre objet Uni.
Voici un exemple :
LOGGER.info("Received new adoption request with the following details :" + name + ", " + description + ", "
+ price + ", " + age + ", " + location);
return Uni.createFrom().item(() -> {
URI uri = uriInfo.getBaseUriBuilder().path("/").build();
MonsterForm monster = new MonsterForm();
monster.setName(name);
monster.setDescription(description);
monster.setPrice(price);
monster.setAge(age);
monster.setLocation(location);
adoptionClient.addMonster(monster);
return Response.seeOther(uri).build();
});
Dans notre exemple, nous devons faire attentionm, effectivement la méthode adoptionClient.addMonster(monster);
est bloquante et nous allons devoir la modification dans l'étape suivante pour la rendre asynchrone.
Soumettre de manière asynchrone la demande d'adoption.
Ouvrez la classe AdoptionClient.java
dans le projet du frontend.
Recherchez la méthode qui implémente l'envoi de la demande d'adoption au backend :
MonsterForm addMonster(MonsterForm monsterForm);
Rendre le backend asynchrone
Nous allons modifier les contrôleurs du backend pour les rendre asynchrones.
Modifier le controleur du backend qui gère les messages de bienvenue
Recherchez la classe GreetingResource.java
dans le projet du backend.
Identifiez les lignes suivantes :
@GET
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> motd() {
return Map.of("message","Bienvenue sur notre site d'adoption");
}
Modifiez le code afin de le rendre asynchrone.
Est-il nécessaire d'utiliser Uni ou Multi dans le cas d'une Map?.
Modifier l'interface du Repository qui gère les adoptions
Nous allons modifier l'interface de notre contrat pour renforcer l'utlisation de pratiques asynchrones dans les futures implémentations de notre backend.
Recherchez la classe AdoptionRepository.java
et modifiez la comme dans cet exemple :
package com.byoskill.adoption.repository;
import java.util.List;
import com.byoskill.adoption.model.Monster;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
/**
* This interface describe the list of methods available to interact with the adoption system.
*/
public interface AdoptionRepository {
Multi<Monster> getAllMonsters();
Uni<Monster> addMonsterToAdopt(Monster monster);
Uni<Monster> getMonsterByUuid(String id);
Multi<Monster> searchMonstersByName(String name);
void deleteMonsterById(String id);
Uni<Monster> updateMonsterByUUID(String id, Monster monster);
}
Modifier le contrôleur du backend qui gère les adoptions
Recherchez la classe AdoptionResource.java
dans le backend.
Modifiez l'ensemble des ressources afin de rendre l'appel à ces ressources asynchrones.
Avant les modifications :
package com.byoskill.adoption.controllers;
import com.byoskill.adoption.model.Monster;
import com.byoskill.adoption.repository.AdoptionRepository;
import jakarta.inject.Inject;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import org.jboss.resteasy.reactive.ResponseStatus;
@Path("/adoptions")
@Produces(MediaType.APPLICATION_JSON)
public class AdoptionResource {
@Inject
AdoptionRepository adoptionRepository;
@GET
public MonsterView getAllMonsters() {
return new MonsterView(adoptionRepository.getAllMonsters());
}
@GET
@Path("/{id}")
public Monster getMonsterByUuid(String id) {
return adoptionRepository.getMonsterByUuid(id);
}
@GET
@Path("/search/{name}")
public MonsterView searchMonstersByName(String name) {
return new MonsterView(adoptionRepository.searchMonstersByName(name));
}
@POST
public Monster createMonster(Monster monster) {
adoptionRepository.addMonsterToAdopt(monster);
return monster;
}
@DELETE
@Path("/{id}")
@ResponseStatus(204)
public void deleteMonsterById(String id) {
adoptionRepository.deleteMonsterById(id);
}
@PUT
@Path("/{id}")
public Monster updateMonsterById(String id, Monster monster) {
return adoptionRepository.updateMonsterByUUID(id, monster);
}
}
Changeons d'abord les signatures des méthodes pour refléter l'utilisation de Mutiny.
package com.byoskill.adoption.controllers;
import com.byoskill.adoption.model.Monster;
import com.byoskill.adoption.repository.AdoptionRepository;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import org.jboss.resteasy.reactive.ResponseStatus;
@Path("/adoptions")
@Produces(MediaType.APPLICATION_JSON)
public class AdoptionResource {
@Inject
AdoptionRepository adoptionRepository;
@GET
public Uni<MonsterView> getAllMonsters() {
return new MonsterView();
}
@GET
@Path("/{id}")
public Uni<Monster> getMonsterByUuid(String id) {
return adoptionRepository.getMonsterByUuid(id);
}
@GET
@Path("/search/{name}")
public Uni<MonsterView> searchMonstersByName(String name) {
return new MonsterView(adoptionRepository.searchMonstersByName(name));
}
@POST
public Uni<Monster> createMonster(Monster monster) {
adoptionRepository.addMonsterToAdopt(monster);
return monster;
}
@DELETE
@Path("/{id}")
@ResponseStatus(204)
public void deleteMonsterById(String id) {
adoptionRepository.deleteMonsterById(id);
}
@PUT
@Path("/{id}")
public Uni<Monster> updateMonsterById(String id, Monster monster) {
return adoptionRepository.updateMonsterByUUID(id, monster);
}
}
Modifions maintenant le code des méthodes pour profiter de la modification de notre interface désormais asynchrone.
package com.byoskill.adoption.controllers;
import com.byoskill.adoption.model.Monster;
import com.byoskill.adoption.repository.AdoptionRepository;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import org.jboss.resteasy.reactive.ResponseStatus;
@Path("/adoptions")
@Produces(MediaType.APPLICATION_JSON)
public class AdoptionResource {
@Inject
AdoptionRepository adoptionRepository;
@GET
public Uni<MonsterView> getAllMonsters() {
return adoptionRepository.getAllMonsters().collect().asList().map(MonsterView::new);
}
@GET
@Path("/{id}")
public Uni<Monster> getMonsterByUuid(String id) {
return adoptionRepository.getMonsterByUuid(id);
}
@GET
@Path("/search/{name}")
public Uni<MonsterView> searchMonstersByName(String name) {
return adoptionRepository.searchMonstersByName(name).collect().asList().map(MonsterView::new);
}
@POST
public Uni<Monster> createMonster(Monster monster) {
return adoptionRepository.addMonsterToAdopt(monster);
}
@DELETE
@Path("/{id}")
@ResponseStatus(204)
public void deleteMonsterById(String id) {
adoptionRepository.deleteMonsterById(id);
}
@PUT
@Path("/{id}")
public Uni<Monster> updateMonsterById(String id, Monster monster) {
return adoptionRepository.updateMonsterByUUID(id, monster);
}
}
Modifier notre implémentation mémoire du composant stockant les adoptions de monstres.
Nous allons modifier le fichier AdoptionMemoryRepository.java
.
En effet, nous avons modifié le contrat de l'interface AdoptionRepository.java
et désormais nous devons reporter les modifications dans notre implémentation.
Cette page peut vous aider à réaliser les modifications.
Voici par exemple l'implémentation avant modification de notre composant stockant les adoptions de monstres :
package com.byoskill.adapters.memory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import com.byoskill.adoption.model.Monster;
import com.byoskill.adoption.repository.AdoptionRepository;
import io.smallrye.mutiny.Multi;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AdoptionMemoryRepository implements AdoptionRepository {
private static int counter = 0;
private List<Monster> monsters;
public AdoptionMemoryRepository() {
monsters = new ArrayList<>();
}
@Override
public Multi<Monster> getAllMonsters() {
return Collections.unmodifiableList(monsters);
}
public void addMonsterToAdopt(Monster monster) {
monsters.add(monster);
monster.setId(++counter);
monster.setMonsterUUID(UUID.randomUUID().toString());
}
@Override
public Monster getMonsterByUuid(String id) {
return monsters.stream().filter(m -> Objects.equals(m.getMonsterUUID(), id)).findFirst().orElse(null);
}
@Override
public List<Monster> searchMonstersByName(String name) {
return monsters.stream().filter(m -> m.getName().contains(name)).collect(Collectors.toList());
}
@Override
public void deleteMonsterById(String id) {
monsters.removeIf(m -> m.getMonsterUUID().equals(id));
}
@Override
public Monster updateMonsterByUUID(String uuid, Monster monster) {
if (uuid == null || monster == null) {
return null;
}
Monster monsterToUpdate = getMonsterByUuid(uuid);
if (monsterToUpdate != null) {
if (monster.getName() != null) monsterToUpdate.setName(monster.getName());
if (monster.getAge() != null) monsterToUpdate.setAge(monster.getAge());
if (monster.getDescription() != null) monsterToUpdate.setDescription(monster.getDescription());
if (monster.getLocation() != null) monsterToUpdate.setLocation(monster.getLocation());
if (monster.getPrice() != null) monsterToUpdate.setPrice(monster.getPrice());
} else throw new IllegalArgumentException("Monster not found");
return monsterToUpdate;
}
}
Modifier les tests du composant de gestion des adoptions
Nous allons devoir modifier les tests pour la gestion des adoptions car nous avons modifier l'interface AdoptionRepository
.
Les deux tests AdoptionTest.java
et MonsterRepositoryTest.java
doivent être adaptés.
Même s'il est possible d'utiliser directement JUnit avec Mutiny, Mutiny fournit un ensemble de méthodes pour simplifier l'écriture de nos tests.
Références :
- https://quarkus.io/guides/cdi-reference
- https://quarkus.io/guides/cdi
- https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#bean_defining_annotations
- Advanced testing.
- Advanced testing 2
- Mutiny Unit-testing