JavaProgrammationDéveloppeur Java Senior

Sous quelle contrainte d'accessibilité spécifique du **JPMS** le mécanisme **ServiceLoader** échoue-t-il à localiser les implémentations de fournisseur situées dans des packages non exportés, malgré leur présence sur le chemin du module ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Le ServiceLoader échoue à localiser les fournisseurs lorsque le module contenant ne déclare pas un provides ... with dans son descripteur module-info.java. Le Java Platform Module System (JPMS) impose une forte encapsulation par défaut, empêchant le ServiceLoader (localisé dans java.base) d'accéder aux classes dans les packages qui ne sont pas exportés ou ouverts. La directive provides agit comme une déclaration contractuelle qui accorde au ServiceLoader un accès réflexif privilégié pour instancier la classe de fournisseur spécifiée, contournant les contraintes d'accessibilité normales des packages sans nécessiter l'exportation du package à tous les modules.

Situation de la vie réelle

Contexte : Un ancien système CRM d'entreprise était en cours de migration de Java 8 à Java 17. L'objectif était de modulariser l'architecture monolithique en domaines distincts : crm-core, crm-email, et crm-api. Le module crm-email contenait une implémentation de l'interface NotificationService définie dans crm-api.

Après migration, l'application a généré une ServiceConfigurationError lors du démarrage. Cela s'est produit malgré le fait que la classe EmailNotificationService soit publique et que les fichiers JAR soient présents sur le chemin du module. La trace de la pile indiquait qu'aucun fournisseur n'avait été trouvé pour le type de service, provoquant l'échec de l'initialisation du sous-système de notification.

Problème : L'équipe de développement supposait que la visibilité publique de la classe d'implémentation était suffisante. Cela reflétait les suppositions de l'époque du classpath où les classes publiques étaient visibles globalement. Cependant, le JPMS empêche le ServiceLoader d'accéder aux classes dans des packages non exportés d'autres modules. Le module crm-email n'exportait pas le package com.crm.email.internal. De plus, le module-info.java ne contenait pas la déclaration provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService. Par conséquent, le ServiceLoader n'a pas pu localiser ou instancier le fournisseur, car le système de modules traitait l'implémentation comme un détail interne encapsulé.

Solutions envisagées :

  • Exporter le package : Ajouter exports com.crm.email.internal; au descripteur du module crm-email. Cette approche a été rejetée car elle exposerait les détails d'implémentation internes à tous les autres modules. Cela violait l'encapsulation et créait un couplage étroit que le système de modules était conçu pour empêcher.

  • Ouvrir le package pour la réflexion : Utiliser opens com.crm.email.internal; ou spécifiquement opens com.crm.email.internal to java.base;. Bien que cela permette un accès réflexif, cela a été jugé trop permissif et sémantiquement incorrect. Cela implique que le package est soumis à une réflexion profonde de manière générale, plutôt que de fournir un service par un mécanisme contrôlé.

  • Utiliser la directive provides ... with : Ajouter la déclaration provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService; au module-info.java. C'est la solution idiomatique du JPMS. Elle déclare explicitement la relation de service et accorde au ServiceLoader les droits d'accès nécessaires pour instancier la classe tout en maintenant une stricte encapsulation.

Solution choisie : L'équipe a sélectionné la troisième option. Cette approche n'exigeait aucun changement dans le code d'implémentation lui-même. Elle préservait la visibilité interne du package et rendait la dépendance de service explicite dans les métadonnées du module.

Résultat : L'application a réussi à charger le EmailNotificationService au moment de l'exécution. La frontière modulaire est restée intacte, empêchant d'autres modules d'instancier ou de dépendre directement des classes d'implémentation internes. Le ServiceLoader a pu découvrir correctement et pourvoir le service via le contrat déclaré.

Ce que les candidats oublient souvent

Pourquoi le ServiceLoader exige-t-il que la classe fournisseur possède un constructeur public sans argument, et quelle exception spécifique se manifeste si cette contrainte est violée ?

Le ServiceLoader instancie les classes de fournisseur via réflexion en utilisant Class.getConstructor().newInstance(). Cela nécessite strictement un constructeur public sans argument. Si ce constructeur est absent ou s'il n'est pas public, le ServiceLoader lance une ServiceConfigurationError. Cette erreur est généralement enveloppée autour d'une NoSuchMethodException ou IllegalAccessException lors de la traversée de l'itérateur. Les candidats oublient souvent que ce constructeur doit être explicitement fourni si d'autres constructeurs sont définis. Ils manquent également que l'instanciation se produit de manière paresseuse lorsque Iterator.next() est invoqué, et non lors de l'appel initial ServiceLoader.load().

Comment le mécanisme ServiceLoader gère-t-il les classes de fournisseur situées dans des modules sans nom lorsque l'interface de service est définie dans un module nommé ?

Lorsque l'interface de service réside dans un module nommé mais que l'implémentation se trouve dans un module sans nom (le classpath), le ServiceLoader peut toujours localiser le fournisseur. Les modules sans nom lisent implicitement tous les modules nommés, et tous les modules nommés lisent implicitement les modules sans nom. Cependant, la classe fournisseur doit toujours être publique avec un constructeur public sans argument. La conception commune est que la forte encapsulation empêche entièrement ce scénario. En réalité, le module sans nom agit comme une couche de compatibilité. Les fournisseurs dans des modules sans nom ne peuvent pas être accessibles par du code dans des modules nommés qui ne lisent pas explicitement le module sans nom. Cela crée une contrainte d'accessibilité directionnelle que les candidats oublient souvent de prendre en compte.

En quoi la méthode ServiceLoader.loadInstalled() se distingue-t-elle de ServiceLoader.load() en termes de délégation de chargeur de classe et de visibilité des fournisseurs ?

ServiceLoader.loadInstalled() utilise le chargeur de classe système (ou le chargeur de classe de plateforme dans les versions modernes de JVM) pour rechercher des fournisseurs. Cela restreint la découverte au répertoire des extensions installées ou aux modules de plateforme. Cela ignore explicitement les fournisseurs sur le chemin du module de l'application ou le classpath. En revanche, ServiceLoader.load() utilise généralement le chargeur de classe de contexte de thread ou un chargeur de classe spécifié. Cela lui permet de découvrir les fournisseurs de niveau application. Les candidats confondent souvent ces méthodes, ce qui entraîne des échecs silencieux où les fournisseurs d'application ne sont pas trouvés. Cela se produit car loadInstalled() a été utilisé incorrectement, s'attendant à ce qu'il se comporte comme la méthode de chargement standard mais avec une visibilité plus large.