Chapitre Messagerie avec Quarkus
Quarkus et RabbitMQ
Nous avons utiliser RabbitMQ comme bus de message pour signaler les nouvelles adoptions et quand une adoption a été réalisée.
Nous allons visiter des fonctionnalités telles que :
- Utilisation de l'extension Quarkus pour RabbitMQ
- exécuter un code au démarrage de Quarkus
- ajouter de la configuration pour l'application
- configurer les traces et les logs
Objectifs
Ce tutoriel vous guidera à travers les étapes pour configurer RabbitMQ en tant que conteneur Docker et l'intégrer avec une application Quarkus afin d'envoyer et recevoir des événements de type "Nouvelle adoption disponible" et "Adoption finalisée".
Prérequis
- Docker installé sur votre machine
- Java JDK 11 ou supérieur
- Maven
- Une installation fonctionnelle de Quarkus CLI (optionnel)
Étape 1 : Lancer RabbitMQ avec Docker
- Créez un fichier
docker-compose.yml
pour configurer et démarrer RabbitMQ :
version: "3"
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: user
RABBITMQ_DEFAULT_PASS: password
- Lancez RabbitMQ avec Docker Compose :
docker compose up -d
RabbitMQ sera maintenant accessible sur localhost:5672 et l'interface de gestion sur localhost:15672.
Ajouter l'extension RabbitMQ
Ajoutez les dépendances nécessaires au backend et au frontend :
quarkus ext add io.quarkiverse.rabbitmqclient:quarkus-rabbitmq-client
./mvnw quarkus:add-extensions -Dextensions="quarkus-messaging-rabbitmq"
ou
<dependency>
<groupId>io.quarkiverse.rabbitmqclient</groupId>
<artifactId>quarkus-rabbitmq-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-messaging-rabbitmq</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.reactive.messaging</groupId>
<artifactId>microprofile-reactive-messaging-api</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-reactive-messaging-rabbitmq</artifactId>
</dependency>
Etape 3 : configurer Quarkus pour RabbitMQ
Ajoutez la configuration suivante dans src/main/resources/application.properties
:
mp.messaging.outgoing.adoption-events.connector=smallrye-rabbitmq
mp.messaging.outgoing.adoption-events.host=localhost
mp.messaging.outgoing.adoption-events.port=5672
mp.messaging.outgoing.adoption-events.username=user
mp.messaging.outgoing.adoption-events.password=password
mp.messaging.outgoing.adoption-events.exchange=adoption
Notez que dans ce cas, nous avons une configuration de connecteur entrant et une configuration de connecteur sortant, chacune ayant un nom distinct. Les propriétés de configuration sont structurées comme suit :
mp.messaging.[outgoing|incoming].{nom-du-canal}.propriété=valeur
Les propriétés de configuration fournies définissent les détails de connexion pour les canaux de messagerie sortants et entrants utilisant RabbitMQ dans une application Quarkus. Décomposons chaque propriété :
rabbitmq-host=localhost
rabbitmq-port=5672
rabbitmq-username=test
rabbitmq-password=test
mp.messaging.outgoing.adoption-events.connector=smallrye-rabbitmq
#mp.messaging.outgoing.adoption-events.host=localhost
#mp.messaging.outgoing.adoption-events.port=5672
mp.messaging.outgoing.adoption-events.username=test
mp.messaging.outgoing.adoption-events.password=test
mp.messaging.outgoing.adoption-events.exchange=adoption
Do not forget to create a user in the RabbitMQ admin console.
Etape 4 : Création des producteurs et consommateurs d'événements
Déclencher des événements RabbitMQ quand une nouvelle demande d'adoption est effectuée et quand un monstre est adopté
Nous allons créer la classe qui représente les événements d'adoption. Nous allons le créer dans le package com.byoskill.domain.adoption.events
et le nom de la classe sera AdoptionEvent
.
Créez une classe pour les événements AdoptionEvent dans le frontend et le backend.
package com.example.adoption;
public class AdoptionEvent {
public String type;
public String message;
public AdoptionEvent() {
}
public AdoptionEvent(String type, String message) {
this.type = type;
this.message = message;
}
}
Créez une classe pour produire les événements AdoptionEventProducer
dans le backend par exemple dans le package com.byoskill.domain.adapters.adoptions.rabbitmq
.
package com.byoskill.adapters.adoptions.rabbitmq;
import com.byoskill.domain.adoption.events.AdoptionEvent;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
@ApplicationScoped
public class AdoptionEventProducer {
@Channel("adoption-events")
Emitter<AdoptionEvent> emitter;
public void sendAdoptionAvailableEvent(String message) {
emitter.send(new AdoptionEvent("Nouvelle adoption disponible", message));
}
public void sendAdoptionFinalizedEvent(String message) {
emitter.send(new AdoptionEvent("Adoption finalisée", message));
}
}
Nous allons modifier AdoptionRepository
pour ajouter la méthode adoptMonster
.
Voici un exemple de code :
/**
* This method is used to notify that a monster have been adopted.
* @param monsterId the monster UUID
*/
void adoptMonster(String monsterId);
Voici un exemple d'implémentation pour le repository qui utilise une base mémoire :
public Uni<Monster> adoptMonster(String monsterId) {
Uni<Monster> monsterByUuid = getMonsterByUuid(monsterId);
return monsterByUuid.flatMap(monster -> {
deleteMonsterByUuid(monsterId);
return Uni.createFrom().item(monster);
});
}
Ajouter un point rest pour tester les adoptions
Modifier le contrôleur des adoptions pour ajouter un point rest pour tester les adoptions.
Voici un exemple de code à insérer :
@Path("/apply/{id}")
@ResponseStatus(204)
@POST
public void adoptMonster(@PathParam("id") final String id) {
adoptionRepository.adoptMonster(id);
}
Nous allons désormais connecter les actions de notre Repository à des événements RabbitMQ.
Pour simplifier, nous utiliserons un décorateur afin de ne pas avoir à dupliquer les codes.
package com.byoskill.domain.common;
import com.byoskill.adapters.adoptions.rabbitmq.AdoptionEventProducer;
import com.byoskill.domain.adoption.model.Monster;
import com.byoskill.domain.adoption.repository.AdoptionRepository;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.annotation.Priority;
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.inject.Inject;
import java.util.Optional;
import java.util.function.Function;
@Priority(10)
@Decorator
public class AdoptionRepositoryDecorator implements AdoptionRepository {
@Inject
@Delegate
AdoptionRepository delegate;
@Inject
AdoptionEventProducer eventProducer;
@Override
public Multi<Monster> getAllMonsters() {
return delegate.getAllMonsters();
}
@Override
public Uni<Monster> addMonsterToAdopt(Monster monster) {
Function<? super Monster, Uni<? extends Monster>> monsterUniFunction = monster1 ->{
if (monster1 != null) {
eventProducer.sendAdoptionAvailableEvent("Monster " + monster1.getName() + " is available for adoption");
}
return Uni.createFrom().item(monster1);
};
return delegate.addMonsterToAdopt(monster).flatMap(monsterUniFunction);
}
@Override
public Uni<Monster> getMonsterByUuid(String id) {
return getMonsterByUuid(id);
}
@Override
public Multi<Monster> searchMonstersByName(String pattern, Optional<Integer> size) {
return delegate.searchMonstersByName(pattern, size);
}
@Override
public Multi<Monster> searchMonstersByDescription(String pattern, Optional<Integer> size) {
return null;
}
@Override
public void deleteMonsterByUuid(String id) {
delegate.deleteMonsterByUuid(id);
}
@Override
public Uni<Monster> updateMonsterByUUID(String id, Monster monster) {
return delegate.updateMonsterByUUID(id, monster);
}
@Override
public Multi<Monster> searchMonstersByAge(Integer age) {
return delegate.searchMonstersByAge(age);
}
@Override
public Uni<Monster> adoptMonster(String monsterId) {
Function<? super Monster, Uni<? extends Monster>> onAdoption = monster1 ->{
if (monster1 != null) {
eventProducer.sendAdoptionAvailableEvent("Monster " + monster1.getName() + " has been adopted");
}
return Uni.createFrom().item(monster1);
};
return delegate.adoptMonster(monsterId)
.flatMap(onAdoption);
}
@Override
public Uni<Monster> changeName(Monster entityToBeUpdated, String newName) {
return delegate.changeName(entityToBeUpdated, newName);
}
}
Tester l'envoi d'événements
Nous allons tester l'envoi d'événements en faisant des appels CURL aux deux endpoints d'adoption et de demande d'adoption.
Ajout d'une adoption avec :
curl -X POST \
http://localhost:8090/adoptions \
-H 'Content-Type: application/json' \
-d '{
"name": "Fluffy",
"description": "A cute and cuddly monster",
"price": 100,
"age": 2,
"location": "New York"
}'
Pour réaliser une adoption ,remplacez monster-id par le UUID de l'adoption.
curl -X POST \
http://localhost:8090/adoptions/apply/monster-id \
-H 'Content-Type: application/json'
Dans RabbitMQ, un exchange devrait être créé pour les événements RabbitMQ.
Capturer les événements RabbitMQ et les afficher
Nous allons créer la même classe AdoptionEvent
dans le frontend.
Créez une classe pour les événements AdoptionEvent dans le frontend.
package com.example.adoption;
public class AdoptionEvent {
public String type;
public String message;
public AdoptionEvent() {
}
public AdoptionEvent(String type, String message) {
this.type = type;
this.message = message;
}
}
Créez une classe pour consommer les événements AdoptionEventConsumer dans le frontend.
package com.byoskill.adoption.events;
import io.smallrye.reactive.messaging.annotations.Blocking;
import io.vertx.core.json.JsonObject;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.reactive.messaging.Incoming;
@ApplicationScoped
public class AdoptionEventConsumer {
@Inject
AdoptionService adoptionService;
@Incoming("adoption-events")
@Blocking
public void process(JsonObject messagePayload) {
AdoptionEvent event = messagePayload.mapTo(AdoptionEvent.class);
if ("Nouvelle adoption disponible".equals(event.type)) {
adoptionService.handleNewAdoption(event.message);
} else if ("Adoption finalisée".equals(event.type)) {
adoptionService.handleFinalizedAdoption(event.message);
}
}
}
L'objet Java a été sérialisé automatiquement par le connecteur RabbitMQ dans le format JSON. Quand nous recevons le message, nous recevons un objet de type JsonObject qui doit être déserialisé en AdoptionEvent
.
Implémenter le service RabbitMQ
Implémentez le service AdoptionService :
package com.example.adoption;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AdoptionService {
public void handleNewAdoption(String message) {
// Logique pour gérer une nouvelle adoption
System.out.println("Nouvelle adoption disponible : " + message);
}
public void handleFinalizedAdoption(String message) {
// Logique pour gérer une adoption finalisée
System.out.println("Adoption finalisée : " + message);
}
}
N'oubliez pas d'ajouter la configuration RabbitMQ dans le fichier application.properties
rabbitmq-host=localhost
rabbitmq-port=5672
rabbitmq-username=test
rabbitmq-password=test
mp.messaging.outgoing.adoption-events.connector=smallrye-rabbitmq
#mp.messaging.outgoing.adoption-events.host=localhost
#mp.messaging.outgoing.adoption-events.port=5672
mp.messaging.outgoing.adoption-events.username=test
mp.messaging.outgoing.adoption-events.password=test
mp.messaging.outgoing.adoption-events.exchange=adoption
Tester l'envoi d'événements et la réception par le frontend.
Démarrez à la fois votre backend et votre frontend.
Utilisez les commandes CURL précédents et vérifiez que les événements sont bien envoyés et reçus dans le Frontend.
Conclusion
Dans cette leçon, nous avons abordé comment intégrer et configurer le connecteur de messaging RabbitMQ pour Quarkus afin de lancer des évènements RabbitMQ et les afficher dans le Frontend.
Eventuellement, configurez l'applicatin pour ne démarrer RabbitMQ qu'avec le profile rabbitmq activé!
Exemple :
@IfBuildProfile(anyOf = "rabbitmq")
@ApplicationScoped
public class AdoptionEventConsumer {
@Inject
AdoptionService adoptionService;
@Incoming("adoption-events")
@Blocking
public void process(JsonObject messagePayload) {
AdoptionEvent event = messagePayload.mapTo(AdoptionEvent.class);
if ("Nouvelle adoption disponible".equals(event.type)) {
adoptionService.handleNewAdoption(event.message);
} else if ("Adoption finalisée".equals(event.type)) {
adoptionService.handleFinalizedAdoption(event.message);
}
}
}