Dernière modification : Dec 08 , 2024

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

  1. 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
  1. 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.

Organisation du projet

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.

Rabbit MQ Exchange creation

Capturer les événements RabbitMQ et les afficher

Nous allons créer la même classe AdoptionEventdans 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.

Rabbit MQ Integration

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);
        }
    }
}

References