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.

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

Et une classe centrale qui orchestre tout :

// Core.java (version robuste)
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.

// EmailNotifierPlugin.java
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 :

// Main.java
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. 

Aucun commentaire:

Enregistrer un commentaire

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