jeudi 13 novembre 2025

Refonte ou refactoring

Refonte vs Refactoring : Quand réécrire, quand faire du refactoring ?

Face à un code legacy, la tentation de tout réécrire est forte. Pourtant, le refactoring et la refonte sont deux stratégies radicalement différentes, avec des impacts opposés sur le risque, le temps et la valeur métier.

Point à prendre en considération

Langage, Framework, Librairie: est-ce qu'ils sont désuet, est-t'il possible de trouver des ressources

Usage interne / externe: il peut avoir plus d'exigence si l'application est utilisé par des clients 

Utilisation: est-ce que l'application est très utilisé

Maintenance: il y a beaucoup de maintenance à faire, temps pour maîtriser le système

Documentation: technique, fonctionnel


Refactoring : Changer sans changer

Le refactoring, est une modification du code interne sans changer son comportement externe. C’est l’art de rendre le code lisible, modulaire, testable sans toucher à la logique métier.

Quand l’utiliser ?

Le code fonctionne, mais est illisible, répétitif ou mal structuré.

Les tests unitaires existent ou peuvent être ajoutés

Le framework ou la technologie sous-jacente est encore pertinent

L’équipe connaît suffisamment le domaine pour identifier les points de fragilité.

Trouver des ressources pour travailler sur le système n'est pas un enjeux

Avantage

Moins de risque, pas de perte de fonctionnalité

Amélioration ciblé, pas à pas

Désavantage

Il peut devenir un gouffre temporel. Pourquoi ? Parce que comprendre un code mal documenté, avec des dépendances cachées et des comportements implicites, prend souvent plus de temps que de le réécrire.

C’est ce que j’ai vécu sur plusieurs projet legacy. Le code ne respectait pas les spécifications et le code avait très peu été testé.

Refonte: Repartir de zéro

Une refonte consiste à reconstruire entièrement un système, souvent avec une nouvelle architecture, un nouveau langage, une nouvelle base de données, ou un nouveau framework (ex. : passer d’un monolithe C++ à un service Spring Boot.

Quand l’utiliser ?

Le code est si complexe qu’il devient une « boîte noire » : personne ne comprend plus ce qu’il fait.
Le framework, librairie est obsolète, par exemple JBoss Seam.
Les besoins métier ont évolué au point que le code existant ne peut plus évoluer sans cassures.
Il n’y a aucune documentation, ni tests automatisés  et personne ne veut plus y toucher.

Avantage 

Mise en place de bonne pratique plus aisément

Opportunité de revoir des règles métier

Désavantage

La perte de connaissance tacite : si personne n’a documenté le comportement réel du système
Le temps de développement est long


Réalité

Dans la réalité, le choix n'est pas toujours évident. Il y a un aspect technologique, ressource, économique et temporel à prendre en considération. J'ai été confronté à mainte reprises aux deux situations avec des cas hybrides.

Cas 1 

Refonte d'un système qui utilisait JEE JBoss avec Seam. Est-ce que ce système avec réellement besoin de JEE? Non, long et lourd à déployer. Seam n'est plus supporté par Red Hat depuis longtemps. Le gros avantage que le système avait c'est la documentation. Un point qui a facilité la refonte. 

Spring Boot a été sélectionné pour la refonte. Passage de Java 1.6 à 21. La qualité du code a été grandement améliorer, sans compté que beaucoup de code a pu être supprimer grace à Spring Data, Lombok et Mapstruct. Il y a eu quand même de nombreuse portion de l'ancien système qui a pu être réutilisé directement et d'autre qui a été améliorer. La documentation technique a été accrue.

Fait à noter, la refonte a pris plus de temps que lorsque le système avait été créer la première fois. Afin de garder les mêmes comportement à fonctionalité égale, il fallait s'assurer que l'existant faisait bien ce qu'il était supposé faire. Des cas très poussé on pu être testé plus en profondeur et permis de décelé des anomalies qui existait depuis des années. Elles ont été fixé dans le nouveau système.

Cas 2

Refonte d'un système qui utilisait Ruby. Il était difficile de trouver des ressources pour ce langage. Des soucis de performance. Pas de documentation fonctionnel.

Spring Boot a été sélectionné pour la réécrire du système, ce qui a permis de trouver des ressources aisément sur le marché. Le modèle de donnée a été complètement revue afin d'être en mesure de supporter de nouvelle exigences fonctionnels.

Le système final comportait plus de 400 000 ligne de code. Certaine partie qui utilisait perl, bash ont été aussi convertie en Java quand il y avait possibilité

Cas 3

Refonte complète de système de recherche d'entrant (courriel, facture, document) dans une entreprise. Changement total du langage de programmation, base de donnée et même architecture cpu qui allait faire tourner l'application. Le cas qui arrive pratiquement jamais dans une vie.

Même le mix de spring / struts avec du GWT était très audacieux. Il y avait aucune compatibilité qui devait être assuré avec l'ancien système, ce qui a réduire considérablement la complexité du projet.

Cas 4

Dans un autre système d'appel d'offre, c'est l'approche refactoring qui a été pris en considération. Le système utilisait vbscript. Encore une fois peu de documentation, mais constamment des modifications devaient être apporté au système. L'entreprise ne voulait pas investir dans le système. Peu de personne voulait toucher au code. Il a été décidé d'investir un peu de temps lors de correction d'anomalie et ajout de fonctionnalité pour améliorer le système.

Conclusion

Le choix n'est pas technique, il est stratégique. Il doit se baser sur une analyse des coûts, des risques et des bénéfices métier.

Refactoring : Pour améliorer ce qui marche.

Refonte : Pour reconstruire ce qui ne marche plus.


Le conseil ultime : Avant de vous lancer, documentez ce que vous avez compris. Parfois, le meilleur refactoring est de s'arrêter pour planifier une refonte ciblée. Surtout, méfiez-vous du piège classique : confondre la difficulté de comprendre un système legacy avec la facilité supposée de le réécrire.

lundi 10 novembre 2025

Gérer le cycle de vie d’un plugin avec Spring Statemachine

Les machines à états sont un sujet très peu discuté, peut-être que le côté mathématique fait peur à certain. Pour faire suite à notre article sur les plugin, nous allons mettre en place une machine à état pour suivre son cycle de vie. 

Le projet State Machine de Spring sera utilisé pour la suite de cet article.

Un plugin peut avoir différent état 

  • INSTALLED, 
  • ACTIVE, 
  • INACTIVE, 
  • UPDATING, 
  • FAILED,
  • UNINSTALLED

Dans un système de gestion de plugin, différente action devrait être mis en place :

  • Téléchargement du JAR
  • Vérification des dépendances
  • Initialisation du contexte Spring
  • Enregistrement des routes ou services
  • Nettoyage des ressources

Chaque transition doit être explicitement autorisée. 

  • On ne peut pas désactiver un plugin qui n’est pas activé.
  • On ne peut pas réactiver un plugin désinstallé.
  • Une erreur pendant l’activation doit mener à l’état FAILED.

Gérer cela avec des if/else mène à un code difficile à maintenir. Une machine à états permet de modéliser ce cycle clairement.

État et évènement

Liste des états


public enum PluginState {
    INSTALLED,
    ACTIVE,
    INACTIVE,
    UPDATING,
    FAILED,
    UNINSTALLED
}

Listes des évènements


public enum PluginEvent {
    INSTALL,
    ACTIVATE,
    DEACTIVATE,
    UPDATE,
    UNINSTALL,
    ERROR
}

Le plugin

Cette classe a été allégé

public class Plugin {

    private String id;
    private PluginState state;

    public Plugin(String id) {
        this.id = id;
        this.state = PluginState.INSTALLED; // Après installation
    }

    public String getId() { return id; }
    public PluginState getState() { return state; }
    public void setState(PluginState state) { this.state = state; }
}
Dans la réalité, il y aurait fort probablement son nom, url, version.

Action du plugin

Activation du plugin

@Component
public class ActivatePluginAction implements Action<PluginState, PluginEvent> {
    private static final Logger log = LoggerFactory.getLogger(ActivatePluginAction.class);
    @Override
    public void execute(StateContext<PluginState, PluginEvent> context) {
        Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
        log.info("Activation du plugin : " + plugin.getId());
    }
}

Désactivation du plugin


@Component
public class DeactivatePluginAction implements Action<PluginState, PluginEvent> {
    private static final Logger log = LoggerFactory.getLogger(DeactivatePluginAction.class);
    @Override
    public void execute(StateContext<PluginState, PluginEvent> context) {
        Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
        log.info("Plugin désactivé : " + plugin.getId());
    }
}

Erreur du plugin


@Component
public class FailPluginAction implements Action<PluginState, PluginEvent> {
    private static final Logger log = LoggerFactory.getLogger(FailPluginAction.class);
    @Override
    public void execute(StateContext<PluginState, PluginEvent> context) {
        Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
        log.warn("Plugin en échec : " + plugin.getId());
        plugin.setState(PluginState.FAILED);
    }
}

Configuration de la machine à états

La classe PluginStateMachineConfig est le coeur de la machine à états.
Elle configure
  • Les états possibles du plugin
  • Les transitions autorisées entre ces états
  • Les événements qui déclenchent ces transitions
  • Les actions à exécuter lors de certaines transitions

@Configuration
@EnableStateMachineFactory
public class PluginStateMachineConfig extends StateMachineConfigurerAdapter<PluginState, PluginEvent> {

    private final ActivatePluginAction activateAction;
    private final DeactivatePluginAction deactivateAction;
    private final FailPluginAction failAction;

    public PluginStateMachineConfig(ActivatePluginAction activateAction,
                                    DeactivatePluginAction deactivateAction,
                                    FailPluginAction failAction) {
        this.activateAction=activateAction;
        this.deactivateAction=deactivateAction;
        this.failAction=failAction;
    }

    @Override
    public void configure(StateMachineStateConfigurer<PluginState, PluginEvent> states) throws Exception {
        states
                .withStates()
                .initial(PluginState.INSTALLED)
                .states(EnumSet.allOf(PluginState.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<PluginState, PluginEvent> transitions) throws Exception {
        transitions
                .withExternal()
                .source(PluginState.INSTALLED)
                .target(PluginState.ACTIVE)
                .event(PluginEvent.ACTIVATE)
                .action(activateAction)
                .and()
                .withExternal()
                .source(PluginState.ACTIVE)
                .target(PluginState.INACTIVE)
                .event(PluginEvent.DEACTIVATE)
                .action(deactivateAction)
                .and()
                .withExternal()
                .source(PluginState.INACTIVE)
                .target(PluginState.ACTIVE)
                .event(PluginEvent.ACTIVATE)
                .action(activateAction)
                .and()
                .withExternal()
                .source(PluginState.ACTIVE)
                .target(PluginState.UPDATING)
                .event(PluginEvent.UPDATE)
                .and()
                .withExternal()
                .source(PluginState.UPDATING)
                .target(PluginState.ACTIVE)
                .event(PluginEvent.ACTIVATE)
                .action(activateAction)
                .and()
                .withExternal()
                .source(PluginState.INSTALLED)
                .target(PluginState.UNINSTALLED)
                .event(PluginEvent.UNINSTALL)
                .and()
                .withExternal()
                .source(PluginState.ACTIVE)
                .target(PluginState.FAILED)
                .event(PluginEvent.ERROR)
                .action(failAction)
                .and()
                .withExternal()
                .source(PluginState.INACTIVE)
                .target(PluginState.FAILED)
                .event(PluginEvent.ERROR)
                .action(failAction)
                .and()
                .withExternal()
                .source(PluginState.FAILED)
                .target(PluginState.UNINSTALLED)
                .event(PluginEvent.UNINSTALL);
    }
}

Diagramme du workflow

Ce diagramme affiche les différents états et les transitions possible.



Cycle de vie du plugin

Cette classe crée, stocke et orchestre les transitions d'états des plugins via Spring StateMachine. Une map sert de stockage temporaire des plugins.

Lors de l'activation d'un plugin, on 

  • Récupère le plugin via son id.
  • Crée une instance de machine à états via la factory.
  • Construit un message contenant l’événement ACTIVATE, avec le plugin comme contexte.
  • Envoie l’événement à la machine.
  • Si la transition échoue, une exception est levée (ex. : INSTALLED → DEACTIVATE n’est pas autorisé).
  • Met à jour l’état du plugin avec le nouvel état de la machine.
  • La machine à états garantit que seul ce qui est autorisé se produit.

@Service
public class PluginService {

    private final Map<String, Plugin> plugins = new ConcurrentHashMap<>();

    @Autowired
    private StateMachineFactory<PluginState, PluginEvent> stateMachineFactory;

    public Plugin createPlugin(String id) {
        Plugin plugin = new Plugin(id);
        plugins.put(id, plugin);
        return plugin;
    }

    public Plugin getPlugin(String id) {
        return plugins.get(id);
    }

    public void activate(String id) {
        Plugin plugin = getPlugin(id);
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin non trouvé : " + id);
        }

        StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();

        Message<PluginEvent> message = MessageBuilder
                .withPayload(PluginEvent.ACTIVATE)
                .setHeader("plugin", plugin)
                .build();

        stateMachine.sendEvent(message);
        plugin.setState(stateMachine.getState().getId());
    }

    public void deactivate(String id) {
        Plugin plugin = getPlugin(id);
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin non trouvé : " + id);
        }

        StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();

        Message<PluginEvent> message = MessageBuilder
                .withPayload(PluginEvent.DEACTIVATE)
                .setHeader("plugin", plugin)
                .build();

        stateMachine.sendEvent(message);
        plugin.setState(stateMachine.getState().getId());
    }

    public void uninstall(String id) {
        Plugin plugin = getPlugin(id);
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin non trouvé : " + id);
        }

        StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();

        Message<PluginEvent> message = MessageBuilder
                .withPayload(PluginEvent.UNINSTALL)
                .setHeader("plugin", plugin)
                .build();

        stateMachine.sendEvent(message);
        plugin.setState(stateMachine.getState().getId());
        plugins.remove(id); // Supprimer du stockage
    }

}

La machine à états valide les transitions. Si un plugin est dans l’état INSTALLED, une tentative de DEACTIVATE échouera.

Il est facile d'ajouter des états / transitions

Test

Un controlleur a été mis en place pour pouvoir tester plus aisément les états.


@RestController
@RequestMapping("/plugins")
public class PluginController {

    private final PluginService pluginService;

    public PluginController(PluginService pluginService) {
        this.pluginService=pluginService;
    }

    @PostMapping("/create/{id}")
    public String create(@PathVariable String id) {
        Plugin plugin = pluginService.createPlugin(id);
        return "Plugin créé : " + id + ", état initial : " + plugin.getState();
    }

    @PostMapping("/{id}/activate")
    public String activate(@PathVariable String id) {
        pluginService.activate(id);
        Plugin plugin = pluginService.getPlugin(id);
        return "Plugin activé : " + id + ", état : " + plugin.getState();
    }

    @PostMapping("/{id}/deactivate")
    public String deactivate(@PathVariable String id) {
        pluginService.deactivate(id);
        Plugin plugin = pluginService.getPlugin(id);
        return "Plugin désactivé : " + id + ", état : " + plugin.getState();
    }

    @PostMapping("/{id}/uninstall")
    public String uninstall(@PathVariable String id) {
        pluginService.uninstall(id);
        return "Plugin désinstallé : " + id;
    }

    @GetMapping("/{id}")
    public String getStatus(@PathVariable String id) {
        Plugin plugin = pluginService.getPlugin(id);
        if (plugin == null) {
            return "Plugin non trouvé";
        }
        return "Plugin " + id + ", état : " + plugin.getState();
    }
}

Par la suite, vous pouvez utiliser ce controller pour tester différents scénarios

Créer un plugin

curl -X POST http://localhost:8080/plugins/create/demo

Tenter de le désactiver (état INSTALLED)

curl -X POST http://localhost:8080/plugins/demo/deactivate

Activer le plugin

curl -X POST http://localhost:8080/plugins/demo/activate

Désactiver 

curl -X POST http://localhost:8080/plugins/demo/deactivate

Tenter de réactiver un plugin désinstallé

curl -X POST http://localhost:8080/plugins/demo/uninstall
curl -X POST http://localhost:8080/plugins/demo/activate

Conclusion

PluginStateMachineConfig est le plan de votre machine à états.
Il définit les règles du jeu : ce qui est autorisé, ce qui n’est pas, et ce qui se passe à chaque étape.
C’est une alternative claire et robuste aux if/else imbriqués, surtout dans des systèmes modulaires comme des plugins ou des workflows métier. 

Les sources sont disponible sur github

jeudi 6 novembre 2025

Construire un système de plugins en Java

Les CMS comme WordPress, Joomla ou Shopify ont popularisé une idée puissante : permettre à n’importe qui d’étendre une application sans toucher à son code source. Même si cela existait bien avant ces applications. Cette extensibilité repose sur un système de plugins (ou extensions), qui transforme une application figée en une plateforme vivante.

Mais est-il possible de reproduire ce modèle en Java, sans framework web, sans Spring, et sans magie ?
Oui ! Et c’est plus simple qu’on ne le pense.

Dans cet article, nous allons construire, pas à pas, un système de plugins modulaire en Java pur, inspiré des meilleures pratiques des CMS les plus utilisés au monde. Le résultat sera une application capable de charger dynamiquement des fonctionnalités tierces, de réagir à des événements, et de rester entièrement découplée de ses extensions.


Pourquoi un système de plugins ?


Avant de coder, posons les fondations :
  • Extensibilité : ajouter des fonctionnalités sans recompiler le cœur.
  • Découplage : le noyau ne dépend pas des plugins (et inversement).
  • Sécurité : les plugins tournent dans un cadre défini.
  • Maintenance : un plugin défectueux n’arrête pas toute l’application.
  • Écosystème : ouvrir la porte à une communauté de développeurs.

C’est exactement ce que font WordPress (avec ses 60 000+ plugins) ou Shopify (avec ses apps). En Java, on peut atteindre le même objectif grâce à trois piliers :
  1. Les interfaces (contrats),
  2. Le chargement dynamique de classes,
  3. Le mécanisme ServiceLoader (standard Java).

Étape 1 : Définir le contrat (l’API du noyau)

Tout commence par une interface. C’est le seul lien entre le cœur de l’application et les plugins.

public interface Plugin {
    String getName();
    void onLoad(Core core);
    void onEvent(String eventName, Object data);
}

Et une classe centrale qui orchestre tout :

import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;

public class Core {
    private static final Logger logger = Logger.getLogger(Core.class.getName());
    private final List<Plugin> plugins = new ArrayList<>();

    public void registerPlugin(Plugin plugin) {
        if (plugin == null) {
            logger.warning("Tentative d'enregistrement d'un plugin null");
            return;
        }
        plugins.add(plugin);
        safeCall(() -> plugin.onLoad(this), plugin.getName(), "onLoad");
    }

    public void fireEvent(String eventName, Object data) {
        for (Plugin plugin : plugins) {
            safeCall(() -> plugin.onEvent(eventName, data), plugin.getName(), "onEvent(\"" + eventName + "\")");
        }
    }

    // Méthode utilitaire pour exécuter du code de plugin de façon sécurisée
    private void safeCall(Runnable action, String pluginName, String context) {
        try {
            action.run();
        } catch (Exception e) {
            logger.log(
                Level.SEVERE,
                String.format(
                    "Le plugin '%s' a échoué lors de l'appel à %s : %s",
                    pluginName,
                    context,
                    e.toString()
                ),
                e // inclut la stack trace complète
            );
            // On continue, le plugin suivant est exécuté
        }
    }

    public void start() {
        logger.info("Démarrage de l'application...");
        fireEvent("app.start", null);
        fireEvent("user.register", "laboiteaprog@gmail.com");
        logger.info("Application terminée.");
    }
}

Ce Core est le cœur de notre application. Il ne sait rien des plugins concrets — il ne connaît que l’interface Plugin. C’est le principe du « program to an interface, not an implementation ».


Étape 2 : Créer un plugin

Imaginons un plugin qui envoie un email lorsqu’un utilisateur s’inscrit.

public class EmailNotifierPlugin implements Plugin {
    @Override
    public String getName() {
        return "EmailNotifier";
    }

    @Override
    public void onLoad(Core core) {
        System.out.println("Email Plugin chargé !");
    }

    @Override
    public void onEvent(String eventName, Object data) {
        if ("user.register".equals(eventName)) {
            System.out.println("Bienvenue envoyé à " + data);
        }
    }
}

Ce plugin est autonome. Il ne dépend que de Plugin et Core — deux classes que nous mettrons dans une API partagée.

Étape 3 : Découvrir les plugins avec ServiceLoader


Java offre depuis Java 6 un mécanisme standard pour charger des implémentations dynamiquement : java.util.ServiceLoader.

Il fonctionne ainsi :
  1. Chaque JAR de plugin contient un fichier de configuration.
  2. Ce fichier indique quelle classe implémente l’interface.
  3. L’application utilise ServiceLoader pour les découvrir automatiquement.

Le fichier de configuration

Dans le JAR du plugin, créez :
META-INF/services/Plugin

Contenu :
EmailNotifierPlugin

Si vous utilisez des packages, indiquez le nom qualifié complet : com.mesplugins.EmailNotifierPlugin.



Étape 4 : Charger et exécuter


Voici le point d’entrée de notre application :
import java.util.ServiceLoader;

public class Main {
    public static void main(String[] args) {
        Core core = new Core();

        // Découverte automatique des plugins
        ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
        for (Plugin p : loader) {
            core.registerPlugin(p);
        }

        core.start();
    }
}

Lorsque vous exécutez l’application avec les JARs des plugins dans le classpath, ils sont chargés automatiquement.


Plugin chargé !
Application démarrée
Bienvenue envoyé à laboiteaprog@gmail.com
Terminé


Aller plus loin

Chargement depuis un dossier

Pour imiter WordPress (/wp-content/plugins), chargez les plugins depuis un dossier à l’exécution :

File pluginDir = new File("plugins");
for (File jar : pluginDir.listFiles((d, name) -> name.endsWith(".jar"))) {
    URLClassLoader loader = new URLClassLoader(
        new URL[]{jar.toURI().toURL()},
        Main.class.getClassLoader()
    );
    ServiceLoader<Plugin> sl = ServiceLoader.load(Plugin.class, loader);
    sl.forEach(core::registerPlugin);
}

Désactivation

Les plugins qui ont échoué au chargement pourrait être désactivé. Une Map pourrait être utilisé pour cette fonctionalité.


Map<String, Integer> errorCount = new HashMap<>();
if (++errorCount.getOrDefault(pluginName, 0) > 3) {
    logger.warning("Plugin '" + pluginName + "' désactivé après trop d'erreurs.");
    // le retirer de la liste `plugins` (attention à la concurrence)
}

Timeout

Pour éviter qu’un plugin ne bloque indéfiniment (boucle infinie, appel réseau bloquant), vous pouvez exécuter les appels dans un Future avec délai :

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(action);
try {
    future.get(5, TimeUnit.SECONDS); // timeout de 5s
} catch (TimeoutException e) {
    future.cancel(true);
    logger.severe("Plugin '" + pluginName + "' a dépassé le délai autorisé.");
}

Conclusion

Un système de plugins n’est pas réservé qu'aux CMS PHP ou aux plateformes SaaS. En Java, avec  le mécanisme standard ServiceLoader, on peut créer une architecture modulaire, extensible et robuste exactement comme WordPress. D'ailleurs les IDE tel que netbeans, eclipse font usage des plugins. 

samedi 1 novembre 2025

Spring Events : la magie du pattern Observer

Spring a une solution simple pour découpler ton code sans ajouter de dépendances. 

Quelques annotations magiques... au final c'’est juste une implémentation propre du bon vieux pattern Observer qui semble sous utilisé dans la communauté Spring.

Le problème classique

Imaginons que lorsqu'une commande est passée, il faut envoyer un email, logger l’action, invalider un cache, et notifier un service d’analytics.

Si tu mets tout ça dans ta méthode placeOrder(), tu mélanges logique métier et effets secondaires, ton code devient difficile à tester, à maintenir, et à étendre.


La solution Spring : les événements

Spring te permet de publier un événement et de laisser d’autres composants réagir sans les connaître.

public record OrderPlacedEvent(Order order) {}

Mise en place de ton évènement

@Service
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;

    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void placeOrder(Order order) {
        // ... logique métier
        eventPublisher.publishEvent(new OrderPlacedEvent(order));
    }
}

Réaction à l'évènement

@Component
public class EmailListener {
    @EventListener
    public void sendConfirmation(OrderPlacedEvent event) {
        // Envoie un courriel
    }
}

@Component
public class AuditListener {
    @EventListener
    public void logOrder(OrderPlacedEvent event) {
        // Écrit dans les logs ou une table d’audit
    }
}

Si tu as besoin d'ajouter un nouveau comportement, tu n'as pas besoin de modifier OrderService.

Attention aux pièges

  • Les listeners synchrones partagent la même transaction que l’émetteur. Si la transaction échoue, aucun effet ne doit persister.
  • Les listeners asynchrones ne peuvent pas participer à la transaction car il y a risque de divergence si tu modifies des données critiques.

Fonctionnalités avancées

Ajout de condition

@EventListener(condition = "#event.order.total > 1000")
public void notifyPremiumSupport(OrderPlacedEvent event) {
...
}

Il y a la possibilité d'imposer un ordre d'exécution

@Component
public class EmailListener {
    @EventListener
    @Order(1)
    public void sendConfirmation(OrderPlacedEvent event) {
        // Envoie un courriel
    }
} 

@Component
public class AuditListener {
    @EventListener
    @Order(2)
    public void logOrder(OrderPlacedEvent event) {
    // Écrit dans les logs ou une table d’audit
    }
}

Détail technique concernant l'ordre

  • Spring collecte tous les @EventListener écoutant un événement donné.
  • Il les trie selon :La valeur de @Order (plus petit = exécuté en premier). Si aucune @Order n’est spécifiée, la valeur par défaut est Ordered.LOWEST_PRECEDENCE (c’est-à-dire exécuté en dernier).
  • Le tri est strictement global au contexte applicatif, pas local à la classe ou au package.Spring permet de mettre des conditions

Pourquoi l’utiliser ?

  • Zéro dépendance externe (c’est dans Spring Core).
  • Typé, testable, et lisible.
  • Parfait pour les architectures modulaires ou les librairies réutilisables.

Il est possible d'utiliser spring modulith pour aller encore plus loin dans la communication de vos modules de votre application monolith.

Ce n’est pas de la magie… c’est du bon design, rendu simple par Spring.

Refonte ou refactoring

Refonte vs Refactoring : Quand réécrire, quand faire du refactoring ? Face à un code legacy, la tentation de tout réécrire est forte. Pourt...