Il ServiceLoader non riesce a localizzare i provider quando il modulo contenitore non dichiara una direttiva provides ... with nel suo descrittore module-info.java. Il Java Platform Module System (JPMS) applica una forte incapsulamento per impostazione predefinita, impedendo al ServiceLoader (situato in java.base) di accedere a classi in pacchetti che non sono esportati o aperti. La direttiva provides funge da dichiarazione contrattuale che concede al ServiceLoader accesso riflessivo privilegiato per istanziare la classe provider specificata, bypassando le normali restrizioni di accessibilità del pacchetto senza richiedere che il pacchetto sia esportato a tutti i moduli.
Contesto: Un sistema CRM aziendale legacy stava venendo migrato da Java 8 a Java 17. L'obiettivo era quello di modularizzare l'architettura monolitica in domini distinti: crm-core, crm-email, e crm-api. Il modulo crm-email conteneva un'implementazione dell'interfaccia NotificationService definita in crm-api.
Dopo la migrazione, l'applicazione ha generato un ServiceConfigurationError durante il bootstrap. Questo è accaduto nonostante la classe EmailNotificationService fosse pubblica e i file JAR fossero presenti nel percorso del modulo. Lo stack trace indicava che non erano stati trovati provider per il tipo di servizio, causando il fallimento dell'inizializzazione del sottosistema delle notifiche.
Problema: Il team di sviluppo presumeva che la visibilità pubblica della classe di implementazione fosse sufficiente. Questo rifletteva assunzioni dell'era del classpath in cui le classi pubbliche erano globalmente visibili. Tuttavia, il JPMS impedisce al ServiceLoader di accedere a classi in pacchetti non esportati di altri moduli. Il modulo crm-email non esportava il pacchetto com.crm.email.internal. Criticamente, il module-info.java mancava della dichiarazione provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService. Di conseguenza, il ServiceLoader non poteva localizzare o istanziare il provider, poiché il sistema dei moduli trattava l'implementazione come un dettaglio interno incapsulato.
Soluzioni considerate:
Esportazione del pacchetto: Aggiungere exports com.crm.email.internal; al descrittore del modulo crm-email. Questo approccio è stato respinto poiché esponeva i dettagli di implementazione interni a tutti gli altri moduli. Violava l'incapsulamento e creava un accoppiamento stretto che il sistema dei moduli era progettato per prevenire.
Apertura del pacchetto per riflessione: Utilizzare opens com.crm.email.internal; o specificamente opens com.crm.email.internal to java.base;. Anche se questo consente l'accesso riflessivo, è stato considerato eccessivamente permissivo e semanticamente errato. Indica che il pacchetto è soggetto a profonda riflessione generalmente, piuttosto che fornire specificamente un servizio attraverso un meccanismo controllato.
Utilizzo della direttiva provides ... with: Aggiungere la dichiarazione provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService; al module-info.java. Questa è la soluzione idiomatica JPMS. Dichiara esplicitamente la relazione di servizio e concede al ServiceLoader i diritti di accesso necessari per istanziare la classe mantenendo una forte incapsulamento.
Soluzione scelta: Il team ha selezionato la terza opzione. Questo approccio non ha richiesto cambiamenti al codice di implementazione stesso. Ha preservato la visibilità interna del pacchetto e ha reso esplicita la dipendenza dal servizio nei metadati del modulo.
Risultato: L'applicazione ha caricato correttamente il EmailNotificationService durante il runtime. Il confine modulare è rimasto intatto, impedendo ad altri moduli di istanziare o dipendere direttamente dalle classi di implementazione interne. Il ServiceLoader ha potuto correttamente scoprire e fornire il servizio attraverso il contratto dichiarato.
Perché il ServiceLoader richiede che la classe provider possieda un costruttore pubblico senza argomenti, e quale specifica eccezione si manifesta se questo vincolo è violato?
Il ServiceLoader istanzia classi provider tramite riflessione utilizzando Class.getConstructor().newInstance(). Questo richiede rigorosamente un costruttore pubblico senza argomenti. Se questo costruttore è assente, o se non è pubblico, il ServiceLoader lancia un ServiceConfigurationError. Questo errore è tipicamente avvolto intorno a un NoSuchMethodException o IllegalAccessException durante la traversata dell'iteratore. I candidati trascurano frequentemente che questo costruttore deve essere esplicitamente fornito se sono definiti altri costruttori. Trascurano anche che l'istanza viene creata pigramente quando viene invocato Iterator.next(), non durante la chiamata iniziale a ServiceLoader.load().
Come gestisce il meccanismo ServiceLoader le classi provider situate in moduli non nominati quando l'interfaccia di servizio è definita all'interno di un modulo nominato?
Quando un'interfaccia di servizio risiede in un modulo nominato ma l'implementazione è in un modulo non nominato (il classpath), il ServiceLoader può comunque localizzare il provider. I moduli non nominati leggono implicitamente tutti i moduli nominati, e tutti i moduli nominati leggono implicitamente i moduli non nominati. Tuttavia, la classe provider deve comunque essere pubblica con un costruttore pubblico senza argomenti. La comune incomprensione è che una forte incapsulamento impedisce completamente questo scenario. In realtà, il modulo non nominato funge da strato di compatibilità. I provider nei moduli non nominati non possono essere accessibili dal codice nei moduli nominati che non leggono esplicitamente il modulo non nominato. Questo crea un vincolo di accessibilità direzionale che i candidati spesso trascurano di considerare.
Cosa distingue il metodo ServiceLoader.loadInstalled() da ServiceLoader.load() in termini di delega del caricatore di classi e visibilità dei provider?
ServiceLoader.loadInstalled() utilizza il caricatore di classi di sistema (o caricatore di classi piattaforma nelle versioni moderne di JVM) per cercare i provider. Limita la scoperta alla directory delle estensioni installate o ai moduli di piattaforma. Ignora esplicitamente i provider presenti nel percorso del modulo applicativo o nel classpath. Al contrario, ServiceLoader.load() utilizza tipicamente il caricatore di classi del contesto del thread o un caricatore di classi specificato. Questo gli consente di scoprire i provider a livello di applicazione. I candidati spesso confondono questi metodi, portando a fallimenti silenziosi in cui i provider dell'applicazione non vengono trovati. Questo accade perché loadInstalled() è stato usato in modo errato, aspettandosi che si comportasse come il metodo di caricamento standard ma con visibilità più ampia.