Creando un sistema con plugins en Java

03Sep08

En este post veremos como crear un sistema que extiende su funcionalidad por medio de plugins. Para ello haremos uso de la clase java.util.ServiceLoader incluida en Java 6.0

Basicamente para que nuestro sistema admita plugins debemos realizar una serie de pasos básicos:

  1. Definir la forma en que los plugins serán agregados a nuestra aplicación
  2. Crear una interface que los plugins deberán implementar
  3. Crear una forma de encontrar y cargar los plugins en tiempo de ejecución
  4. Crear los plugins
  5. Probar que todo funcione según lo previsto

Veremos con un pequeño ejemplo para entender como llevar a cabo cada uno de estos pasos, donde los plugins de nuestra aplicación simplemente devolveran un mensaje (un String) que la aplicación principal mostrará por pantalla.

0- Creando el proyecto de ejemplo

Crearemos el proyecto principal de ejemplo de nuestra aplicación. Por ahora solo crearemos un proyecto con la calse que contiene el método main.

package ar.lefunes;

public class Main {
    public static void main(String[] args) {
    }
}
Proyecto Principal

Proyecto Principal

1- Definir la forma en que los plugins serán agregados a nuestra aplicación

En este paso debemos dejar en claro como queremos que funcione nuestra aplicacion a la hora de cargar y utilizar los plugins. Aqui no programaremos nada, pero es importante identificar exactamente lo que queremos conseguir.

En el caso de nuestro ejemplo, lo que haremos es crear un directorio “plugins” en el directorio local de nuestra aplicación donde agregaremos los plugins (sus .jar), por lo tanto el sistema debe ser capaz de encontrarlos, cargarlos y utilizarlos desde esa ubicación.

2- Crear una interface que los plugins deberán implementar

Definimos una interface que todo plugins que quiera ser incorporado al sistema deberá implementar.

Crearemos un proyecto independiente del principal que contendrá unicamente la interface. Este será incluido tanto en los proyectos de cada plugin como asi también en el proyecto principal.

package ar.lefunes.plugins;

public interface IPluginMensaje {
    String getMensaje();
}
Proyecto de la Interface

Proyecto de la Interface

Incluimos a este proyecto dentro del proyecto principal:

Incluyendo el proy. de la interface en el proy. principal

Incluyendo el proy. de la interface en el proy. principal

3- Crear una forma de encontrar y cargar los plugins en tiempo de ejecución

Para cargar los plugins utilizaremos java.util.ServiceLoader, la cual busca en el classpath todas las implementaciones declaradas de una interface determinada.

Por lo tanto, para que un plugin exponga una implementación de la interface (en nuestro ejemplo IPluginMensaje) se deben cumplir dos condiciones:

  • El .jar del plugin debe estar incluido en el classpath para que ServiceLoader lo revise.
  • El plugin debe indicarle a ServiceLoader que dentro de él existe una implementación de la interface y donde puede encontrarla.

Para cumplir la primera consideración deberemos buscar todos los .jars que se encuentre dentro del directorio “plugins”, incorporandolos luego al classpath mediante la clase ModificadorClassPath que vimos en un post anterior.

Para que se cumpla la segunda condición ServiceLoader tiene que encontrar un archivo dentro del plugin en la dirección “META-INF/services/” con el nombre calificado de la interface (ar.lefunes.plugins.IPluginMensaje). Dentro del archivo se lista (linea por linea) el nombre calificado de cada una de las clases dentro del plugin que implementan la interface. En el proximo punto veremos como crear este archivo.

Agregamos entonces la clase ar.lefunes.classpath.ModificadorClassPath a nuestro proyecto principal, y creaamos una clase nueva llamada CargadorPlugins:

Clase Cargadora de Plugins

Clase Cargadora de Plugins

Código comentado de la clase CargadorPlugins:

package ar.lefunes.plugins;

import ar.lefunes.classpath.ModificadorClassPath;
import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.Vector;

public class CargadorPlugins {

    private static final String EXTENSION_JAR = ".jar";
    private static final String DIRECTORIO_PLUGINS = "plugins/";

    /**
     * carga los plugins encontrados al classpath
     * @return true si se cargaron los plugins,
     *         false en caso de existir algun error
     */
    public static boolean cargarPlugins() {
        boolean cargados = true;
        try {
            //obtiene el listado de archivos .jar dentro del directorio
            File[] jars = buscarPlugins();

            if (jars.length > 0) {
                ModificadorClassPath cp = new ModificadorClassPath();

                //a cada jar lo incluye al classpath
                for (File jar : jars) {
                    try {
                        cp.addArchivo(jar);
                    } catch (MalformedURLException ex) {
                        System.err.println("URL incorrecta: " +
                                ex.getMessage());
                    }
                }
            }
        } catch (Exception ex) {
            cargados = false;
            System.err.println(ex.getMessage());
        }
        return cargados;
    }

    /**
     * Busca todos los jars de en el directorio de plugins
     * @return jars del directorio de plugins
     */
    private static File[] buscarPlugins() {
        //crea lista vacia de archivos
        Vector< File > vUrls = new Vector< File >();

        //si existe el directorio "plugins" continua
        File directorioPlugins = new File(DIRECTORIO_PLUGINS);
        if (directorioPlugins.exists() && directorioPlugins.isDirectory()) {

            //obtiene todos los archivos con la extension .jar
            File[] jars = directorioPlugins.listFiles(new FilenameFilter() {

                @Override
                public boolean accept(File dir, String name) {
                    return name.endsWith(EXTENSION_JAR);
                }
            });

            //los agrega a la lista de archivos
            for (File jar : jars) {
                vUrls.add(jar);
            }
        }

        //retorna todos los archivos encontrados
        return vUrls.toArray(new File[0]);
    }

    /**
     * Obtiene todos los plugins IPluginMensaje encontrados en el classpath
     * @return lista de plugins encontrados e instanciados
     */
    public static IPluginMensaje[] getPlugins() {

        //cargamos todas las implementaciones de IPluginMensaje
        //encontradas en el classpath
        ServiceLoader< IPluginMensaje > sl =
                ServiceLoader.load(IPluginMensaje.class);
        sl.reload();

        //crea una lista vacia de plugins IPluginMensaje
        Vector< IPluginMensaje > vAv = new Vector< IPluginMensaje >();

        //cada plugin encontrado es agregado a la lista
        for (Iterator< IPluginMensaje  > it = sl.iterator(); it.hasNext();) {
            try {
                IPluginMensaje pl = it.next();
                vAv.add(pl);
            } catch (Exception ex) {
                System.err.println("Excepcion al obtener plugin: " +
                        ex.getMessage());
            }
        }

        //retorna los plugins encontrados y cargados
        return vAv.toArray(new IPluginMensaje[0]);
    }
}

En este punto ya podemos incorporar la funcionalidad al método main del proyecto:

package ar.lefunes;

package ar.lefunes;

import ar.lefunes.plugins.CargadorPlugins;
import ar.lefunes.plugins.IPluginMensaje;

public class Main {

    public static void main(String[] args) {
        System.out.println("-------------------------------");

        //se cargan los jars del directorio "plugins" al classpath
        boolean cargados = CargadorPlugins.cargarPlugins();

        if (cargados) {
            try {
                //obtiene una instancia de cada plugin IPluginMensaje encontrado
                IPluginMensaje[] avisadores = CargadorPlugins.getPlugins();

                if (avisadores.length > 0) {
                    for (IPluginMensaje a : avisadores) {
                        //por cada plugin muestra la clase y el mensaje que devuelve
                        System.out.println("Plugin: \t" + a.getClass().getCanonicalName());
                        System.out.println("Mensaje:\t" + a.getMensaje());
                        System.out.println();
                    }
                } else {
                    System.out.println("No se Encontraron Plugins");
                }
            } catch (Exception ex) {
                System.err.println("Excepcion: " + ex.getMessage());
                ex.printStackTrace();
            }
        } else {
            System.out.println("Plugins No Cargados");
        }

        System.out.println("-------------------------------");
    }
}

Si en este punto ejecutamos la aplicación obtendremos:

——————————-
No se Encontraron Plugins
——————————-

debido a que aún no hemos instalado ningun plugin en la misma.

4- Crear los plugins

Creamos un proyecto nuevo para nuestro primer plugin para la aplicación y agregamos la dependencia hacie el proyecto de la interface.

Proyecto del Primer Plugin

Proyecto del Primer Plugin

Creamos la clase PluginNumeroUno que va ha ser la implementacion de la interface:

package ar.lefunes.plugins.impl;

import ar.lefunes.plugins.IPluginMensaje;

public class PluginNumeroUno implements IPluginMensaje {
    public String getMensaje() {
        return "Este es el plugin numero uno !!!";
    }
}

De esta manera, a pesar a que funcionalmente hemos finalizado con el plugin, este no será cargado en la aplicación debido a que le falta el descriptor para la clase ServiceLoader. Para ello creamos un archivo con el nombre de la interface dentro del directorio “META-INF.services”

Agregando Descriptor de Servicio

Agregando Descriptor de Servicio

Donde adentro del mismo colocaremos una sola linea indicando el nombre calificado de la clase que implementa la interface:

Definición del Descriptor de Servicio

Definición del Descriptor de Servicio

5- Probar que todo funcione según lo previsto

Para probar compilamos los tres proyectos y armamos la siguiente estructura de archivos:

Estructura del Archivos del Ejemplo

Estructura del Archivos del Ejemplo

Al ejecutar, el programa mostrará por la consola:

——————————-
Plugin:         ar.lefunes.plugins.impl.PluginNumeroUno
Mensaje:        Este es el plugin numero uno !!!

——————————-

con lo que podemos observar que cargo correctamente el plugin.

Más de un plugin por JAR

El paso de creación es similar al explicado para un solo plugin: Agregamos la dependencia al proyecto de la interface y creamos las clases que la  implementan:

Proyecto con Dos Plugins

Proyecto con Dos Plugins

La diferencia radica solamente que dentro del descriptor para ServiceLoader indicaremos las clases, una por linea:

Descriptor de los Dos Plugins

Descriptor de los Dos Plugins

Con lo que obtendremos al incorporar el Jar a la carpeta “plugins” del proyecto principal y ejecutando este último:

——————————-
Plugin:         ar.lefunes.plugins.impl.PluginNumeroUno
Mensaje:        Este es el plugin numero uno !!!

Plugin:         ar.lefunes.plugins.multiple.PluginDos
Mensaje:        Plugin Nº 2

Plugin:         ar.lefunes.plugins.multiple.PluginTres
Mensaje:        Plugin Nº 3

——————————-

Plugin por defecto

Devido a que el propio proyecto principal se encuentra dentro del classpath (y por esto ServiceLoader lo revisará) podemos brindar un plugin que se cargará aún cuando no exista ningun jar en el directorio “plugins”.

Para ello creamos la clase que implementa el servicio y la indicamos con un descriptor dentro del proyecto:

Agregando Plugin por Defecto

Agregando Plugin por Defecto

Descriptor del Plugin por Defecto

Descriptor del Plugin por Defecto

Donde obtendremos en la salida al ejecutar sin ningún plugin en el directorio “plugins”:

——————————-
Plugin:         ar.lefunes.plugins.defecto.PluginDefecto
Mensaje:        PLUGIN POR DEFECTO

——————————-

Recursos

Se puede descargar el proyecto de ejemplo (aprox. 71kB) de: http://lefunes.googlecode.com/files/app_plugins.zip. En él se incluyen tanto los jars para ejecutar el ejemplo directamente como los proyectos en NetBeans 6.5 creados para este post.

Espero les sirva.
Hasta la proxima.

Más Info



19 Responses to “Creando un sistema con plugins en Java”

  1. 1 Luis

    Hola buenas noches, solo para hacerte una pregunta, estoy realizando un sistema con plugIns y estoy teniendo el siguiente error.

    ServiceConfigurationError
    Plugins.IPluginAutorizador: Provider Plugins.Autorizacion not found

    donde la interfase es la que deben implementar todos los plugins y la clase Autorizacion es la clase del Plugin que implementa la interfase, no se porque me esta generando este error ya que habia segudo los pasos que muestras en este blog y habia funcionado perfecto.

    ¿de casualidad sabes el origen de este error?

    gracias por tu respuesta
    saludos.

  2. 2 Jesus

    Hola Lefunes,
    Una pregunta sobre este gran post. Lo quiero utilizar para una aplicación Java que permite incorporar nueva funcionalidad de forma dinamica y funiona perfecto. El problema me ha surgido cuando he querido implementar una aplicación web que gestione que plugins hay instalado y su estado. Cuando lanzo el Tomcat y ejecuto se me queda “colgado” en la parte de addURL del ModificadorClassPatch…

    Supongo que será por la forma que Tomcat gestiona su ClassPatch, ¿tienes alguna idea al respecto?

    Muchisimas gracias.
    Un saludo.
    Jesús.

  3. 3 joalbert

    Bueno ya logre hacer mi programa, y le incorpore dos plugins. Es un mini editor, todo lo cree a lo criollo a PATA, asi si lo entendi fino. ahora hacerlo lo un IDE mas facil, pero primero lo primero. Gracias por el tuto, no he visto muchos por alli, y las pag que encuentro me dirigen aqui ?? si saben sobre otros, o si pueden hacer el tuto mas avanzado me gustaria mucho… Gracias!!

  4. 4 joalbert

    Señores todo fino y todo, pero trate de hacer un ejemplo en eclipse y no es igual pues… tal ves si pueden hacen uno pero en eclipse o sino, sin algun IDE, seria fino para entender con exactitud como hacer un plugin. es que eso tambin de los paquetes naaaa, paquetes por todas partes y me enrredo más. sorry!!! gracias por el articulo, me interesa full.

  5. 5 EdwinF.

    Hombre, felicitaciones por el post. Te me adelantaste con el articulo🙂, en mi tesis de grado hago lo mismo, exactamente lo mismo, con la diferencia de agregar una clase de retorno en caso de no encontrar el tipo (ademas de dejarle al usuario definir los plugins/servicios que requiere).

    Saludos.

  6. @babyware No vas a tener problemas al cargar nuevos jars al classpath, pero para que te tome un cambio sobre uno ya cargado deberías reiniciar la aplicación con este sistema de ClassLoader.

    Para solucionar este comportamiento lo más fácil es directamente implementar un nuevo ClassLoader independiente del por defecto del sistema.

    Saludos

  7. 7 barbyware

    Me gustaria saber como se recargaria un plugin.

    Estoy intentando montar un sistema de preload, en los que se cargaria una clase a partir de un classloader o algo parecido.

    Me encuentro con el problema de la recarga que tendria que hacer para recarga.

  8. 8 Jhugs

    Good, this example It’s very easy to understand, thanks.🙂

  9. @Pablo tal como le comentaba a Diego en el post anterior, es precisamente la alternativa para llegar al mismo resultado: Utilizar alguna subclase de ClassLoader (ya seá una propia o una como URLClassLoader) para no tocar el ClassLoader del sistema vía reflección.

    De hecho si solo se vá a utilizar el ClassLoader para cargar los .jars de los plugins se puede llevar adelante de la forma que describis, ya que no es necesario tener la clase ModificadorClassPath “esperando” agregar clases al ClassLoader.

    Muchas gracias por compartir tu experiencia. Espero dentro de poco poder escribir algo al respecto.

    Saludos

  10. Muchas gracias por tu post, me sirvió mucho.
    Acerca de incorporar los .jars al classpath mediante la clase ModificadorClassPath de tu post anterior, te quería comentar que en vez de modificar el classpath de la aplicación, se puede crear un URLClassLoader sólo para el propósito de cargar los plugins.
    Para esto, creé un URLClassLoader usando el constructor que recibe una lista de URL, entregándole los path de los plugins jar.
    Luego, usé el método ServiceLoader.load(Class service, ClassLoader loader) y le pasé el URLClassLoader creado.
    De esta manera el classpath original de la JVM permanece limpio, sin ser tocado.

    • 11 Jesus

      Hola Pablo,
      Estoy intentando hacer tu propuesta pero no lo consigo, puedes decidme como creas tu URLClassLoader?

      Gracias.

  11. @hectornet felicitaciones, muy buen trabajo! También me parece destacable de la parte de plugins la forma de incorporar las opciones del plugin a la configuración general de LinCE

    Por ejemplo una forma de utilizar algo similar a la que conseguís con los atributos del manifest con ServiceLoader sería crear una interface IPlugin:

    public interface IPlugin{
        String getVersion();
        String getDependencias();
        void load();
        void unload();
    }
    

    Que cada plugin implementaria.

    Saludos

  12. Hola,
    para mi proyecto de fin de carrera implementé un sistema de carga de plugins.
    Lleva un script en ANT que se encarga de compilar y empaquetar los plugins.
    Los plugins son tambien archivos JAR que se copian en el directorio plugins, pero en su archivo MANIFEST.MF hay un atributo que indica que método debe llamarse al cargar y descargar el plugin (usando introspección).
    También se implementa un sistema sencillo de chequeo de dependencias, los plugins que no las satisfagan no se cargarán.
    Hay una descripción mas detallada a partir de la página 87 de la memoria de mi PFC: http://compan.iespana.es/MemoriaPFC_LinCE.pdf y tambien podeis descargaros el código en: http://www.supercable.es/~compan1
    o contactar conmigo.

    Saludos y espero que sea útil!

  13. @Seraphinux muchas gracias

    @greeneyed Me alegro que te haya servido (y no solo a mi :))

    @batch4j ClassLoader es participe principal en este proceso y ServiceLoader no busca reemplazarlo. De hecho para cargar clases al classpath lo estamos utilizando directamente. Lo que ServiceLoader busca es evitarnos implementar el mecanismo de registro y descubrimiento de servicios (en este caso, las clases que implementan una determinada interface)

    En mi caso particular, anteriormente para realizar este mismo proceso caía siempre en al punto de tener que llevar un registro (con un XML por ejemplo) de las clases que quería instanciar según los plugins cargados. Esto se simplifica usando ServiceLoader ya que simplemente le indicamos la interfaz que tienen que implementar las clases que queremos cargar y cuando las necesitemos (vía el ClassLoader, por supuesto) las obtendremos.

    Esto no es algo que no está probado, de hecho NetBeans utiliza un servicio de descubrimiento de plugins inspirado en ServiceLoader.

    Gracias por el comentario, realmente es algo que no había quedado claro

  14. 15 batch4j

    Que hace service loader que no se pueda hacer
    utilizando un classloader normal.

    Cuando lei el post al principio pense que era
    una manera de cargar clases dinamicamente.

    No le encuentro ninguna ventaja respecto a
    hacerlo con un classloader.

  15. 16 greeneyed

    Lo tenia en mente pero no me había decidido hasta ahora a probarlo, lo he leido aquí y me ha animado a ponerme, así que ya he implementado un sistema para cargar unas clases como servicios, que antes cargaba en base a ifs🙂.

    Así que gracias, este mini-tutorial ha sido suficiente😉.

  16. WooW

    Tio te quedo genial este tuto, hace dias me preguntaba como funcionaba esas yerbas de los plugins. Me lo apunto para posterior analisis.

    Saludos!!!


  1. 1 Adsocks v0.7.1 OpenSource. Unico server multiplataforma
  2. 2 Bitacoras.com

A %d blogueros les gusta esto: