El ServiceLoader no puede localizar proveedores cuando el módulo que lo contiene no declara una directiva provides ... with en su descriptor module-info.java. El Java Platform Module System (JPMS) impone una fuerte encapsulación por defecto, lo que impide que el ServiceLoader (ubicado en java.base) acceda a clases en paquetes que no están exportados o abiertos. La directiva provides actúa como una declaración contractual que otorga al ServiceLoader acceso reflexivo privilegiado para instanciar la clase del proveedor especificado, eludiendo las restricciones normales de accesibilidad de paquetes sin requerir que el paquete sea exportado a todos los módulos.
Contexto: Un sistema CRM empresarial heredado estaba siendo migrado de Java 8 a Java 17. El objetivo era modularizar la arquitectura monolítica en dominios distintos: crm-core, crm-email y crm-api. El módulo crm-email contenía una implementación de la interfaz NotificationService definida en crm-api.
Después de la migración, la aplicación lanzó ServiceConfigurationError durante el arranque. Esto ocurrió a pesar de que la clase EmailNotificationService era pública y los archivos JAR estaban presentes en la ruta del módulo. El rastreo de pila indicó que no se encontraron proveedores para el tipo de servicio, lo que causó que el subsistema de notificaciones fallara en la inicialización.
Problema: El equipo de desarrollo asumió que la visibilidad pública de la clase de implementación era suficiente. Esto reflejaba supuestos de la era de classpath donde las clases públicas eran globalmente visibles. Sin embargo, JPMS impide que el ServiceLoader acceda a clases en paquetes no exportados de otros módulos. El módulo crm-email no exportaba el paquete com.crm.email.internal. Críticamente, el module-info.java carecía de la declaración provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService. Como resultado, el ServiceLoader no pudo localizar o instanciar el proveedor, ya que el sistema de módulos trató la implementación como un detalle interno encapsulado.
Soluciones consideradas:
Exportar el paquete: Agregar exports com.crm.email.internal; al descriptor del módulo crm-email. Este enfoque fue rechazado porque expondría detalles de implementación interna a todos los demás módulos. Violaba la encapsulación y creaba un acoplamiento estrecho que el sistema de módulos estaba diseñado para prevenir.
Abrir el paquete para reflexión: Utilizar opens com.crm.email.internal; o específicamente opens com.crm.email.internal to java.base;. Si bien esto permite el acceso reflexivo, se consideró demasiado permisivo y semánticamente incorrecto. Señala que el paquete está sujeto a reflexión profunda en general, en lugar de proporcionar un servicio a través de un mecanismo controlado.
Usar la directiva provides ... with: Agregar la declaración provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService; al module-info.java. Esta es la solución idiomática de JPMS. Declara explícitamente la relación de servicio y otorga al ServiceLoader los derechos de acceso necesarios para instanciar la clase mientras mantiene la estricta encapsulación.
Solución elegida: El equipo seleccionó la tercera opción. Este enfoque no requirió cambios en el código de implementación en sí. Preservó la visibilidad interna del paquete y hizo que la dependencia del servicio fuera explícita en los metadatos del módulo.
Resultado: La aplicación cargó exitosamente el EmailNotificationService en tiempo de ejecución. La frontera modular permaneció intacta, impidiendo que otros módulos instanciaran o dependieran directamente de las clases de implementación internas. El ServiceLoader pudo descubrir y proporcionar correctamente el servicio a través del contrato declarado.
¿Por qué el ServiceLoader requiere que la clase del proveedor posea un constructor público sin argumentos, y qué excepción específica se manifiesta si se viola este requisito?
El ServiceLoader instancia clases de proveedores a través de reflexión usando Class.getConstructor().newInstance(). Esto requiere estrictamente un constructor público sin argumentos. Si este constructor está ausente, o si no es público, el ServiceLoader lanza un ServiceConfigurationError. Este error suele estar envuelto en un NoSuchMethodException o IllegalAccessException durante la iteración. Los candidatos a menudo pasan por alto que este constructor debe ser proporcionado explícitamente si se definen otros constructores. También pasan por alto que la instanciación ocurre de manera perezosa cuando se invoca Iterator.next(), no durante la llamada inicial a ServiceLoader.load().
¿Cómo maneja el mecanismo ServiceLoader las clases de proveedores ubicadas en módulos sin nombre cuando la interfaz de servicio está definida dentro de un módulo con nombre?
Cuando una interfaz de servicio reside en un módulo con nombre pero la implementación está en un módulo sin nombre (el classpath), el ServiceLoader aún puede localizar al proveedor. Los módulos sin nombre leen implícitamente todos los módulos con nombre, y todos los módulos con nombre leen implícitamente los módulos sin nombre. Sin embargo, la clase del proveedor aún debe ser pública con un constructor público sin argumentos. La concepción errónea común es que la fuerte encapsulación impide completamente este escenario. En realidad, el módulo sin nombre actúa como una capa de compatibilidad. Los proveedores en módulos sin nombre no pueden ser accedidos por código en módulos con nombre que no lean explícitamente el módulo sin nombre. Esto crea una restricción de accesibilidad direccional que los candidatos a menudo no consideran.
¿Qué distingue al método ServiceLoader.loadInstalled() de ServiceLoader.load() en términos de delegación de cargador de clases y visibilidad del proveedor?
ServiceLoader.loadInstalled() utiliza el cargador de clases del sistema (o el cargador de clases de plataforma en versiones modernas de JVM) para buscar proveedores. Restringe el descubrimiento al directorio de extensiones instaladas o a los módulos de plataforma. Ignora explícitamente a los proveedores en la ruta del módulo de la aplicación o classpath. En contraste, ServiceLoader.load() normalmente utiliza el cargador de clases de contexto del hilo o un cargador de clases especificado. Esto le permite descubrir proveedores a nivel de aplicación. Los candidatos a menudo confunden estos métodos, lo que lleva a fallos silenciosos en los que no se encuentran proveedores de aplicación. Esto sucede porque se utilizó loadInstalled() incorrectamente, esperando que se comportara como el método de carga estándar pero con una visibilidad más amplia.