Gestion des événements génériques avec Spring ApplicationEvent et ResolvableTypeProvider

L'utilisation de ApplicationEventPublisher et de l'annotation @EventListener est une pratique courante dans l'écosystème Spring pour découpler les modules d'une application. Cependant, une limitation technique survient rapidement lors de l'utilisation de types génériques pour les événements : les écouteurs spécifiques ne parviennent pas à intercepter les messages.

Le problème de la redondance des classes d'événements

Imaginons un système de synchronisation de données où chaque entité déclenche un événement après une opération de persistance. Sans généricité, nous sommes contraints de créer une classe par type d'entité :

// Classes d'événements spécifiques
class ClientCreatedEvent {
    private Client client;
    private ActionType action;
}

class ProductCreatedEvent {
    private Product product;
    private ActionType action;
}

// Publication
publisher.publishEvent(new ClientCreatedEvent(client, ActionType.CREATE));
publisher.publishEvent(new ProductCreatedEvent(product, ActionType.CREATE));

// Écouteurs dédiés
@EventListener
public void handleClient(ClientCreatedEvent event) {
    log.info("Client reçu : {}", event.getClient());
}

@EventListener
public void handleProduct(ProductCreatedEvent event) {
    log.info("Produit reçu : {}", event.getProduct());
}

Cette approche génère un code répétitif. La solution naturelle semble être l'utilisation d'une classe générique unique.

Échec de l'approche générique standard

Si nous définissons une classe GenericWrapper<T>, nous constatons que les écouteurs typés ne sont jamais déclenchés :

public class GenericWrapper<T> {
    private T content;
    private ActionType action;
}

// Tentative d'écoute spécifique
@EventListener
public void onClientEvent(GenericWrapper<Client> event) {
    // Ce code ne sera jamais exécuté pour un GenericWrapper<Client>
}

Ce dysfonctionnement est dû à la manière dont Spring résout les types d'événemants au moment de l'exécution. Par défaut, Spring ne parvient pas à extraire les informations de type générique du payload, traitant ainsi tous les événements comme des GenericWrapper<?>.

Implémentation de ResolvableTypeProvider

Pour pallier cette limitation, Spring propose l'interface ResolvableTypeProvider. Elle permet à l'objet événement de fournir explicitement des informations sur son type, guidant ainsi le framework lors du dispatching.

import org.springframework.core.ResolvableType;
import org.springframework.core.ResolvableTypeProvider;

public class AppNotification<T> implements ResolvableTypeProvider {
    private final T payload;

    public AppNotification(T payload) {
        this.payload = payload;
    }

    public T getPayload() {
        return payload;
    }

    @Override
    public ResolvableType getResolvableType() {
        // Construction du type complet incluant le paramètre générique
        return ResolvableType.forClassWithGenerics(
            getClass(), 
            ResolvableType.forClass(payload.getClass())
        );
    }
}

Avec cette implémentation, Spring est désormais capable de faire la distinction entre AppNotification<Client> et AppNotification<Product>, permettant aux écouteurs de fonctionner comme prévu.

Analyse du mécanisme interne

Le processus de distribution des événements dans Spring repose sur la méthode publishEvent de la classe AbstractApplicationContext. Voici un aperçu simplifié de la logique de résolution :

// Extrait conceptuel de AbstractApplicationContext
protected void publishEvent(Object event, ResolvableType eventType) {
    ApplicationEvent appEvent;
    if (event instanceof ApplicationEvent) {
        appEvent = (ApplicationEvent) event;
    } else {
        // Emballage des objets simples dans un PayloadApplicationEvent
        appEvent = new PayloadApplicationEvent<>(this, event);
        if (eventType == null) {
            eventType = ((PayloadApplicationEvent<?>) appEvent).getResolvableType();
        }
    }
    
    // Multidiffusion vers les écouteurs
    getApplicationEventMulticaster().multicastEvent(appEvent, eventType);
}

La méthode cruciale est ResolvableType.forInstance(Object instance). Son rôle est de déterminer la signature de type de l'objet fourni :

public static ResolvableType forInstance(Object instance) {
    if (instance instanceof ResolvableTypeProvider) {
        // Priorité à l'implémentation personnalisée
        ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType();
        if (type != null) {
            return type;
        }
    }
    // Comportement par défaut : récupération de la classe simple
    return ResolvableType.forClass(instance.getClass());
}

En l'absence de l'interface ResolvableTypeProvider, Spring se contente de instance.getClass(), ce qui efface les informations sur les types génériques (Type Erasure). En implémentant l'interface, nous forçons le framework à utiliser une définition de type enrichie, garantissant que le multicastEvent trouve les abonnés correspondant exactement à la signature générique de l'événement.

Étiquettes: spring-framework Java-Generics ApplicationEvent EventListener ResolvableType

Publié le 9 juillet à 01h42