domingo, 2 de junio de 2019

Java SPI: que es y como funciona


Hoy vengo con una caracteristica de Java bastante antigua pero que pasa algo más desapercibidad. Y de la cual me di cuenta buscando información sobre custom mediators de WSO2. Aunque en un principio no la usemos directamente, es una característica que se encuentra detras de otras que si usamos.

Para entender su funcionamiento, debemos aclarar terminos claves:
  • Service: Un conjunto de interfaces de programación y clases que proporcionan acceso a alguna funcionalidad o característica específica de la aplicación. Como podría ser JDBC. 
  • Service Provider Interface: Será aquella o aquellas interfaces públicas que definen el servicio anterior. Actuando de punto de entrada para la implentación del servicio. Como podría ser la clase  java.sql.Driver de JDBC. 
  • Service Provider: Sería una implementación específica de ese SPI, la cual nos permiten extender el funcionamiento. Como podrían ser la implementación de MySQL del driver JDBC, com.mysql.jdbc.Driver
Todo ello junto a al ServiceLoader, nos brindará la posibilidad de buscar y cargar todos los services providers que se encuentren dentro de nuestro classpath. Dentro de JDBC, este papel sería realizado por la clase java.sql.DriverManager

¿Que quiere decir todo esto?. Pues que Java nos provee un método sencillo para cargar y utilizar dinamicamente implementaciones de determinados servicios en nuestras aplicaciones. Con el caso de JDBC sería cualquier driver, MySQL, Oracle, MariaDB, etc. 

Ahora veremos su funcionamiento. Por un lado creamos una librería que incluya un servicio, su correspondiente SPI. En casos en los que el servicio sea una única interfaz actuará también como SPI.

package es.home.example.greeting.spi;
public interface GreetingService {
    String hello();
    String getLanguage();
}

Esta librería también incluirá el service loader a modo de utilidad. 

public final class GreetingHelper {
    private static ServiceLoader<GreetingService> loader;
    static {
        loader = ServiceLoader.load(GreetingService.class);
    }
    private GreetingHelper() {
        super();
    }
    public static String sayHello(final String language) {
        String hello = null;
        try {
            Iterator<GreetingService> greetings = loader.iterator();
            while (hello == null && greetings.hasNext()) {
                GreetingService d = greetings.next();
                if (d.getLanguage().equals(language)) {
                    hello = d.hello();
                }
            }
        } catch (ServiceConfigurationError serviceError) {
            hello = null;
            serviceError.printStackTrace();
        }
        return hello;
    }
}

En otra librería aparte creamos diferentes services providers de ese servicio. 

public class GreetingEspServiceProvider implements GreetingService {
    @Override
    public String hello() {
        return "hola";
    }
    @Override
    public String getLanguage() {
        return "esp";
    }
}

Y aquí viene una de las partes importantes. En esta librería tendremos que crear un fichero, con el nombre del SPI, paquete incluido. Y dentro de el, incluiremos los nombres completos de los services providers de esa librería. Este fichero debe ir dentro de la carpeta 'META-INF/services/'. En este caso el fichero se llamará es.home.example.greeting.spi.GreetingService.

#Providers for Greeting Service
es.home.example.greeting.multi.spi.GreetingEngServiceProvider
es.home.example.greeting.multi.spi.GreetingEspServiceProvider
es.home.example.greeting.multi.spi.GreetingItaServiceProvider

Ya solo nos queda crear la clase que usara estos services providers, por ejemplo:


@GET
@Path("/hello/{language}")
public String hello(final String language) {
    return GreetingHelper.sayHello(language);
}

Podemos chequear su funcionamiento con unas simples pruebas

@Test
public void methodOK() {
    assertEquals("ciao", sample.hello("ita"));
    assertEquals("hello", sample.hello("eng"));
    assertEquals("hola", sample.hello("esp"));
    assertEquals(null, sample.hello("fra"));
}

Más información en su página oficial

No hay comentarios:

Publicar un comentario