Aplicación Java extensible con la clase ServiceLoader

Escrito por el .
blog-stack java planeta-codigo programacion
Comentarios

Java ofrece un mecanismo incluido en el propio JDK para hacer las aplicaciones extensibles o ampliables en un momento posterior al de desarrollo. La clase ServiceLoader permite obtener las implementaciones definidas en el classpath de una determinada interfaz. En este artículo explico esta clase y muestro un ejemplo sencillo de como usarla.

Java

Puede que al desarrollar una aplicación necesitamos que esta sea extensible, esto significa que en el momento de desarrollo no conocemos las implementaciones de un determinado servicio que se proporcionarán en un futuro. Un servicio no es más que la implementación de una determinada interfaz que definimos en el momento de desarrollo. Java con la clase ServiceLoader proporciona un mecanismo estándar e incorporado en el JDK para cargar servicios con alguna propiedad interesante.

A través de la clase ServiceLoader y con su método estático load cargamos los servicios que implementen una determinada interfaz, en el parámetro de tipo Class indicamos la interfaz del servicio. Por ejemplo, supongamos que tenemos la siguiente definición de servicio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.serviceloader;

import java.util.Locale;

interface Saludador {
    /**
     * Retorna el idioma en el que este servicio saluda.
     */
    Locale getLocale();

    /**
     * Imprime en la consola un mensaje de saludo.
     */
    void saluda(String nombre);
}

En el momento de desarrollar esta aplicación podemos definir unos cuantos servicios que implementen la interfaz Saludador pero deseamos que en futuro podamos o un tercero pueda añadir más servicios para otros locales. La implementación de estos servicios en Español, Inglés y Euskera sería:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.github.picodotdev.serviceloader;

import java.util.Locale;

public class EspanolSaludador implements Saludador {
    public Locale getLocale(){
        return new Locale("es", "ES");
    }

    public void saluda(String nombre) {
        System.out.printf("¡Hola %s!\n", nombre);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.github.picodotdev.serviceloader;

import java.util.Locale;

public class InglesSaludador implements Saludador {
    public Locale getLocale(){
        return new Locale("en", "GB");
    }

    public void saluda(String nombre) {
        System.out.printf("Hello %s!\n", nombre);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.github.picodotdev.serviceloader;

import java.util.Locale;

public class EuskeraSaludador implements Saludador {
    public Locale getLocale(){
        return new Locale("eu", "ES");
    }

    public void saluda(String nombre) {
        System.out.printf("Kaixo %s!\n", nombre);
    }
}

Iniciando la aplicación podemos obtener los servicios disponibles para ser usados con el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package io.github.picodotdev.serviceloader;

import java.util.List;
import java.util.Locale;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class Main1 {
    public static void main(String[] args) {
        ServiceLoader<Saludador> loader = ServiceLoader.load(Saludador.class);
        Iterable<Saludador> iterable = () -> loader.iterator();
        Stream<Saludador> stream = StreamSupport.stream(iterable.spliterator(), false);
        List<Locale> locales = stream.map(i -> i.getLocale()).collect(Collectors.toList());
    
        System.out.printf("Idiomas soportados:%s\n", locales);
    }
}

Para obtener el mensaje de saludo en el idioma que deseemos basta con obtenerlo de la lista si está disponible y usarlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package io.github.picodotdev.serviceloader;

import java.util.Locale;
import java.util.ServiceLoader;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class Main2 {
    public static void main(String[] args) {
        Locale locale = new Locale("eu", "ES");
        String nombre = "picodotdev";
    
        ServiceLoader<Saludador> loader = ServiceLoader.load(Saludador.class);
        Iterable<Saludador> iterable = () -> loader.iterator();
        Stream<Saludador> stream = StreamSupport.stream(iterable.spliterator(), false);
        Saludador saludador = stream.filter(i -> i.getLocale().equals(locale)).findFirst().get();
    
        System.out.printf("Saludo en %s: ", locale);
        saludador.saluda(nombre);
    }
}

La clase ServiceLoader busca los servicios en los archivos META-INF/services/[interfaz] que haya disponibles en cualquiera de las librerías jar incluidas en el classpath, en el caso de este ejemplo el archivo sería META-INF/services/io.github.picodotdev.serviceloader.Saludador y este su contenido con las tres implementaciones de servicios incluidas en el ejemplo:

1
2
3
io.github.picodotdev.serviceloader.EspanolSaludador
io.github.picodotdev.serviceloader.InglesSaludador
io.github.picodotdev.serviceloader.EuskeraSaludador

La salida para el programa Main1.java y Main2.java respectivamente es:

1
2
3
4
5
# Main1.java
Idiomas soportados:[es_ES, en_GB, eu_ES]

# Main2.java
Saludo en eu_ES: Kaixo picodotdev!

Esta es una forma interesante de hacer extensible o ampliable una aplicación en un futuro. Destacar que simplemente incluyendo en el classpath una librería que incluya en el directorio META-INF/services un archivo con el nombre de la interfaz con su nombre cualificado, io.github.picodotdev.serviceloader.Saludador, las nuevas implementaciones de servicios se devolverán usando el método Service.load(Saludador.class) que se encargará de buscar los archivos en las librerías jar que los tengan.

Este mecanismo es que el se usa para permitir definir nuevos proveedores de ratios entre divisas en la librería de referencia Java Money (JSR-354) que proporciona una API para el trabajo con importes monetarios, ratios y divisas en Java. En otro artículo mostraré cómo definir un nuevo proveedor de ratios en esta API de Java Money.

El código fuente completo del ejemplo está en uno de mis repositorios de GitHub.

Referencia: