Dans cet article, je voudrais vous expliquer comment créer rapidement et facilement un framework d'application Java avec prise en charge du chargement dynamique de plugins. Le lecteur pensera probablement immédiatement qu'une telle tâche a été résolue depuis longtemps, et vous pouvez simplement utiliser des frameworks prêts à l'emploi ou écrire votre chargeur de classe, mais rien de tout cela ne sera requis dans la solution que je propose:
- Nous n'avons pas besoin de bibliothèques ou de frameworks spéciaux ( OSGi , Guice, etc.)
- Nous n'utiliserons pas l'analyse de bytecode avec ASM et des bibliothèques similaires.
- Nous n'écrirons pas notre chargeur de classe.
- Nous n'utiliserons pas de réflexion et d'annotations.
- Pas besoin de s'occuper du chemin de classe pour trouver des plugins. Nous ne toucherons pas du tout au chemin de classe.
- De plus, nous n'utiliserons pas XML, YAML ou tout autre langage déclaratif pour décrire les points d'extension (points d'extension dans les plugins).
Cependant, il y a encore une exigence - une telle solution ne fonctionnera que sur Java 9 ou supérieur. Parce qu'il sera basé sur des
modules et des services .
Commençons donc. Nous formulons le problème plus spécifiquement:
Vous devez implémenter un cadre d'application minimal qui, au démarrage, chargera les plugins utilisateur à partir du dossier plugins
.
Autrement dit, l'application assemblée devrait ressembler à ceci:
plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar …
Commençons par le module de
core
. Ce module est au cœur de notre application, c'est-à-dire en fait notre framework.
Pour ceux qui apprécient le temps, le projet terminé est disponible sur GitHub. Instructions de montage.Lien git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core
Créez les 4 fichiers Java suivants dans le module:
core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java
Le premier fichier,
IService.java
est le fichier qui décrit notre point d'extension. D'autres plugins pourront alors contribuer à ce point d'expansion («contribution»). Il s'agit du principe standard de création d'applications plug-in, appelé
principe d'inversion de dépendance (Dependency Inversion). Ce principe est basé sur le fait que le noyau ne dépend pas de classes spécifiques, mais d'interfaces.
J'ai donné au point d'extension le nom abstrait
IService
, car je démontre maintenant un concept exclusivement. En réalité, il peut s'agir de n'importe quel point d'extension spécifique, par exemple, si vous écrivez un éditeur graphique, cela peut être l'effet du traitement d'image, par exemple
IEffectProvider
,
IEffectContribution
ou autre chose, selon la façon dont vous préférez nommer les points d'extension. Dans le même temps, l'application elle-même contiendra un ensemble d'effets de base, et les développeurs tiers pourront écrire des effets supplémentaires plus sophistiqués et les fournir sous forme de plug-ins. L'utilisateur n'a qu'à placer ces effets dans le dossier des
plugins
et redémarrer l'application.
Le fichier
IService.java
est le suivant:
… public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } }
Ainsi,
IService
n'est qu'une interface qui fait un travail abstrait
doJob()
(je le répète, les détails ne sont pas importants, en réalité ce sera quelque chose de concret).
getServices()
également attention à la deuxième méthode
getServices()
. Cette méthode renvoie toutes les implémentations de l'interface
IService
trouvées dans cette couche de module et ses parents. Nous en parlerons plus en détail plus tard.
Le deuxième fichier,
BasicService.java
, est l'implémentation de base de l'interface
IService
. Il sera toujours présent, même s'il n'y a pas de plugins dans l'application. En d'autres termes, le
core
n'est pas seulement le noyau, mais aussi en même temps un plugin pour lui-même, qui sera toujours chargé. Le fichier
BasicService.java
ressemble à ceci:
… public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } }
Pour plus de simplicité,
doJob()
simplement la chaîne
"Basic service"
et c'est tout.
Ainsi, pour le moment, nous avons l'image suivante:

Le troisième fichier,
Main.java
, est l'endroit où la méthode
main()
est implémentée. Il y a un peu de magie dedans, pour comprendre ce dont vous avez besoin pour savoir ce qu'est une couche de module.
À propos des couches de modules
Lorsque Java lance l'application, tous les modules de la plate-forme + modules répertoriés dans l'argument
--module-path
(et également le
--module-path
, le cas échéant) tombent dans la couche dite de
Boot
. Dans notre cas, si nous compilons le module core.jar et
java --module-path core.jar --module core
partir de la ligne de commande, alors au moins les modules
java.base
et
core
seront dans la couche
Boot
:

La couche de
Boot
est toujours présente dans n'importe quelle application Java, et c'est la configuration la plus petite possible. La plupart des applications existent dans une seule couche de modules. Cependant, dans notre cas, nous voulons faire un chargement dynamique des plugins depuis le dossier
plugins
. Nous pourrions simplement forcer l'utilisateur à corriger la ligne de lancement de l'application afin qu'il ajoute lui-même les plugins nécessaires à
--module-path
, mais ce ne sera pas la meilleure solution. Surtout les gens qui ne sont pas programmeurs et ne comprennent pas pourquoi ils doivent grimper quelque part et réparer quelque chose pour une chose aussi simple.
Heureusement, il existe une solution: Java vous permet de créer vos propres couches de modules lors de l'exécution, ce qui chargera les modules de l'endroit dont nous avons besoin. Pour nos besoins, une nouvelle couche pour les plugins sera suffisante, qui aura une couche de
Boot
tant que parent (toute couche doit avoir un parent):

Le fait que la couche plug-in ait la couche
Boot
comme parent signifie que les modules de la couche plug-in peuvent référencer les modules de la couche
Boot
, mais pas l'inverse.
Donc, sachant maintenant ce qu'est une couche de module, vous pouvez enfin regarder le contenu du fichier
Main.java
:
… public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins");
Si c'est la première fois que vous regardez ce code, cela peut sembler très compliqué, mais c'est une fausse sensation en raison du grand nombre de nouvelles classes inconnues. Si vous comprenez un peu la signification des
classes ModuleFinder ,
Configuration et
ModuleLayer , alors tout se met en place. Et puis, il n'y a que quelques dizaines de lignes! C'est toute la logique qui est écrite une fois.
Descripteur de module
Il y a un autre (quatrième) fichier que nous n'avons pas considéré:
module-info.java
. Il s'agit du fichier le plus court contenant la déclaration de notre module et une description des services (points d'extension):
… module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; }
La signification des lignes de ce fichier doit être évidente:
- Premièrement, le module exporte le package
org.example.pluginapp.core
que les plugins puissent hériter de l'interface IService
(sinon IService
ne serait pas accessible en dehors du module core
). - Deuxièmement, il annonce qu'il utilise l'
IService
. - Troisièmement, il dit qu'il fournit une implémentation du service
IService
via la classe BasicService
.
La déclaration du module étant écrite en Java, nous bénéficions d'avantages très importants:
vérifications du compilateur et garanties statiques . Par exemple, si nous avions fait une erreur dans le nom des types ou indiqué un paquet inexistant, nous l'aurions obtenu tout de suite. Dans le cas de certains OSGi, nous n'aurions aucun contrôle au moment de la compilation, car la déclaration des points d'extension serait écrite en XML.
Ainsi, le cadre est prêt. Essayons de l'exécuter:
> java --module-path core.jar --module core Basic service
Qu'est-il arrivé?
- Java a essayé de trouver les modules dans le dossier des
plugins
et n'en a trouvé aucun. - Un calque vide a été créé.
- ServiceLoader a commencé à rechercher toutes les implémentations d'
IService
. - Dans la couche vide, il n'a trouvé aucune implémentation de service, car il n'y a pas de modules.
- Après cette couche, il a poursuivi sa recherche dans la couche parent (c'est-à-dire la couche de
Boot
) et a trouvé une implémentation de BasicService
dans le module core
. - Toutes les implémentations trouvées avaient la méthode
doJob()
appelée. Puisqu'une seule implémentation a été trouvée, seul le "Basic service"
été imprimé.
Écrire un plugin
Après avoir écrit le cœur de notre application, il est temps d'essayer d'écrire des plugins pour cela.
plugin1
deux plugins
plugin1
et
plugin2
: laissez le premier imprimer
"Service 1"
, le second -
"Service 2"
. Pour ce faire, vous devez fournir deux implémentations
plugin1
plugin2
respectivement dans
plugin1
et
plugin2
:

Créez le premier plugin avec deux fichiers:
plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java
Fichier
Service1.java
:
… public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } }
Fichier
module-info.java
:
… module plugin1 { requires core; provides IService with Service1; }
Notez que
plugin1
dépend du
core
. C'est le principe d'inversion de dépendance que j'ai mentionné plus tôt: le noyau ne dépend pas des plugins, mais vice versa.
Le deuxième plugin est complètement similaire au premier, donc je ne le donnerai pas ici.
Maintenant, collectons les plugins, mettons-les dans le dossier des
plugins
et exécutons l'application:
> java --module-path core.jar --module core Service 1 Service 2 Basic service
Hourra, les plugins ont été récupérés! Comment est-ce arrivé:
- Java a trouvé deux modules dans le dossier
plugins
. - Une couche a été créée avec deux modules
plugins1
et plugins2
. - ServiceLoader a commencé à rechercher toutes les implémentations d'
IService
. - Dans la couche plugin, il a trouvé deux implémentations du service
IService
. - Après cela, il a continué à rechercher dans la couche parent (c'est-à-dire la couche de
Boot
) et a trouvé une implémentation de BasicService
dans le module core
. - Toutes les implémentations trouvées avaient la méthode
doJob()
appelée.
Notez que c'est précisément parce que la recherche de fournisseurs de services commence par les couches enfants, puis passe aux couches parentes, puis
"Service 1"
et
"Service 2"
imprimés en premier, puis
"Basic Service"
. Si vous souhaitez que les services soient triés de manière à ce que les services de base passent en premier, puis les plugins, vous pouvez modifier la méthode
IService.getServices()
en y ajoutant le tri (vous devrez peut-être ajouter la
int getOrdering()
à l'interface
IService
).
Résumé
J'ai donc montré comment organiser rapidement et efficacement une application plug-in Java qui possède les propriétés suivantes:
- Simplicité: pour les points d'extension et leurs liaisons, seules les fonctionnalités Java de base (interfaces, classes et ServiceLoader ) sont utilisées, sans frameworks, réflexion, annotations et chargeurs de classe.
- Déclarabilité: les points d'extension sont décrits dans les descripteurs de module. Regardez simplement
module-info.java
et comprenez quels points d'extension existent et quels plugins contribuent à ces points. - Garanties statiques: en cas d'erreur dans les descripteurs de module, le programme ne compilera pas. De plus, en bonus, si vous utilisez IntelliJ IDEA, vous recevrez des avertissements supplémentaires (par exemple, si vous avez oublié d'utiliser
uses
et d'utiliser ServiceLoader.load()
) - Sécurité: le système Java modulaire vérifie au démarrage que la configuration des modules est correcte et refuse d'exécuter le programme en cas d'erreur.
Je le répète, je n'ai montré que l'idée. Dans une véritable application de plug-in, il y aurait des dizaines à des centaines de modules et des centaines à des milliers de points d'extension.
J'ai décidé de soulever ce sujet car depuis 7 ans, j'écris une application modulaire utilisant Eclipse RCP, dans laquelle le fameux OSGi est utilisé comme système de plug-in et les descripteurs de plug-in sont écrits en XML. Nous avons plus d'une centaine de plugins et nous sommes toujours assis sur Java 8. Mais même si nous passons à une nouvelle version de Java, il est peu probable que nous utilisions des modules Java, car ils sont fortement liés à OSGi.
Mais si vous écrivez une application plug-in à partir de zéro, les modules Java sont l'une des options possibles pour sa mise en œuvre. N'oubliez pas que les modules ne sont qu'un outil et non un objectif.
En bref sur moi
Je programme depuis plus de 10 ans (dont 8 en Java), je réponds à
StackOverflow et dirige mon propre
canal en Telegram dédié à Java.