viernes, 19 de marzo de 2010

Gestión simple de módulos y dependencias con java.util.ServiceLoader

En la entrada de hoy vamos a tratar un problema un poco más complejo, y por lo tanto la solución requiere también un poco más de trabajo, pero el resultado final merece la pena.

En este caso el enunciado del problema sería algo tal que así:
  • Supongamos un proceso que puede tener varias implementaciones diferentes y queremos permitir añadir diferentes versiones y permitir escoger una implementación simplemente a través de una cadena de configuración.
La solución que proponemos usa los mecanismos proporcionados en el API básico de Java (se necesita Java 6), sin acudir a soluciones más complejas como OsGI. En caso de necesitar poder actualizar/cambiar las dependencias en tiempo de ejecución y/o tener que poder permitir varias versiones de la misma dependencia a la vez en una misma JVM, sí deberíamos usar algo más complejo, pero si no es necesario, quizá no nos merezca la pena entrar en soluciones tan complejas.

Volviendo a nuestra solución propuesta, la idea es utilizar la clase java.util.ServiceLoader que nos permite declarar y recorrer las clases que implementan una Interfaz dada, pudiendo añadir nuevas implementaciones en el classpath simplemente empaquetando las clases adecuadamente.

El primer punto a tener en cuenta es que podríamos cargar con el ServiceLoader directamente las implementaciones del proceso, sin embargo esta estrategia presenta un problema. ServiceLoader instancia las clases al recorrerlas buscando las implementaciones posibles, así que tenemos que tener toda las dependencias de todas las implementaciones posibles en el classpath o tendremos problemas. En algunos casos eso puede no ser problemático, pero imaginemos que tenemos varias soluciones y que unas usan Hibernate, otras JPA, otras JDO... al cargarlas todas tendríamos que tener todas las dependencias en el classpath, aunque sólo fuéramos a usar JDO, por ejemplo.

Así pues, lo que haremos será que la clase cargada a través de ServiceLoader sea únicamente una utilidad que nos diga el nombre (una cadena) de clase que realmente implemente el proceso. De esta forma podemos cargar la clase utilidad tranquilamente en el classpath sin necesitar cargar todas las dependencias de la implementación.
En la implementación necesitaremos:
  • La interfaz a implementar por todas las clases utilidad. Para hacerlo más flexible permitiremos que una sola clase de utilidad pueda especificar varias implementaciones, cada una con su “clave”:
package my.pckg;

public interface ProcessImplementationRegistration
{
  /*
   Las implementaciones han de devolver una lista de pares clave/implementacion del proceso.
   Por ejemplo: {{“jpa”,”com.my.JPAImplementation”},
                 {“jdo”,”com.my.JDOImplementation”}}
  */
  public String[][] getProcessImplementations();
}
  • En la clase que va a utilizar las implementaciones, cargar las clases que utilizando el mecanismo de la clase java.util.ServiceLoader, y registrar las implementaciones según su clave:
ServiceLoader theServiceLoader = ServiceLoader.load(ProcessImplementationRegistration.class);
    for(ProcessImplementationRegistration pirInstance: theServiceLoader)
    {
      // Usar pirInstance.getProcessImplementations() para registrar
      // las implementaciones por clave
      ...
    }
  • Una vez hecho esto, para añadir una implementación simplemente tenemos que crear la clase que implemente la interfaz my.pckg.ProcessImplementationRegistration, empaquetarla en un jar y añadir en el directorio META-INF del jar, un fichero llamado my.pckg.ProcessImplementationRegistration donde incluiremos el nombre de la clase que implemente la interfaz.
Por ejemplo, clase que implementará la interfaz será:

package com.my;

public class MyJPARegistration implements ProcessImplementationRegistration
{
  /*
   Devolvemos la implementacion que registra este .jar
  */
  @Override
  public String[][] getProcessImplementations()
  {
   return new String[][]{{“jpa”,”com.my.JPAImplementation”}}
  }
}

El fichero META-INF/my.pckg.ProcessImplementationRegistration contendrá únicamente la linea:
com.my.MyJPARegistration
y obviamente en el .jar incluiremos también la clase com.my.JPAImplementation y sus clases auxiliares necesarias si las hubiera.

Colocamos el .jar en el classpath, reiniciamos la maquina virtual Java y voilà: nuestra implementación será descubierta y clasificada según la clave “jpa”. Además, si no la usamos no hará falta que tengamos las clases de JPA en el classpath, por lo que podemos tener los .jar de registro de todas nuestras opciones y sólo necesitaremos tener en el classpath las dependencias de las que realmente usemos.

Este es un mecanismo que utilizan algunos frameworks para incluir plugins opcionales de forma sencilla, basta incluir el fichero .jar en el classpath, sin necesitar incluir las dependencias a no ser que realmente los utilicemos.

Eso es todo. No es una implementación completa pero espero haberos provisto de todos los detalles para que podáis experimentar vosotros mismos. Sólo hace falta tener Java 6, así que animaros a hacer alguna prueba.

Un saludo y hasta la próxima.

2 comentarios:

  1. Me encanta el párrafo "quizá no nos merezca la pena entrar en soluciones tan complejas" a propósito de OSGI. Me alegra ver que no soy el único en el mundo que prefiere las soluciones sencillas a las complejas cuando no necesita estas últimas.

    Últimamente se está volviendo un poco inquisitivo esto del Java y si no usas Spring, OSGI, JPA, anotaciones, y la madre que lo parió, te excomulgan en nanosegundos.

    En cuanto a la clase ServiceLoader tengo que decir que ya la conocía y que a mi no se me ajusta del todo, pero que inspirándome en ella he hecho otra cosa parecida que, según el JAR que despliegues te coge una implementación u otra de cada servicio. Así puedo modularizar las aplicaciones en base a distintos JAR, y luego desplegar el/los elegido/s.

    En definitiva, que en el API estándar de Java hay muchas ideas buenas. Y es una pena que algunas no tengan tanto éxito como las cosas de Apache, Spring, y demás.

    ResponderEliminar
  2. Totalmente de acuerdo, Ivan, a veces parece que sea obligatorio complicarse la vida y/o usar cuantas más cosas más complejas y con más siglas para... no se... compensar algún tipo de complejo de inferioridad que parecen tener los programadores.

    Y gracias por el comentario, ya pensaba que hablaba al vacío :D.

    ResponderEliminar