CodeGym /Cours /Docker for Spring /Profil messaging dans Spring Boot

Profil messaging dans Spring Boot

Docker for Spring
Niveau 20, Leçon 2
Disponible

1. Rôle du profil messaging

Si vous avez travaillé uniquement avec l'exécution locale bootRun, vous pouvez penser que les profils sont juste un « embellissement » plutôt qu'un outil. Mais dans un environnement conteneurisé, les profils sont littéralement un interrupteur d'infrastructure : aujourd'hui le service vit seul, demain avec une base de données, après-demain avec Redis et RabbitMQ, tandis que l'image Docker reste la même. Si nous activons aveuglément la configuration RabbitMQ en permanence, nous casserons la moitié des modes du projet : standalone et même postgres,cache commenceront à essayer de se connecter à un broker qui n'existe pas.

En pratique, cela se présente ainsi : nous voulons que l'application n'exige pas RabbitMQ par défaut. Elle doit démarrer et fonctionner normalement sans broker, jusqu'à ce que vous activiez le profil messaging. Cela correspond parfaitement à notre principe de cours : same image, different runtime config.

C'est pourquoi nous faisons deux choses simultanément.

Premièrement, dans la configuration application-messaging.yml, nous décrivons les propriétés de connexion au broker et quelques petits réglages qui ne doivent exister qu'en mode messaging.

Deuxièmement, dans le code, nous marquons les beans liés à RabbitMQ — principalement Queue et les services d'envoi/réception — avec l'annotation @Profile("messaging"), afin qu'ils ne soient pas créés dans les autres modes.

Par exemple, dans un lancement Compose, cela ressemblera à :

services:
  app:
    environment:
      SPRING_PROFILES_ACTIVE: postgres,cache,messaging # Même image, simplement le mode messaging est activé

Le sens de cette ligne n'est pas que nous avons « écrit une nouvelle application ». Le sens est que nous exécutons le même service, mais en activant une capacité d'infrastructure supplémentaire — le messaging.

Pour que le tableau soit tout à fait clair, il est utile de garder en tête un flux simple :

flowchart TD
    A[La même image Docker] --> B[SPRING_PROFILES_ACTIVE]
    B --> C["application.yml + application-*.yml"]
    C --> D["Création des beans selon @Profile"]
    D --> E[La connexion RabbitMQ et la file d'attente apparaissent uniquement en messaging]

2. Auto-configuration pour RabbitMQ

À ce stade, un débutant a très envie d'écrire : « D'accord, maintenant nous allons créer manuellement le client, le socket, le protocole… » Et là, Spring Boot, comme un bon collègue, pose doucement sa main sur votre épaule et dit : « Il n'y a pas besoin. »

Si le projet inclut spring-boot-starter-amqp, alors Spring Boot active l'auto-configuration pour RabbitMQ. Cela signifie que de nombreux beans de base seront créés automatiquement, si la configuration est suffisante. Les plus importants dont nous avons besoin pour une journée « minimale » sont :

1) ConnectionFactory — la fabrique de connexions à RabbitMQ ; c'est ce qui connaît host/port/username/password.

2) RabbitTemplate — le modèle principal pour envoyer des messages. Il implémente l'interface AmqpTemplate, et donc dans le code il est pratique de dépendre directement de l'interface.

3) RabbitAdmin — un bean administratif qui sait déclarer les files d'attente, les exchanges et les bindings au démarrage de l'application, si vous les avez décrits en tant que beans Spring.

Et voici une conclusion pratique importante : nous n'écrivons pas « client RabbitMQ » à la main. Nous décrivons les propriétés spring.rabbitmq.*, nous décrivons la topologie minimalement — aujourd'hui une seule file d'attente — et Spring Boot avec Spring AMQP font le reste.

Si la dépendance n'est pas encore dans le projet, dans build.gradle.kts cela ressemble à :

dependencies {
    // Spring Boot tirera automatiquement les bibliothèques nécessaires de Spring AMQP et du client RabbitMQ
    implementation("org.springframework.boot:spring-boot-starter-amqp")
}

Oui, c'est littéralement une seule ligne qui ajoute à votre projet tout un pan d'intégration. C'est pourquoi Spring est tant aimé… et parfois un peu craint. Mais nous l'aimerons en connaissance de cause.

3. Propriétés spring.rabbitmq.*

Quand on entend « configure RabbitMQ », les débutants essayent souvent soit de tout configurer, soit au contraire se limitent à une seule ligne host=rabbitmq et s'étonnent ensuite que quelque chose ne fonctionne pas. Nous avons besoin d'une option intermédiaire honnête : des propriétés minimales qui aident l'application à se connecter au broker dans le réseau Compose et qui n'exigent pas de code en dur.

Dans Spring Boot, les propriétés RabbitMQ vivent sous le préfixe spring.rabbitmq. Pour cette leçon, nous nous concentrons sur la connexion :

spring.rabbitmq.host — où se connecter ; dans Compose, c'est le nom de service rabbitmq.

spring.rabbitmq.port — sur quel port se connecter ; pour AMQP, c'est 5672.

spring.rabbitmq.username et spring.rabbitmq.password — les identifiants. Dans les exemples sur Internet, vous verrez souvent guest/guest, mais dans notre baseline Compose, il est plus utile d'avoir immédiatement un utilisateur dédié — par exemple catalog/catalog, que nous avons créé du côté de RabbitMQ.

spring.rabbitmq.virtual-host — on le laisse généralement par défaut /, jusqu'à ce qu'un besoin réel apparaisse.

Une précision importante tirée des « pièges dans lesquels tout le monde tombe » : le port 15672 n'est pas le port pour l'application. C'est le port de l'interface web RabbitMQ Management UI, c'est-à-dire ce que votre navigateur ouvre sur la machine hôte. L'application communique avec le broker via le port AMQP 5672.

Pour éviter la confusion, il est utile de voir cela une fois sous forme de tableau et d'y revenir quand les yeux commencent à « glisser » sur le YAML.

Ce que nous configurons Propriété Spring Variable d'environnement Valeur typique dans notre stack À qui cela sert
Adresse du broker spring.rabbitmq.host SPRING_RABBITMQ_HOST rabbitmq à l'application
Port AMQP spring.rabbitmq.port SPRING_RABBITMQ_PORT 5672 à l'application
Utilisateur spring.rabbitmq.username SPRING_RABBITMQ_USERNAME catalog à l'application
Mot de passe spring.rabbitmq.password SPRING_RABBITMQ_PASSWORD catalog à l'application
Port de l'interface Management UI (pas une propriété Spring pour la connexion) (non nécessaire) 15672 à l'humain dans le navigateur

Notez un détail subtil : RABBITMQ_DEFAULT_USER et RABBITMQ_DEFAULT_PASS sont les variables de configuration du conteneur RabbitMQ lors du premier démarrage. Tandis que SPRING_RABBITMQ_USERNAME/PASSWORD sont ce que notre application utilise pour se connecter. Il est pratique de les lier via .env, mais elles vivent dans des « univers de responsabilité » différents.

4. application-messaging.yml et valeurs par défaut

Les configurations de profil dans Spring Boot sont une manière de garder le projet propre : les réglages de base restent de base, et les extensions d'infrastructure ne sont pas dispersées dans un seul fichier application.yml géant. C'est particulièrement important dans le monde Docker/Compose, car vous lirez constamment la configuration quand quelque chose « ne se connecte pas ».

Dans notre projet, nous attendons le fichier src/main/resources/application-messaging.yml. Et à l'intérieur, nous décrivons uniquement ce qui a du sens précisément pour le mode messaging : la connexion à RabbitMQ.

La version minimale la plus honnête ressemble à ceci :

spring:
  rabbitmq:
    host: ${SPRING_RABBITMQ_HOST:rabbitmq}         # Par défaut dans Compose, on se connecte par nom de service
    port: ${SPRING_RABBITMQ_PORT:5672}             # Port AMQP (ne pas confondre avec le 15672 pour l'interface UI)
    username: ${SPRING_RABBITMQ_USERNAME:catalog}  # Mêmes identifiants que ceux définis pour RabbitMQ dans l'environnement local
    password: ${SPRING_RABBITMQ_PASSWORD:catalog}  # Si nécessaire, peut être redéfini de l'extérieur

Il y a ici une petite mais importante magie, qui en réalité n'est pas de la magie mais une discipline soigneuse : nous utilisons le placeholder ${...:default}. Cela signifie : « si la variable d'environnement est définie, on la prend ; sinon, on utilise la valeur par défaut ». Dans un conteneur, c'est pratique car nous obtenons un comportement prévisible même sans recopiage dans le code.

Le nom de la file d'attente pour cette route, nous ne le mettons pas dans une autre propriété, mais dans une constante de code unique. Ainsi, le sender et le listener se réfèrent au même catalog.events et la configuration ne s'étale pas pour une seule ligne.

5. Bean Queue et déclaration de la file d'attente

Jusqu'à présent, tout concernait « comment se connecter ». Maintenant, il s'agit de « ce que nous voulons exactement voir dans le broker ». Et cet endroit est très en phase avec la logique Compose : l'environnement doit être décrit comme du code, pas comme un ensemble d'actions manuelles « ouvre l'interface, clique ».

Dans RabbitMQ, une file d'attente est un conteneur nommé de messages. Si la file d'attente n'existe pas, l'envoi ou la réception échouent ou se comportent de manière inattendue. À des fins pédagogiques, nous ne voulons pas dépendre du hasard et des clics manuels. Nous faisons donc de la file d'attente une partie explicite de la configuration de l'application.

Dans Spring AMQP, il y a une classe Queue. Et dans le monde Spring Boot, il y a un lien important : si vous avez un bean Queue dans le contexte, et que l'auto-configuration a créé RabbitAdmin, alors au démarrage de l'application, Spring essaiera de déclarer cette file d'attente dans RabbitMQ. Autrement dit, l'application ne se contente pas de « se connecter », elle s'assure aussi que la file d'attente nécessaire existe.

Pour cette route, une seule constante avec le nom de la file d'attente suffit. Il est important que le sender et le listener ne puissent physiquement pas « diverger » vers des littéraux de chaîne différents.

Une mini-classe avec la constante peut ressembler à ceci :

package com.example.catalog.messaging;

/**
 * Constantes avec les noms des files d'attente.
 * Ainsi nous ne dispersons pas les littéraux de chaîne dans le code.
 */
public final class CatalogQueues {

    // Nom de la file d'attente avec laquelle notre service travaille
    public static final String CATALOG_EVENTS = "catalog.events";

    private CatalogQueues() {
        // Interdire la création d'instances de la classe utilitaire
    }
}

Ensuite, nous créons une configuration qui ne vit qu'en mode messaging, et à l'intérieur de celle-ci nous déclarons le bean Queue :

package com.example.catalog.messaging;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("messaging") // Dans les autres modes, cette configuration n'est pas activée
public class MessagingConfig {

    @Bean
    public Queue catalogEventsQueue() {
        // Bean Queue + RabbitAdmin = la file d'attente sera déclarée au démarrage de l'application
        return new Queue(CatalogQueues.CATALOG_EVENTS);
    }
}

Ici, plusieurs significations importantes méritent d'être explicitées, et pas simplement « copier le code ».

Premièrement, @Profile("messaging") sur la configuration est une assurance qu'en modes sans RabbitMQ, nous n'essaierons pas de communiquer avec un broker inexistant. Pour un projet pédagogique, ce n'est pas une « optimisation », c'est un moyen de garder les modes de base fonctionnels.

Deuxièmement, le nom de la file d'attente devient partie du contrat : nous ne comptons pas sur le fait que quelqu'un, quelque part, l'a créée. Nous déclarons que le service a besoin de la file d'attente catalog.events, et il essaie de la créer lui-même.

Troisièmement, cela s'intègre très bien dans l'habitude issue de Compose : « si l'environnement est décrit, il est reproductible ». De la même manière, une file d'attente décrite comme bean Spring devient une partie reproductible de l'environnement.

6. Intégration du messaging dans le code

Lorsqu'une nouvelle dépendance d'infrastructure apparaît, un débutant a deux scénarios extrêmes. Le premier : « Je vais ajouter RabbitMQ directement dans CatalogService et dans le contrôleur, et que ça y reste. » Le second : « Je vais faire dix abstractions, trois façades et une passerelle de messaging d'entreprise. » Les deux scénarios se terminent généralement par le fait qu'au bout d'un jour, vous ne pouvez plus expliquer vous-même ce que vous avez fait.

Dans notre cours, l'objectif est différent : la partie messaging doit être petite, isolée et activable par profil. C'est-à-dire que nous voulons que le domaine principal — catalogue, export — ne commence pas à « connaître » RabbitMQ comme s'il était l'unique façon de vivre.

Une bonne pratique ici est de dépendre dans le domaine non pas directement des classes RabbitMQ, mais d'une petite interface « publication d'événements ». En mode sans messaging, nous aurons une implémentation no-op, et en mode messaging, une implémentation utilisant AmqpTemplate. Cela permet de garder le code propre : le domaine « demande » de publier un signal, et l'infrastructure décide comment.

L'interface peut être aussi simple que possible :

package com.example.catalog.messaging;

/**
 * Publication des événements de domaine du catalogue.
 * Le domaine dépend de l'interface, pas directement de RabbitMQ.
 */
public interface CatalogEventPublisher {

    /**
     * Signal « produit créé ».
     * @param itemId identifiant du produit
     * @param sku    SKU du produit
     */
    void itemCreated(long itemId, String sku);
}

L'implémentation no-op, qui vit dans tous les modes sauf messaging, ressemble à ceci :

package com.example.catalog.messaging;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

@Service
@Profile("!messaging") // Quand messaging est désactivé — on utilise l'implémentation « vide »
public class NoOpCatalogEventPublisher implements CatalogEventPublisher {

    @Override
    public void itemCreated(long itemId, String sku) {
        // messaging désactivé — on n'envoie rien (important : l'application continue de fonctionner)
    }
}

Et l'implémentation « réelle » pour le mode messaging peut être provisoirement une ébauche. Il est important de montrer le principe du wiring : Spring nous donnera lui-même AmqpTemplate, car il est créé par l'auto-configuration de spring-boot-starter-amqp.

package com.example.catalog.messaging;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

@Service
@Profile("messaging") // Quand messaging est activé — on utilise l'implémentation RabbitMQ
public class RabbitCatalogEventPublisher implements CatalogEventPublisher {

    private final AmqpTemplate amqpTemplate;

    public RabbitCatalogEventPublisher(AmqpTemplate amqpTemplate) {
        // AmqpTemplate est créé par l'auto-configuration de Spring Boot (si le starter est présent et les propriétés définies)
        this.amqpTemplate = amqpTemplate;
    }

    @Override
    public void itemCreated(long itemId, String sku) {
        // À ce stade, le wiring est important : lorsque le profil messaging est actif, AmqpTemplate est disponible,
        // et ensuite il ne reste plus qu'à envoyer un message textuel dans CatalogQueues.CATALOG_EVENTS.
    }
}

Notez que nous n'écrivons pas encore comment envoyer le message exactement : nous avons d'abord besoin d'un wiring stable sans code dur et sans if (messagingEnabled) dans tout le projet. Quand le profil messaging est désactivé, nous avons NoOpCatalogEventPublisher et l'application ne plante pas. Quand le profil messaging est activé, l'implémentation Rabbit et la file d'attente décrite dans MessagingConfig apparaissent.

Si vous ressentez un léger soulagement en ce moment, c'est une réaction normale. Vous venez d'éviter l'une des choses les plus désagréables dans les projets pédagogiques : le chaos des branches conditionnelles dispersées dans tout le code.

7. Erreurs typiques lors du travail avec messaging

Erreur n°1 : RabbitMQ activé « en permanence » parce qu'on a oublié @Profile("messaging").
Cela arrive généralement quand vous avez ajouté un bean Queue ou un service d'envoi de messages sans profil. En conséquence, l'application commence à essayer de créer une file d'attente et/ou de se connecter au broker même dans les modes où RabbitMQ n'est pas démarré. Le symptôme ressemble à « pourquoi mon standalone plante soudainement, je n'ai pas touché au code métier ». On y remédie par la discipline : tout ce qui nécessite RabbitMQ vit sous le profil messaging.

Erreur n°2 : confondre le port 5672 et 15672.
15672 est le management UI pour le navigateur, et il n'est pas destiné à la connexion AMQP de l'application. Si vous spécifiez SPRING_RABBITMQ_PORT=15672, vous obtiendrez des erreurs de connexion qui ressemblent à « le broker ne fonctionne pas ». En réalité, le broker peut être parfaitement vivant, vous frappez simplement à la mauvaise porte. Pour l'application, gardez 5672, et laissez 15672 à l'humain et au navigateur.

Erreur n°3 : dans la configuration de l'application, on a écrit localhost parce que « c'était comme ça avec le lancement local ».
Dans Compose, localhost signifie « moi-même », pas le service voisin. Donc spring.rabbitmq.host=localhost depuis le conteneur de l'application signifie « essaye de trouver RabbitMQ à l'intérieur de ce même conteneur ». En général, il n'y est pas. Le bon chemin est le nom de service, c'est-à-dire rabbitmq.

Erreur n°4 : confondre les variables d'environnement du conteneur RabbitMQ et celles de l'application.
RABBITMQ_DEFAULT_USER/PASS configurent le conteneur broker. SPRING_RABBITMQ_USERNAME/PASSWORD configurent l'application Spring Boot. Parfois, un développeur change le mot de passe du broker, mais oublie de changer celui de l'application — et obtient « access refused ». Ce n'est pas un problème de code Java, c'est un problème de configurations non synchronisées. Une bonne habitude est de les lier via .env, mais même sans cela, il est important de distinguer à qui appartient quoi.

Erreur n°5 : la file d'attente « créée quelque part à la main » et on a oublié de la fixer comme bean Queue.
À court terme, cela peut fonctionner : vous avez cliqué dans l'interface, la file d'attente est apparue, l'application a envoyé un message. Mais sur une autre machine, dans un environnement vierge ou après un down -v, tout disparaît, et vous rencontrez soudainement des problèmes mystérieux. Le bean Queue est votre moyen de rendre l'environnement reproductible sans magie. Même si la file d'attente est unique et petite, la décrire comme code est plus utile que de se souvenir « je crois que je l'ai créée dans l'interface hier soir, mais je n'en suis pas sûr ».

1
Mission
Docker for Spring, niveau 20, leçon 2
Bloqué
Bean de file d'attente dans le profil `messaging`
Bean de file d'attente dans le profil `messaging`
1
Mission
Docker for Spring, niveau 20, leçon 2
Bloqué
Envoi de chaîne via `AmqpTemplate`
Envoi de chaîne via `AmqpTemplate`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION