Java Modules: Servicios
Patrón ServiceLoader en el sistema de módulos de Java usando las directivas uses y provides.
Un objetivo central de los módulos es el desacoplamiento: el código que usa una capacidad no debería nombrar la clase que la implementa. El Java Platform Module System (JPMS) convierte esto en una característica de primer orden con dos directivas — uses y provides — conectadas en tiempo de ejecución por ServiceLoader. Esta es la alternativa modular a la antigua convención de classpath que consistía en colocar un archivo META-INF/services en un JAR.
Este capítulo asume que ya sabes cómo escribir un descriptor module-info.java; aquí añadimos las directivas de servicio a él.
Los tres roles
Un servicio consta de tres partes, idealmente en tres módulos diferentes:
- La interfaz de servicio (o clase abstracta) — el contrato, p. ej.
PricingRule. Vive en un módulo que la exporta. - El consumidor — código que solicita implementaciones. Su módulo declara
uses com.acme.PricingRule;y llama aServiceLoader.load(PricingRule.class). - Uno o más proveedores — módulos de implementación. Cada uno declara
provides com.acme.PricingRule with com.acme.impl.StandardPricing;.
El consumidor nunca importa StandardPricing. Solo conoce la interfaz. Añade un nuevo módulo proveedor a la ruta de módulos y el consumidor lo detecta automáticamente — sin recompilación, sin cambios en el código.
Las directivas en module-info.java
// module com.acme.api
module com.acme.api {
exports com.acme; // export the PricingRule interface
}
// module com.acme.app (the consumer)
module com.acme.app {
requires com.acme.api;
uses com.acme.PricingRule; // "I will ServiceLoader.load this"
}
// module com.acme.standard (a provider)
module com.acme.standard {
requires com.acme.api;
provides com.acme.PricingRule
with com.acme.standard.StandardPricing;
}uses le indica al resolutor que el consumidor consultará ese servicio, por lo que el módulo tiene permitido llamar a ServiceLoader.load. provides … with … registra una implementación. La clase proveedora debe tener un constructor público sin argumentos o un método estático público provider() que devuelva una instancia.
Consumir con ServiceLoader
ServiceLoader<PricingRule> loader = ServiceLoader.load(PricingRule.class);
for (PricingRule rule : loader) {
System.out.println(rule.describe());
}
// or pick the first available
PricingRule rule = ServiceLoader.load(PricingRule.class)
.findFirst()
.orElseThrow();ServiceLoader es perezoso — cada proveedor se instancia solo cuando el iterador llega a él — y almacena en caché las instancias. Implementa Iterable, por lo que un bucle for-each recorre todos los proveedores registrados.
Un ejemplo práctico: ServiceLoader sobre un servicio real del JDK
El JDK ya incluye un servicio que puedes cargar sin construir tres módulos: java.util.spi.ToolProvider. El compilador (javac), la herramienta JAR (jar) y otros están registrados como proveedores de esa interfaz dentro de los módulos del JDK. Este programa los carga mediante ServiceLoader — exactamente el código consumidor anterior, contra un servicio que ya está configurado.
Qué observar en la ejecución:
- El bucle for-each recorrió cada
ToolProviderque el runtime registró (normalmentejavac,jar,javadoc, …) sin que el programa importara nunca una de esas clases de implementación. Ese es el propósito de los servicios: el consumidor depende deToolProvider, la interfaz, y descubre proveedores concretos en tiempo de ejecución. - Cada proveedor imprimió un nombre de clase de implementación diferente aunque comparten una misma interfaz. Los módulos del JDK declararon
provides java.util.spi.ToolProvider with …en sus descriptores;ServiceLoaderlos recopiló todos. Añadir otro módulo proveedor haría que apareciera en este mismo bucle sin ningún cambio aquí. ToolProvider.findFirst("javac")devolvió unOptionaly el código manejó ambas ramas. Las búsquedas de servicios son inherentemente "podría estar ausente" — un runtime mínimo podría no incluir proveedores de herramientas — por lo que la API te obliga a planificar para el caso vacío en lugar de asumir que existe una implementación.- Ejecutar
javac --versiona través del proveedor cargado demuestra que el objeto es completamente funcional, alcanzado puramente a través del contrato de servicio. El consumidor invocó comportamiento real sin una dependencia en tiempo de compilación sobre las clases del compilador. ServiceLoaderinstancia de forma perezosa y solo lo que iteras; en un montaje real de tres módulos, elmodule-infodel consumidor necesitaríauses java.util.spi.ToolProvider;para que la llamada esté permitida. Los propios módulos del JDK ya lo declaran, por eso esto se ejecuta sin cambios.
Por qué usar servicios
- Arquitecturas de plugins — coloca un JAR proveedor en la ruta de módulos para extender una aplicación.
- Implementaciones opcionales — elige un SSL, logging o controlador de base de datos en tiempo de ejecución según qué módulo proveedor esté presente.
- Inversión de dependencia — el módulo consumidor de alto nivel depende de un módulo de interfaz, nunca de los proveedores de bajo nivel, por lo que las flechas de dependencia apuntan todas hacia el contrato estable.
Este es el mismo mecanismo que el JDK usa para DriverManager, proveedores de charset y las cronologías de java.time. El capítulo final de esta parte, Migrating to Java Modules, lo une todo: cómo mover una aplicación de classpath existente a la ruta de módulos paso a paso.