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.

jeudi 26 juin 2025

Concevoir un système d’annotations pour l’automatisation

À la suite de l'article concernant Automatisation d'interface utilisateur, j'ai reçu différents commentaires relatifs au choix qui avait ét fait. J'ai décidé d'écrire un autre article présentant différentes conceptions, avec leurs avantages et inconvénieunts respectifs

Annotation et convention over configuration

L'approche qui avait été adoptée était basée sur une annotation par responsabilité, avec le moins de choix possible. Un maximum d'automatisation avait été mis en place.


@Order(value = 9)
@HideField(value = SectionEnum.TABLE)
@Mandatory
private Integer groundElementOwnerId;

Les annotations indiquent 

  • que le champs serait affiché à la neuvième position.
  • que le champ serait caché dans la section table de la page.
  • que le champ est obligatoire

@Order(value = 7)
@TableStruture
private Map<String,String> properties;

Ici il y avait détection de la map et un tableau était affiché.




Différentes conventions avaient été mises en place.

Concernant les select, lorsque le nom de l'attribut contenait "id" et comportait plus de deux caractères, une tentative était faite pour rechercher un service au nom du dto et de trouver une méthode s'appelant findAll. Le résultat était mis dans le modèle et pouvait être utilisé dans thymeleaf.

Lorsqu'un traitement spécifique était nécessaire pour un champ, ce dernier pouvait être masqué via l'annotation et une gestion personnalisée était alors implémentée dans un template thymeleaf.

Cette première approche avait été simple en mettre place, car il y avait peu de cas spécifique à gérer. Le développeur pouvait aisément ajouter de nouveau dto.

La gestion de la construction de la structure dynamique était assez simple malgré les quelques annotations à gérer. Peu d'attribut était disponible dans chaune des annotations.


Annotation générique

Avec cette approche, une seule annotation est définie avec l'ensemble des attributs possibles. C'est possiblement la plus simple, même si le développeur doit fournir davantage d'informations. Le principal défaut est qu'il est possible que le développeur assigne des valeurs inexistantes  pour le type spécifié. Deplus, l'annotation a plusieurs responsabilités.

@FormField(type = FormFieldType.PHONE, order = 9, pattern = "999 999-9999")
private String phone;
    
@FormField(type = FormFieldType.SELECT, optionsProvider = "getCities", order = 5 )
private String city;
    
@FormField(type = FormFieldType.CHECKBOX, options = {"sportCheck:Sport", "musiqueCheck:Musique", "lectureCheck:Lecture"}, order = 15 )
private List<String> interest;

Une seule annotation facilite la construction de la structure dynamique. Seuls les attributs existants pour un type donnée sont pris en compte.


Annotation par responsabilité

Les responsabilités sont définies via différentes annotations. Le dto devient plus verbeux, et la gestion plus complexe pour construire la structure dynamique. C'est une version avancé que la première approche.

@FormField(type = FieldType.SELECT)
@SelectConfig(valueField = "id", labelField = "name")
private List<Ville> villes;

@FormField(type = FieldType.INPUT, placeholder = "+1 (555) 123-4567")
@FormStyle(cssClass="form-control")
@FormValidation(required=true, readonly=true, pattern="'mask' : '999 999-9999'")
@Order(8)
private String phone;

Annotation générique avec avec type spécifique

Chaque type dispose de sa propre annotation avec ses propres attributs. Ainsi, le développeur ne peut pas se tromper sur les propriétés applicables.

@HiddenInput(order=1)
private Long idEditeur;

@TextInput(minLength = 2, maxLength = 20, order=2)
private String name;

@TextInput(readonly=true, order=3)
private String age;

@SelectInput(optionsProvider = "getCities", required = true, order=4)
private String city

Approche assez simple à comprendre, cependant l'attribut order bien qu'utile pour la position d'affichage n'étant pas un attribut HTML, il vient briser le concept de séparation des responsabilités. Il pourrait être mis dans une autre annotation. Parfois un juste milieu est la meilleur solution.


Une librairie a été créé: https://github.com/marccollin/formatic


Quel approche au niveau des annotations préférez-vous?



jeudi 19 juin 2025

Automatisation d'interface utilisateur

La solution qui est présentée se base sur thymeleaf et boostrap 3. 

Interface utilisateur

Depuis le tout début de ma carrière, à chaque emploi j'ai eu à créer de nombreuses interfaces web pour des applications de gestion. Ce n'est pas nécessairement très intéressant, c'est redonnant et c'est de plus en plus complexe pour au final prendre des données, les afficher à l'utilisateur, lui permettre de les modifier et finalement les retourner au serveur.

Après avoir fait du jsp, jsf, asp, php, struts, gwt, gxt, gwt-ext, jquery, thymeleaf j'avais de grosse attente envers les nouveaux framework / librairie frontend. J'ai été malheureusement fortement déçu après avoir fait quelques nouvelles applications from scratch en angular / react.

Encore plus complexe qu'auparavant, mais plus découpé. Courbe d'apprentissage assez élevé et maintenance coûteuse. Le temps et argent pris pour constamment mettre à jour des libraires c'est du temps, argent en moins pour ajouter des nouvelles fonctionnalités à l'utilisateur.

Sur un nouveau projet qui allait demander la création d'une centaine d'écran avec la majeure partie du temps que deux développeurs, il fallait trouver une solution efficace et rapide. Une preuve de concept au tout début avec  react / angular / vue afin de voir si c'était viable de les utiliser fût créé.

Personne à l'interne n'avait de l'expérience avec ces outils. De plus la centaine d'écrans à réaliser n'était qu'une petite partie insignifiante du système à créer. Il fût rapidement décidé qu'avec le temps et le budget alloué que ces outils ne pouvaient assurer une réussite au projet.

Idée

Une centaine d'écrans à réaliser en peu de temps, comment y parvenir?
J'ai alors commencé à penser à des critères que les interfaces avaient en commun. Est-ce qu'il y aurait un moyen d'automatiser les interfaces utilisateur d'une application de gestion?
Qu'est-ce qui peut changer? Les types de données. Est-ce que le champ est obligatoire? L'ordre d'affichage des composants. Est-ce qu'il doit être formaté? L'affichage du champs est nécessaire? Est-ce qu'il doit avoir un style particulier à appliquer au champ?

Une annoation à été créé pour ces différents critères. Certaine aurait pu être combiné et avoir des attributs.

Reflection

Afin d'éviter le plus possible d'écrire du code, dans un esprit de généricité avec les différents critères récurrents d'un écran à l'autre, j'ai regardé du côté de la réflection en java. J'ai mis en place des annotations pour différent critère qui serait utilisé dans un dto (représentant les données d'une interface utilisateur). Par la suite, via la réflection ces dto  sont parcouru et un  objet générique est utilisé pour construire l'interface utilisateur via thymeleaf.

public class UsersDto {

    @Order(value = 1)
    private Long id

    @Mandatory
    @Order(value = 2)
    private String firstname;

    @Order(value = 3)
    private String lastname;

    @Order(value = 4)
    private String username;

    @TextArea(value=true)
    @OuterCssClass(value="col-md-12")
    @Order(value = 17)
    private String comment;

    @MandatoryField(value=true)
    @Formatter(value="'mask' : '999 999-9999'")
    private String phone;

    private LocalDate birthDate;

    private List<Ville> villesId

}


Structure de données où les informations étaient mises

@Data
public class UIReflection {

    private String field;
    
    private Object value;

    private String formatter;
    private String sortable;
    private boolean mandatory;
    
    private String i18n;
    
    private String dataType;
    private String uiComponent;
}

Ensuite une opération analysait le dto et pour chacun des champs du dto, un objet de ce type était créer.
Si le type de donnée était de type Date, un DateTimePicker serait inséré dans l'attribut uiComponent Ceci est une version simplifié de ce qui a été fait.

Class<?> myUserDto = UsersDto.class;
Field[] allFields = myClass.getFields();

List<UIReflection> uiReflections = new ArrayList<>();

for (Field field : allFields) {

    UIReflection uiReflection = new UIReflection();
    uiReflection.setField(field.getName());
    uiReflection.setI18n(field.getName());
    uiReflection.setMandatory(mandatoryValue(field));
    uiReflection.setFormatter(formatter(field));
    
    if (field.getType().equals(LocalDate.class)) {
        uiReflection.setDataType("localdate");
    } else if (field.getType().equals(boolean.class)) {
        uiReflection.setDataType("boolean");
    } else if (ReflectionUtils.isNumeric(field.getType()) && !field.getType().getName().contains("Id")){
        uiReflection.setDataType("number");
    } else if (field.getType().equals(String.class)) {
        if(textArea(field)){
            uiReflection.setDataType("textArea");
        }
    }
    uiReflections.add(uiReflection);
}
    

La création d'une nouvelle écran se résumait majoritairement à créer un dto et mettre les annotations sur chacun des champs.

Template / fragment Thymeleaf

Par la suite, un template  parcourait les objets UIReflection  affectaient les valeurs css, attribut aux composants html.

 <div th:fragment="Generic">
            <input id="genericFormId" name="id" type="hidden"/>
            <div th:each="uiGenericForm, iterStat : ${uiReflectionsForm}" th:remove="tag">
                
                <div class="col-sm-6">
                    
                    <div th:class="${uiGenericForm.mandatory ? 'form-group required' : 'form-group' }" class="form-group">
                        <label th:attr="for='genericForm'+${uiGenericForm.field}" class="col-sm-3 control-label" th:text="#{${uiGenericForm.i18n}}"></label>
                        <div class="col-sm-9">
                            <div th:switch="${uiGenericForm.dataType}" th:remove="tag">

                                <div th:case="localDateTime" class="input-group date" th:id="'genericForm'+${uiGenericForm.field}+'DatePicker'">
                                    <input type="text" th:id="'genericForm'+${uiGenericForm.field}" th:attr="name=${uiGenericForm.field}" class="form-control"/>
                                    <span class="input-group-addon">
                                        <span class="glyphicon glyphicon-calendar"></span>
                                    </span>
                                </div>

                                <div th:case="selection" th:remove="tag">
                                    
                                    <div th:if="${uiGenericForm.mandatory}" th:remove="tag">
                                        
                                        <select  class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:name="${uiGenericForm.field}" th:attr="data-msg=#{form.mandatory.field}" >
                                            <option th:value="NULL" selected="selected" th:text="#{form.select.empty}"></option>
                                            <option th:each="generic : ${__${uiGenericForm.field}__}" th:attr="data-id=${generic.id}" th:value="${generic.id}" th:text="${generic.name}"></option>
                                         </select>
                                        
                                    </div>
                                    
                                    <div th:unless="${uiGenericForm.mandatory}" th:remove="tag">
                                        
                                        <select  class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:name="${uiGenericForm.field}" >
                                            <option th:value="NULL" selected="selected" th:text="#{form.select.empty}"></option>
                                            <option th:each="generic : ${__${uiGenericForm.field}__}" th:attr="data-id=${generic.id}" th:value="${generic.id}" th:text="${generic.name}"></option>
                                         </select>
                                        
                                    </div>
                                    
                                </div>

                                <input th:case="boolean" type="checkbox" th:attr="name=${uiGenericForm.field}"/>

                                <div th:case="map">
                                   <div th:include="fragments/map-table :: map-table" th:remove="tag"></div>
                                </div>

                                <div th:case="number" th:remove="tag">
                                   
                                    <div th:if="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="number" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="data-msg=#{form.mandatory.field}, name=${uiGenericForm.field}" required/>
                                    </div>  

                                    <div th:unless="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="number" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="name=${uiGenericForm.field}"/>
                                    </div> 
                                    
                                </div>
                                
                                <div th:case="*" th:remove="tag">
                                
                                    <div th:if="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="text" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="data-msg=#{form.mandatory.field}, name=${uiGenericForm.field}" required/>
                                    </div>  

                                    <div th:unless="${uiGenericForm.mandatory}" th:remove="tag">
                                        <input type="text" class="form-control" th:id="'genericForm'+${uiGenericForm.field}" th:attr="name=${uiGenericForm.field}"/>
                                    </div> 
                                
                                </div>

                             </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>


Une fois que le template est traité.


Possibilité


Préférez-vous les formulaires horizontaux au formulaires verticaux?  Appréciez-vous davantage Tailwind css que bootstrap? Quelques modification dans les classes css du template et le tour est joué.

Il est tout à fait possible de changer ce script thymeleaf par un autre moteur de template orienté serveur tel que freemaker, groovy, pebble... Htmx pourrait être utilisé afin de ne pas rafraichir la page au complet.

Dans notre cas, htmx n'existait pas. On chargait les templates via fetch afin d'éviter de recharger la page au complet. Un api rest était utiliser pour afficher, modifier et sauvegarer une ressource. Donc au final du MVC et REST.

Il serait aussi envisageable de retourner la structure de UIReflection en JSON. VueJs, React, Angular... pourrait alors être utilisé pour générer le UI au lieu d'un template serveur. 




lundi 16 juin 2025

Kde 6 vs Windows 11

Nous allons comparer Kde 6 à l'environnement graphique de Windows 11.

Le précédent comparatif date de 2015 


Voici une liste d'ancien comparatif.




Menu

Le menu sous Kde est toujours divisé par des catégories. Prenez note que sous une autre distribution, cela pourrait être différent.







Calculatrice


Un mode simple, scientifique, statistique et numérique est disponible 



Configuration

Le panneau de configuration s'est simplifié, un menu par catégorie s'est ajouté.




Explorateur de fichier





Bureau





Navigateur internet




Konqueror est le navigateur par défaut, cependant, il y a aussi maintenant aussi Falkon. Konquer gère toujours la gestion des fichiers et supporte de multiple protocole réseau.



Ressource système


Cette application a été revu. La vue principal donne une bonne idée de l'état actuel du système. Pour plus de détail, les autres sections sont disponible.





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...