JavaprogramowanieStarszy programista Java

Pod jakim konkretnym ograniczeniem dostępności JPMS mechanizm ServiceLoader nie może zlokalizować implementacji dostawców znajdujących się w nieeksportowanych pakietach, mimo że są obecne na ścieżce modułów?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie

Mechanizm ServiceLoader nie może zlokalizować dostawców, gdy moduł zawierający nie deklaruje dyrektywy provides ... with w swoim opisie module-info.java. Java Platform Module System (JPMS) domyślnie egzekwuje silną enkapsulację, uniemożliwiając ServiceLoader (znajdujący się w java.base) dostęp do klas w pakietach, które nie są eksportowane ani otwarte. Dyrektywa provides działa jako deklaracja umowy, która przyznaje ServiceLoader uprawniony dostęp refleksyjny do instancjonowania określonej klasy dostawcy, omijając normalne ograniczenia dostępności pakietu bez konieczności eksportowania pakietu do wszystkich modułów.

Sytuacja z życia

Kontekst: Legacyny system CRM w przedsiębiorstwie był migrowany z Java 8 do Java 17. Celem było zmodularyzowanie monolitycznej architektury na wyraźne obszary: crm-core, crm-email, i crm-api. Moduł crm-email zawierał implementację interfejsu NotificationService zdefiniowanego w crm-api.

Po migracji aplikacja zgłaszała ServiceConfigurationError podczas uruchamiania. Miało to miejsce mimo że klasa EmailNotificationService była publiczna, a pliki JAR były obecne na ścieżce modułów. Stos śladów wskazywał, że nie znaleziono dostawców dla typu usługi, co spowodowało, że podsystem powiadomień nie mógł się zainicjalizować.

Problem: Zespół deweloperski zakładał, że publiczna widoczność klasy implementacji była wystarczająca. Odzwierciedlało to założenia ery classpath, gdzie publiczne klasy były powszechnie widoczne. Jednak JPMS uniemożliwia ServiceLoader dostęp do klas w nieeksportowanych pakietach innych modułów. Moduł crm-email nie eksponował pakietu com.crm.email.internal. Krytyczne było to, że plik module-info.java nie zawierał deklaracji provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService. W związku z tym ServiceLoader nie mógł zlokalizować ani zainstancjonować dostawcy, ponieważ system modułów traktował implementację jako wewnętrzny szczegół.

Rozważane rozwiązania:

  • Eksportowanie pakietu: Dodanie exports com.crm.email.internal; do opisu modułu crm-email. To podejście zostało odrzucone, ponieważ ujawniałoby wewnętrzne szczegóły implementacji wszystkim innym modułom. Naruszało to enkapsulację i tworzyło silne powiązania, których system modułów został zaprojektowany, aby unikać.

  • Otwarcie pakietu dla refleksji: Użycie opens com.crm.email.internal; lub konkretnie opens com.crm.email.internal to java.base;. Mimo że umożliwia to dostęp refleksyjny, uznano to za zbyt liberalne i semantycznie niepoprawne. Sygnotyzuje to, że pakiet jest podatny na głęboką refleksję ogólnie, a nie specyficznie zapewniając usługę przez kontrolowany mechanizm.

  • Użycie dyrektywy provides ... with: Dodanie deklaracji provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService; do module-info.java. To jest idiomatyczne rozwiązanie JPMS. Wyraźnie deklaruje relację usługi i przyznaje ServiceLoader niezbędne prawa dostępu do instancjonowania klasy przy jednoczesnym zachowaniu ścisłej enkapsulacji.

Wybrane rozwiązanie: Zespół wybrał trzecią opcję. To podejście nie wymagało żadnych zmian w samej implementacji kodu. Utrzymywało wewnętrzną widoczność pakietu i czyniło zależność od usługi wyraźną w metadanych modułu.

Wynik: Aplikacja pomyślnie załadowała EmailNotificationService w czasie wykonywania. Granica modularności pozostała nietknięta, uniemożliwiając innym modułom bezpośrednie instancjonowanie lub poleganie na wewnętrznych klasach implementacyjnych. ServiceLoader mógł poprawnie odkryć i udostępnić usługę za pośrednictwem zadeklarowanej umowy.

Co często umyka kandydatom

Dlaczego ServiceLoader wymaga, aby klasa dostawcy miała publiczny konstruktor bezargumentowy, a jaki konkretna wyjątek manifestuje się, jeśli to ograniczenie jest naruszone?

ServiceLoader instancjonuje klasy dostawców za pomocą refleksji, używając Class.getConstructor().newInstance(). To wymaga ścisłego posiadania publicznego konstruktora bezargumentowego. Jeśli tego konstruktora brakuje, lub jeśli nie jest publiczny, ServiceLoader zgłasza błąd ServiceConfigurationError. Ten błąd zazwyczaj jest opakowany wokół NoSuchMethodException lub IllegalAccessException podczas przeszukiwania iteratora. Kandydaci często nie zauważają, że ten konstruktor musi być jawnie dostarczony, jeśli zdefiniowane są jakiekolwiek inne konstruktory. Przeoczyć mogą również, że instancjonowanie odbywa się leniwie, gdy wywoływana jest Iterator.next(), a nie podczas początkowego wywołania ServiceLoader.load().

Jak mechanizm ServiceLoader radzi sobie z klasami dostawców znajdującymi się w nieznanych modułach, gdy interfejs usługi jest zdefiniowany w nazwanym module?

Gdy interfejs usługi znajduje się w nazwanym module, ale implementacja w nieznanym module (classpath), ServiceLoader wciąż może zlokalizować dostawcę. Nieznane moduły domyślnie odczytują wszystkie nazwane moduły, a wszystkie nazwane moduły implicitnie odczytują nieznane moduły. Jednak klasa dostawcy musi nadal być publiczna i posiadać publiczny konstruktor bezargumentowy. Powszechnym nieporozumieniem jest to, że silna enkapsulacja całkowicie uniemożliwia ten scenariusz. W rzeczywistości nieznany moduł działa jako warstwa kompatybilności. Dostawcy w nieznanych modułach nie mogą być dostępni dla kodu w nazwanych modułach, które nie odczytują jawnie nieznanego modułu. Tworzy to kierunkowe ograniczenie dostępności, które kandydaci często nie biorą pod uwagę.

Co odróżnia metodę ServiceLoader.loadInstalled() od ServiceLoader.load() pod względem delegacji załadowania klasy i widoczności dostawców?

ServiceLoader.loadInstalled() używa systemowego załadowacza klas (lub załadowacza platformy w nowoczesnych wersjach JVM) do wyszukiwania dostawców. Ogranicza odkrywanie do zainstalowanego katalogu rozszerzeń lub modułów platformy. Wyraźnie ignoruje dostawców na ścieżce modułu aplikacji lub classpath. W przeciwieństwie do tego, ServiceLoader.load() zazwyczaj wykorzystuje załadowcza kontekstu wątku lub określony załadowca klas. Pozwala to odkrywać dostawców na poziomie aplikacji. Kandydaci często mylą te metody, co prowadzi do cichych błędów, gdy dostawcy aplikacji nie są znajdowani. Dzieje się tak, ponieważ loadInstalled() był używany nieprawidłowo, oczekując, że będzie zachowywał się jak standardowa metoda ładowania, ale z szerszą widocznością.