Contactez-nous

Utilisation des événements Spring (`ApplicationEvent`, `@EventListener`)

Maîtrisez le système d'événements intégré de Spring pour créer des applications découplées et réactives en utilisant ApplicationEvent et l'annotation @EventListener.

Introduction : Le pattern Observer et le découplage

Dans de nombreuses applications, certains composants ont besoin de réagir lorsque quelque chose se produit dans un autre composant. Une approche naïve consisterait à injecter directement le composant "réactif" dans le composant "acteur" et à appeler une de ses méthodes. Cependant, cela crée un couplage fort entre les composants, rendant le code plus difficile à maintenir, à tester et à faire évoluer.

Le pattern de conception Observer (ou Publish-Subscribe) offre une solution élégante à ce problème. Un objet (le sujet ou publicateur) maintient une liste de ses dépendants (les observateurs ou auditeurs) et les notifie automatiquement de tout changement d'état, généralement en appelant une de leurs méthodes. Les observateurs ne connaissent pas les autres observateurs, et le sujet ne connaît les observateurs que via une interface abstraite.

Spring Framework intègre ce pattern de manière native grâce à son système d'événements applicatifs. Il permet aux beans Spring de publier des événements et à d'autres beans de s'abonner (écouter) ces événements de manière découplée. Le composant qui publie l'événement n'a pas besoin de savoir qui va l'écouter, et vice-versa.

L'événement : `ApplicationEvent`

Au coeur du système se trouve la classe abstraite `ApplicationEvent`. Tous les événements personnalisés que vous souhaitez publier doivent hériter de cette classe. Un `ApplicationEvent` représente quelque chose qui s'est produit dans l'application et qui pourrait intéresser d'autres composants.

Il est de bonne pratique de créer des classes d'événements spécifiques et immuables pour représenter des événements métier distincts. Ces classes contiennent généralement les données pertinentes relatives à l'événement, passées via le constructeur.

Exemple : un événement signalant la création d'une commande.

import org.springframework.context.ApplicationEvent;

// Classe immuable représentant l'événement
public class OrderCreatedEvent extends ApplicationEvent {

    private final Order orderData; // Données associées à l'événement

    /**
     * Constructeur de l'événement.
     * @param source L'objet sur lequel l'événement s'est produit initialement (ou le publicateur).
     * @param orderData Les données de la commande créée.
     */
    public OrderCreatedEvent(Object source, Order orderData) {
        super(source); // Le constructeur parent stocke la source et le timestamp
        this.orderData = orderData;
    }

    // Getter pour accéder aux données de l'événement
    public Order getOrderData() {
        return orderData;
    }
    
    // Classe Order (exemple)
    public record Order(String orderId, String customerId, double amount) {}
}

La classe `ApplicationEvent` stocke automatiquement l'objet `source` qui a publié l'événement et le `timestamp` de sa création.

Le publicateur : `ApplicationEventPublisher`

Pour publier un événement, un bean Spring a besoin d'une référence à l'interface `ApplicationEventPublisher`. Spring injecte automatiquement une instance de cette interface si vous la déclarez comme dépendance (via `@Autowired` ou, mieux, via l'injection par constructeur).

L'interface `ApplicationEventPublisher` possède une méthode principale : `publishEvent(Object event)`. Vous appelez simplement cette méthode en lui passant une instance de votre événement personnalisé.

Exemple : un service qui publie l'événement `OrderCreatedEvent` après avoir créé une commande.

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;

    // Injection par constructeur (recommandé)
    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public OrderCreatedEvent.Order createOrder(String customerId, double amount) {
        // 1. Logique de création de la commande...
        String orderId = "ORD-" + System.currentTimeMillis(); 
        OrderCreatedEvent.Order newOrder = new OrderCreatedEvent.Order(orderId, customerId, amount);
        System.out.println("Order created: " + orderId);

        // 2. Publier l'événement
        OrderCreatedEvent event = new OrderCreatedEvent(this, newOrder); // 'this' est la source
        eventPublisher.publishEvent(event);
        System.out.println("OrderCreatedEvent published.");

        return newOrder;
    }
}

Lorsque `publishEvent` est appelée, Spring recherche tous les listeners intéressés par cet événement spécifique (ou un de ses super-types) et les invoque.

L'auditeur : `@EventListener`

La manière moderne et la plus simple d'écouter des événements est d'utiliser l'annotation `@EventListener`. Vous pouvez placer cette annotation sur n'importe quelle méthode d'un bean Spring géré (`@Component`, `@Service`, etc.).

Spring détecte automatiquement ces méthodes et les enregistre comme des listeners. Le type du premier (et souvent unique) argument de la méthode détermine le type d'événement que la méthode écoutera. Lorsque un événement correspondant est publié, Spring appelle la méthode en lui passant l'objet événement.

Exemple : un composant qui envoie une notification par email lorsqu'une commande est créée.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class NotificationService {

    @EventListener
    public void handleOrderCreatedEvent(OrderCreatedEvent event) {
        // Réagit spécifiquement à OrderCreatedEvent
        OrderCreatedEvent.Order order = event.getOrderData();
        System.out.println("-------------------------------------------");
        System.out.println("NotificationService: Received OrderCreatedEvent!");
        System.out.println("Sending notification for order: " + order.orderId() + 
                           " to customer: " + order.customerId());
        // Logique d'envoi de l'email...
        System.out.println("-------------------------------------------");
    }

    // On peut avoir plusieurs listeners dans la même classe
    @EventListener
    public void handleAnyApplicationEvent(ApplicationEvent event) {
        // Ecoute tous les événements Spring (y compris ceux intégrés)
        // Moins courant, sauf pour le logging générique ou l'audit
        // System.out.println("Generic Listener: Received event - " + event.getClass().getSimpleName());
    }
}

Notez que `NotificationService` n'a aucune dépendance directe vers `OrderService`. Ils sont découplés via le système d'événements.

Fonctionnalités avancées des listeners

Les listeners `@EventListener` offrent plusieurs fonctionnalités :

  • Exécution synchrone (par défaut) : Par défaut, le listener est exécuté de manière synchrone dans le thread du publicateur. Si le listener prend du temps ou échoue, cela impacte le publicateur. L'exécution se fait également au sein de la transaction du publicateur (si une transaction est active).
  • Exécution asynchrone (`@Async`) : Si vous annotez la méthode `@EventListener` avec `@Async` (et que vous avez activé le support asynchrone avec `@EnableAsync` dans votre configuration), le listener sera exécuté dans un pool de threads séparé. Le publicateur n'attendra pas la fin de l'exécution du listener. C'est utile pour les tâches longues ou indépendantes qui ne doivent pas bloquer le flux principal.
@Async
@EventListener
public void handleOrderCreatedEventAsync(OrderCreatedEvent event) {
    System.out.println("Running listener asynchronously in thread: " + Thread.currentThread().getName());
    // Longue tâche...
}
  • Ecoute conditionnelle (`condition`) : Vous pouvez spécifier une expression SpEL dans l'attribut `condition` de `@EventListener`. La méthode ne sera appelée que si l'expression retourne `true` lorsqu'évaluée par rapport à l'objet événement.
@EventListener(condition = "#event.orderData.amount > 1000")
public void handleHighValueOrder(OrderCreatedEvent event) {
    // Ne s'exécute que si le montant de la commande > 1000
    System.out.println("Processing high value order: " + event.getOrderData().orderId());
}
  • Ordre d'exécution (`@Order`) : Si plusieurs listeners écoutent le même événement, vous pouvez utiliser l'annotation `@Order` sur les méthodes pour influencer leur ordre d'exécution (valeurs inférieures exécutées en premier).

Alternative : L'interface `ApplicationListener`

Avant l'introduction de `@EventListener`, la manière standard d'écouter des événements était d'implémenter l'interface `ApplicationListener`. Un bean qui implémente cette interface est automatiquement enregistré comme listener pour le type d'événement spécifié par le paramètre générique `E`.

import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class AuditService implements ApplicationListener {

    @Override
    public void onApplicationEvent(OrderCreatedEvent event) {
        // Logique d'audit ici
        System.out.println("AuditService: Logging order creation - " + event.getOrderData().orderId());
    }
}

Bien que fonctionnelle, cette approche est moins flexible que `@EventListener` (une classe par type d'événement, pas de conditionnalité facile). `@EventListener` est généralement préféré pour les nouveaux développements.

Conclusion : Un outil puissant pour le découplage

Le système d'événements applicatifs de Spring est un mécanisme puissant et élégant pour implémenter le pattern Observer et découpler les composants de votre application. En définissant des événements métier clairs (`ApplicationEvent`), en les publiant via `ApplicationEventPublisher`, et en y réagissant de manière ciblée avec `@EventListener`, vous pouvez construire des architectures plus modulaires, plus faciles à tester et à faire évoluer.

Que ce soit pour déclencher des effets de bord (notifications, audit), pour communiquer entre différents modules sans dépendances directes, ou pour réagir à des changements d'état internes, le système d'événements Spring est un outil essentiel dans la boîte à outils du développeur Spring Boot.