Java Modules : Services
Patron service-loader dans le système de modules Java avec les directives uses et provides.
L'un des objectifs centraux des modules est le découplage : le code qui utilise une capacité ne doit pas nommer la classe qui l'implémente. Le Java Platform Module System (JPMS) en fait une fonctionnalité de première classe grâce à deux directives — uses et provides — reliées à l'exécution par ServiceLoader. Il s'agit du remplacement modulaire de l'ancienne convention classpath consistant à déposer un fichier META-INF/services dans un JAR.
Ce chapitre suppose que vous savez déjà comment écrire un descripteur module-info.java ; nous y ajoutons ici les directives de service.
Les trois rôles
Un service se compose de trois parties, idéalement dans trois modules distincts :
- L'interface de service (ou classe abstraite) — le contrat, par exemple
PricingRule. Elle réside dans un module qui l'exporte. - Le consommateur — le code qui demande des implémentations. Son module déclare
uses com.acme.PricingRule;et appelleServiceLoader.load(PricingRule.class). - Un ou plusieurs fournisseurs — des modules d'implémentation. Chacun déclare
provides com.acme.PricingRule with com.acme.impl.StandardPricing;.
Le consommateur n'importe jamais StandardPricing. Il ne connaît que l'interface. Ajoutez un nouveau module fournisseur au chemin de modules et le consommateur le détecte automatiquement — sans recompilation ni modification du code.
Les directives dans module-info.java
// module com.acme.api
module com.acme.api {
exports com.acme; // export the PricingRule interface
}
// module com.acme.app (the consumer)
module com.acme.app {
requires com.acme.api;
uses com.acme.PricingRule; // "I will ServiceLoader.load this"
}
// module com.acme.standard (a provider)
module com.acme.standard {
requires com.acme.api;
provides com.acme.PricingRule
with com.acme.standard.StandardPricing;
}uses indique au résolveur que le consommateur recherchera ce service, de sorte que le module est autorisé à appeler ServiceLoader.load. provides … with … enregistre une implémentation. La classe fournisseur doit posséder soit un constructeur public sans argument, soit une méthode statique publique provider() qui retourne une instance.
Consommation avec ServiceLoader
ServiceLoader<PricingRule> loader = ServiceLoader.load(PricingRule.class);
for (PricingRule rule : loader) {
System.out.println(rule.describe());
}
// or pick the first available
PricingRule rule = ServiceLoader.load(PricingRule.class)
.findFirst()
.orElseThrow();ServiceLoader est paresseux — chaque fournisseur n'est instancié que lorsque l'itérateur l'atteint — et il met les instances en cache. Il implémente Iterable, donc une boucle for-each parcourt tous les fournisseurs enregistrés.
Un exemple concret : ServiceLoader sur un vrai service JDK
Le JDK fournit déjà un service qu'on peut charger sans construire trois modules : java.util.spi.ToolProvider. Le compilateur (javac), l'outil JAR (jar) et d'autres sont enregistrés en tant que fournisseurs de cette interface dans les modules JDK. Ce programme les charge via ServiceLoader — exactement le code consommateur ci-dessus, appliqué à un service déjà câblé.
Ce qu'il faut retenir de l'exécution :
- La boucle for-each a parcouru chaque
ToolProviderenregistré par le runtime (généralementjavac,jar,javadoc, …) sans que le programme n'importe jamais l'une de ces classes d'implémentation. C'est tout le principe des services : le consommateur dépend deToolProvider, l'interface, et découvre les fournisseurs concrets à l'exécution. - Chaque fournisseur a affiché un nom de classe d'implémentation différent alors qu'ils partagent la même interface. Les modules JDK ont déclaré
provides java.util.spi.ToolProvider with …dans leurs descripteurs ;ServiceLoaderles a tous collectés. L'ajout d'un autre module fournisseur le ferait apparaître dans cette même boucle sans aucune modification ici. ToolProvider.findFirst("javac")a retourné unOptionalet le code a géré les deux branches. Les recherches de service sont par nature « peut être absent » — un runtime minimal pourrait ne livrer aucun fournisseur d'outil — donc l'API vous oblige à prévoir le cas vide plutôt qu'à supposer qu'une implémentation existe.- L'exécution de
javac --versionvia le fournisseur chargé prouve que l'objet est pleinement fonctionnel, atteint uniquement via le contrat de service. Le consommateur a invoqué un comportement réel sans dépendance au moment de la compilation sur les classes du compilateur. ServiceLoaderinstancie paresseusement et seulement ce que vous itérez ; dans une configuration réelle à trois modules, lemodule-infodu consommateur devrait déclareruses java.util.spi.ToolProvider;pour que l'appel soit autorisé. Les propres modules du JDK le déclarent déjà, c'est pourquoi cela s'exécute sans modification.
Pourquoi utiliser les services
- Architectures de plugins — déposez un JAR fournisseur sur le chemin de modules pour étendre une application.
- Implémentations optionnelles — choisissez un pilote SSL, de journalisation ou de base de données à l'exécution selon le module fournisseur présent.
- Inversion de dépendance — le module consommateur de haut niveau dépend d'un module interface, jamais des fournisseurs de bas niveau, de sorte que les flèches de dépendance pointent toutes vers le contrat stable.
C'est le même mécanisme que le JDK utilise pour DriverManager, les fournisseurs de jeux de caractères et les chronologies java.time. Le dernier chapitre de cette partie, Migrating to Java Modules, rassemble tout : comment déplacer une application classpath existante vers le chemin de modules une étape à la fois.