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 :
- Les interfaces (contrats),
- Le chargement dynamique de classes,
- Le mécanisme ServiceLoader (standard Java).
Étape 1 : Définir le contrat (l’API du noyau)
// Plugin.java
public interface Plugin {
String getName();
void onLoad(Core core);
void onEvent(String eventName, Object data);
}
// 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.");
}
}
É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);
}
}
}
É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 :
- Chaque JAR de plugin contient un fichier de configuration.
- Ce fichier indique quelle classe implémente l’interface.
- L’application utilise ServiceLoader pour les découvrir automatiquement.
Le fichier de configuration
Dans le JAR du plugin, créez :
META-INF/services/Plugin
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();
}
}
Plugin chargé !Application démarréeBienvenue envoyé à laboiteaprog@gmail.comTerminé
Aller plus loin
Chargement depuis un dossier
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);
}
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é.");
}
Aucun commentaire:
Enregistrer un commentaire