Der ServiceLoader kann Anbieter nicht finden, wenn das enthaltende Modul keinen provides ... with-Befehl in seiner module-info.java-Beschreibung deklariert. Das Java Platform Module System (JPMS) erzwingt standardmäßig eine starke Kapselung, die es dem ServiceLoader (im java.base) verhindert, auf Klassen in Paketen zuzugreifen, die nicht exportiert oder geöffnet sind. Die provides-Richtlinie fungiert als vertragliche Erklärung, die dem ServiceLoader privilegierten reflektiven Zugang ermöglicht, um die angegebene Anbieterkategorie zu instanziieren und die normalen Zugriffsbedingungen der Pakete zu umgehen, ohne dass das Paket für alle Module exportiert werden muss.
Kontext: Ein älteres Unternehmens-CRM-System wurde von Java 8 auf Java 17 migriert. Ziel war es, die monolithische Architektur in verschiedene Domänen zu modularisieren: crm-core, crm-email und crm-api. Das Modul crm-email enthielt eine Implementierung des Interfaces NotificationService, das in crm-api definiert war.
Nach der Migration warf die Anwendung während des Bootstrap-Vorgangs einen ServiceConfigurationError. Dies geschah trotz der Tatsache, dass die Klasse EmailNotificationService öffentlich war und sich die JAR-Dateien im Modulpfad befanden. Der Stacktrace zeigte an, dass keine Anbieter für den Diensttyp gefunden wurden, was zur fehlerhaften Initialisierung des Benachrichtigungssystems führte.
Problem: Das Entwicklungsteam ging davon aus, dass die öffentliche Sichtbarkeit der Implementierungsklasse ausreichend war. Dies spiegelte Annahmen aus der Classpath-Ära wider, in der öffentliche Klassen global sichtbar waren. Der JPMS hingegen verhindert, dass der ServiceLoader auf Klassen in nicht exportierten Paketen anderer Module zugreift. Das Modul crm-email exportierte das Paket com.crm.email.internal nicht. Kritisch war, dass die module-info.java die Erklärung provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService nicht enthielt. Folglich konnte der ServiceLoader den Anbieter nicht finden oder instanziieren, da das Modulsystem die Implementierung als ein gekapseltes internes Detail betrachtete.
Berücksichtigte Lösungen:
Exportieren des Pakets: Hinzufügen von exports com.crm.email.internal; zum Modul-Descriptor von crm-email. Dieser Ansatz wurde abgelehnt, da er interne Implementierungsdetails für alle anderen Module zugänglich machen würde. Es verletzte die Kapselung und erzeugte eine enge Kopplung, die das Modulsystem zu verhindern versuchte.
Öffnen des Pakets für Reflexion: Verwendung von opens com.crm.email.internal; oder spezifisch opens com.crm.email.internal to java.base;. Obwohl dies reflektiven Zugriff ermöglicht, wurde es als zu permissiv und semantisch inkorrekt erachtet. Es signalisiert, dass das Paket im Allgemeinen tiefgreifender Reflexion unterliegt, anstatt spezifisch einen Dienst über einen kontrollierten Mechanismus anzubieten.
Verwendung der provides ... with-Richtlinie: Hinzufügen der Erklärung provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService; zu der module-info.java. Dies ist die idiomatische JPMS-Lösung. Sie erklärt ausdrücklich die Dienstbeziehung und gewährt dem ServiceLoader die erforderlichen Zugriffsrechte, um die Klasse zu instanziieren, während die strenge Kapselung beibehalten wird.
Gewählte Lösung: Das Team wählte die dritte Option. Dieser Ansatz erforderte keine Änderungen am Implementierungscode selbst. Er bewahrte die interne Sichtbarkeit des Pakets und machte die Dienstabhängigkeit in den Moduldaten explizit.
Ergebnis: Die Anwendung lud zur Laufzeit erfolgreich den EmailNotificationService. Die modulare Grenze blieb intakt, was anderen Modulen verhinderte, direkt die internen Implementierungsklassen zu instanziieren oder davon abzuhängen. Der ServiceLoader konnte den Dienst über den deklarierten Vertrag korrekt entdecken und bereitstellen.
Warum erfordert der ServiceLoader, dass die Anbieterkategorie einen öffentlichen Konstruktor ohne Argumente besitzt, und welche spezifische Ausnahme tritt auf, wenn dieser Rahmen verletzt wird?
Der ServiceLoader instanziiert Anbieterkategorien über Reflexion mit Class.getConstructor().newInstance(). Dies erfordert strikt einen öffentlichen Konstruktor ohne Argumente. Wenn dieser Konstruktor fehlt oder nicht öffentlich ist, wirft der ServiceLoader einen ServiceConfigurationError. Dieser Fehler wird typischerweise mit einer NoSuchMethodException oder IllegalAccessException während der Iterator-Durchlauf umwickelt. Kandidaten übersehen häufig, dass dieser Konstruktor ausdrücklich bereitgestellt werden muss, wenn andere Konstruktoren definiert sind. Sie übersehen auch, dass die Instanziierung lazy erfolgt, wenn Iterator.next() aufgerufen wird, nicht während des initialen Aufrufs von ServiceLoader.load().
Wie behandelt der ServiceLoader Anbieterklassen, die sich in nicht benannten Modulen befinden, wenn das Dienstinterface innerhalb eines benannten Moduls definiert ist?
Wenn sich ein Dienstinterface in einem benannten Modul befindet, die Implementierung jedoch in einem nicht benannten Modul (dem Classpath), kann der ServiceLoader dennoch den Anbieter finden. Nicht benannte Module lesen implizit alle benannten Module, und alle benannten Module lesen implizit nicht benannte Module. Der Anbieterkategorie muss jedoch weiterhin öffentlich sein und einen öffentlichen Konstruktor ohne Argumente haben. Das gängige Missverständnis besteht darin, dass starke Kapselung dieses Szenario vollständig verhindert. In Wirklichkeit fungiert das nicht benannte Modul als Kompatibilitätsschicht. Anbieter in nicht benannten Modulen können von Code in benannten Modulen, der das nicht benannte Modul nicht explizit liest, nicht erreicht werden. Dies schafft eine gerichtete Zugriffsbeschränkung, die Kandidaten oft nicht berücksichtigen.
Was unterscheidet die Methode ServiceLoader.loadInstalled() von ServiceLoader.load() hinsichtlich der Klassenladerdelegation und der Sichtbarkeit der Anbieter?
ServiceLoader.loadInstalled() verwendet den Systemklassenlader (oder den Plattformklassenlader in modernen JVM-Versionen), um nach Anbietern zu suchen. Es beschränkt die Entdeckung auf das Verzeichnis installierter Erweiterungen oder Plattformmodule. Es ignoriert ausdrücklich Anbieter im Anwendungsmodulpfad oder Classpath. Im Gegensatz dazu verwendet ServiceLoader.load() typischerweise den Thread-Kontext-Klassenlader oder einen spezifischen Klassenlader. Dies ermöglicht es ihm, anwendungsbezogene Anbieter zu entdecken. Kandidaten verwechseln häufig diese Methoden, was zu stillen Fehlern führt, bei denen Anwendungsanbieter nicht gefunden werden. Dies geschieht, weil loadInstalled() fälschlicherweise verwendet wurde und erwartet wurde, dass es sich wie die Standardlade-Methode verhält, jedoch mit breiterer Sichtbarkeit.