Registrar servicios Java con ServiceLoader

De ChuWiki

Vamos a ver cómo registrar servicios en java y qué utilidad puede tener. Tienes el ejemplo concreto en service_loader y también el fichero META-INF/services/com.chuidiang.ejemplos.service_loader.IfzService que se menciona por ahí abajo.

La necesidad[editar]

Imagina que tienes una clase A que utiliza una clase B. Por ejemplo, una clase A cualquiera que tiene que llamar a un método sayHello() de una clase B. Para que A pueda ver a B y llamar a su método, la opción más fácil es esta

public class A {
    public void unMetodoDeA() {
        B b = new B();
        b.sayHello();
    }
}

Imagina por ejemplo que B, en su método sayHello() saca por pantalla "Hola Mundo" y nosotros ahora quisiéramos que en vez de por pantalla lo sacara por impresora. Podemos hacer una clase C que tenga un método sayHello() y saque ese "Hola mundo" por impresora. Solo tenemos que tocar el código de A para que haga new de C en vez de hacerlo de B.

Esto está bien para pequeños programas que nos hagamos, pero imagina un proyecto más complejo, donde la clase A se ha metido en un jar ya compilado, que en un futuro podrías mandar el "Hola Mundo" también en un email, más en el futuro enviarlo a un web service o publicarlo en twitter. Tocar el código de A cada vez no mola. Y menos si tienes varios proyectos distintos en el que en uno se saca por pantalla, en otro se envía por email y en otro se guarda el hola mundo en un fichero.

Como solucionarlo[editar]

Para este tipo de problemas hay dos soluciones. Las dos pasan por hacer una interface java con el método sayHello(), de forma que B, C y demás futuras clases implementen ese método.

package com.chuidiang.ejemplos.service_loader;

public interface IfzService {
   void sayHello();
}

Y ahora, en vez de hacer A el new de esa clase, hay que inventar alguna forma de que la clase A obtenga una instancia concreta de IfzService. En cada proyecto concreto la que sea, la que saca por pantalla, la que manda por email o la que lo pone en un panel luminoso.

Y aquí es donde hay dos opciones:

  • Que en el constructor de A o con un método setService(IfzService) alguien le pase la instancia concreto. Este mecanismo se conoce como inyección de dependencias
  • Que sea A cuando necesite llamar al servicio el que llame a alguna clase que le devuelva la instancia concreta. Este mecanismo se conoce como localizador de servicios.

Tienes detalles de ambos mecanismos en Inversión de Control. En este ejemplo vamos con el localizador de servicios, pero usando la clase que java nos proporciona ServiceLoader

ServiceLoader[editar]

ServiceLoader es fácil de usar. Solo son dos pasos.

Registro del servicio[editar]

El primer paso es que las clases que implementan el servicio (B, C, etc) estén registradas en ServiceLoader. El mecanismo que ofrece java para esto es que en el jar donde estén estas clases, haya un fichero cuyo nombre sea el de la interface del servicio (con su paquete) en un directorio específico.

META-INF/services/com.chuidiang.ejemplos.service_loader.IfzService

Es decir, en el directorio META-INF dentro del jar, un subdirectorio services y en él un fichero de nombre com.chuidiang.ejemplos.service_loader.IfzService, que coincide con el paquete y nombre de clase de la IfzService que hicimos arriba.

Dentro de este fichero, las clases java que haya en el jar e implementen ese servicio. En nuestro ejemplo, si vamos a nombres más molones que B, C y las clase son AService y AnotherService, el contenido del fichero sería

com.chuidiang.ejemplos.service_loader.AService  #Ejemplo servicio
com.chuidiang.ejemplos.service_loader.AnotherService  #Ejemplo otro servicio

Con # podemos poner detrás comentarios si queremos.

Bien, con esto, cuando arranquemos nuestra aplicación, java busca dentro de los jar el directorio META-INF/services e irá registrando todos los servicios que encuentre. El nombre del fichero indica la interface que implementan los servicios y dentro del fichero los nombres de las clases que implementan ese servicio.

Uso del servicio[editar]

Ahora, la clase A cuando quiera usar el servicio, tiene que hacer esto

package com.chuidiang.ejemplos.service_loader;

import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * @author fjabellan
 * @date 31/10/2020
 */
public class ServiceLoaderMain {
    public static void main(String[] args) {
        ServiceLoader<IfzService> serviceLoader = ServiceLoader.load(IfzService.class);
        final Iterator<IfzService> iterator = serviceLoader.iterator();
        iterator.forEachRemaining(service->{
            System.out.print(service + ": ");
            service.sayHello();
        });
    }
}

Bueno, ya no la hemos llamado A, le hemos puesto un nombre más serio, ServiceLoaderMain ni más ni menos. Y es en el método main() donde buscamos y llamamos al servicio. Si te fijas:

  • Se llama a ServiceLoader.load(IfzService.class) pasándole la interfaz, esto nos devuelve un ServiceLoader específico para ese servicio (esa interfaz).
  • Se obtiene un iterador, puede haber como en nuestro ejemplo, varias clases que implementen la IfzService.
  • Se va llamando al metodo de todos ellos en un bucle.

Así que si solo tenemos registrado un servicio que saque por pantalla, nuestra clase sacará Hola Mundo por pantalla. Si el servicio registrado lo mando por email, lo mandará por email. Y si ambos están registrados, hará ambas cosas.

La gracia final: separar en jars[editar]

A esto le podemos sacar mucho más partido si separamos cada implementación de servicio en un jar separado. Por ejemplo, podemos tener los siguientes jar

  • hola-email.jar
  • hola-pantalla.jar
  • hola-facebook.jar
  • ...

Cada jar tendría sólo una de las clases que implementan IfzService y dentro del jar,su fichero META-INF/services/com.chuidiang.ejemplos.service_loader.IfzService sólo registraría la clase que tiene. De esta forma, si en nuestro proyecto no añadimos ninguno de estos jar como dependencia, la clase ServiceLoaderMain se quedará a dos velas y no hará nada, porque no encontrará ningún servicio que implemente IfzService, si metemos uno de los jar, econtrará un servicio, si metemos dos jar, econtrará dos servicios.

Esto nos permite que nuestra clase ServiceLoaderMain nos sirva tal cual, sin tocarla, para todos nuestros proyectos y en cada proyecto concreto, añadimos el jar de servicio que queramos.