Le framework Spring offre un puissant système de gestion d'événements basé sur le patron de conception observateur. Ce mécanisme permet une communication découplée entre différents composants d'une application grâce à l'interface ApplicationEventPublisher du conteneur IoC.
Scénario métier
Nous allons implémenter un système de gestion de livraison repas en ligne avec les étapes suivantes :
- Soumission d'une commande par le client
- Acceptation de la commande par le restaurant
- Notification du livreur pour récupération
- Le livreur confirme la prise en charge
- Le client réceptionne sa livraison
Structure du projet
L'application est organisée autour de trois couches principales : les événements, les modèles de données et les écouteurs réactifs.
Définition des événements
Classe de base pour les événements de commande
package com.delivery.event;
import org.springframework.context.ApplicationEvent;
public abstract class BaseDeliveryEvent<T> extends ApplicationEvent {
protected BaseDeliveryEvent(T payload) {
super(payload);
}
@SuppressWarnings("unchecked")
@Override
public T getSource() {
return (T) super.getSource();
}
}
Événement de nouvelle commande
package com.delivery.event;
import com.delivery.entity.PurchaseOrder;
import lombok.Getter;
@Getter
public class NewOrderEvent extends BaseDeliveryEvent<PurchaseOrder> {
private final long timestamp;
public NewOrderEvent(PurchaseOrder order) {
super(order);
this.timestamp = System.currentTimeMillis();
}
}
Événement de demande de livraison
package com.delivery.event;
import com.delivery.entity.PurchaseOrder;
import lombok.Getter;
@Getter
public class DeliveryRequestEvent extends BaseDeliveryEvent<PurchaseOrder> {
private final String priority;
public DeliveryRequestEvent(PurchaseOrder order, String priority) {
super(order);
this.priority = priority;
}
}
Événement de livraison terminée
package com.delivery.event;
import com.delivery.entity.Courier;
import lombok.Getter;
@Getter
public class DeliveryCompletedEvent extends BaseDeliveryEvent<Courier> {
public DeliveryCompletedEvent(Courier courier) {
super(courier);
}
}
Entités du domaine
Commande d'achat
package com.delivery.entity;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class PurchaseOrder {
private String referenceCode;
private String clientFullName;
private String contactNumber;
private String deliveryAddress;
private BigDecimal totalAmount;
private String specialInstructions;
}
Livreur
package com.delivery.entity;
import lombok.Data;
@Data
public class Courier {
private PurchaseOrder associatedOrder;
private String courierName;
private int experienceYears;
private String vehicleType;
}
Contrôleur REST pour les commandes
Le contrôleur déclenche la création de commande et publie l'événement correspondant :
package com.delivery.web;
import com.delivery.event.NewOrderEvent;
import com.delivery.entity.PurchaseOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@RestController
@Slf4j
@RequestMapping("/api/v1/purchases")
public class PurchaseOrderController {
private final ApplicationEventPublisher publisher;
public PurchaseOrderController(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@PostMapping
public String submitOrder() {
PurchaseOrder order = buildSampleOrder();
publisher.publishEvent(new NewOrderEvent(order));
log.info("Événement de création de commande diffusé avec succès");
return "Commande enregistrée";
}
private PurchaseOrder buildSampleOrder() {
PurchaseOrder order = new PurchaseOrder();
order.setReferenceCode("CMD-20240315-001");
order.setClientFullName("Marie Dupont");
order.setContactNumber("+33612345678");
order.setDeliveryAddress("15 Rue de la Paix, 75002 Paris");
order.setTotalAmount(new BigDecimal("24.50"));
order.setSpecialInstructions("Sonner à l'interphone B3");
return order;
}
}
Écouteurs d'événements
Écouteur du restaurant
Cet écouteur traite la commande et déclenche une demande de livraison via ApplicationEventPublisher :
package com.delivery.listener;
import com.delivery.event.DeliveryRequestEvent;
import com.delivery.event.NewOrderEvent;
import com.delivery.entity.PurchaseOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class RestaurantOrderHandler {
@EventListener
@Async
public void processNewOrder(NewOrderEvent event) throws InterruptedException {
PurchaseOrder order = event.getSource();
log.info("Restaurant a reçu la commande: {}", order.getReferenceCode());
log.info("Préparation en cours pour: {}", order.getClientFullName());
// Simulation du temps de préparation
TimeUnit.SECONDS.sleep(8);
// Publication de la demande de livraison avec priorité haute
log.info("Commande prête, envoi de la demande de livraison");
}
}
Écouteur du service de livraison
package com.delivery.listener;
import com.delivery.event.DeliveryRequestEvent;
import com.delivery.event.DeliveryCompletedEvent;
import com.delivery.entity.Courier;
import com.delivery.entity.PurchaseOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class DeliveryServiceHandler {
private final ApplicationEventPublisher eventPublisher;
public DeliveryServiceHandler(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@EventListener
@Async
public void handleDeliveryRequest(DeliveryRequestEvent event) throws InterruptedException {
PurchaseOrder order = event.getSource();
log.info("Livreur assigné pour la commande: {}", order.getReferenceCode());
log.info("Adresse de livraison: {}", order.getDeliveryAddress());
TimeUnit.SECONDS.sleep(6);
Courier courier = new Courier();
courier.setAssociatedOrder(order);
courier.setCourierName("Jean Martin");
courier.setExperienceYears(3);
courier.setVehicleType("Vélo électrique");
eventPublisher.publishEvent(new DeliveryCompletedEvent(courier));
log.info("Événement de livraison terminée publié");
}
}
Écouteur côté client
package com.delivery.listener;
import com.delivery.event.DeliveryCompletedEvent;
import com.delivery.entity.Courier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class ClientNotificationHandler {
@EventListener
@Async
public void onDeliveryCompleted(DeliveryCompletedEvent event) throws InterruptedException {
Courier courier = event.getSource();
log.info("Livraison reçue! Commande: {}", courier.getAssociatedOrder().getReferenceCode());
log.info("Livré par: {} ({} ans d'expérience)",
courier.getCourierName(), courier.getExperienceYears());
log.info("Véhicule utilisé: {}", courier.getVehicleType());
TimeUnit.SECONDS.sleep(2);
log.info("Réception confirmée par le client");
}
}
Comportement synchrone vs asynchrone
Par défaut, le mécanisme publishEvent fonctionne de manière synchrone. L'appelant reste bloqué jusqu'à ce que tous les écouteurs aient terminé leur traitement.
Pour activer le traitement asynchrone, deux conditions sont nécessaires :
- Ajouter l'annotation
@EnableAsyncsur la classe de configuration principale - Combiner
@EventListeneravec@Asyncsur chaque méthode d'écoute
package com.delivery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class DeliveryApplication {
public static void main(String[] args) {
SpringApplication.run(DeliveryApplication.class, args);
}
}
Exemple de traces d'exécution asynchrone
14:22:31.105 [http-nio-8080-exec-1] INFO PurchaseOrderController - Événement de création de commande diffusé avec succès
14:22:31.107 [AsyncExecutor-1] INFO RestaurantOrderHandler - Restaurant a reçu la commande: CMD-20240315-001
14:22:39.112 [AsyncExecutor-1] INFO DeliveryServiceHandler - Livreur assigné pour la commande: CMD-20240315-001
14:22:45.118 [AsyncExecutor-1] INFO DeliveryServiceHandler - Événement de livraison terminée publié
14:22:45.120 [AsyncExecutor-2] INFO ClientNotificationHandler - Livraison reçue! Commande: CMD-20240315-001
14:22:47.125 [AsyncExecutor-2] INFO ClientNotificationHandler - Réception confirmée par le client
Contrôle de l'ordre d'exécution
Lorsque plusieurs écouteurs taritent le même événement, il est possible d'ordonner leur exécution grâce à l'annotation @Order :
@Component
@Order(1)
public class AuditLogListener {
@EventListener
public void logOrderCreation(NewOrderEvent event) {
// Enregistrement dans le journal d'audit en premier
}
}
@Component
@Order(2)
public class NotificationListener {
@EventListener
public void sendConfirmation(NewOrderEvent event) {
// Envoi de notification après l'audit
}
}
Résumé des points essentiels
ApplicationContext.publishEvent()est synchrone par défaut- L'ajout de
@Asyncsur les écouteurs permet un traitement non bloquant dans des threads séparés - L'annotation
@EnableAsyncdoit être activée au niveau de l'application - L'annotation
@Orderpermet de maîtriser la séquence d'exécution des écouteurs - Il est recommandé de maintenir un état de commande (
OrderStatus) pour tracer chaque transition