Introducción a EJB 3.1 (III)

En el artículo anterior vimos algunos conceptos comunes a los componentes de una aplicación EJB, como el contexto de sesión, o la diferencia entre remoto y local. Además, vimos en cierta profundidad los dos tipos de Session Bean más comunes en una aplicación EJB: Stateless y Stateful. En este artículo vamos a ver el último tipo de Session Bean (Singleton Session Bean), dos conceptos aplicables a todos los Session Bean y que son nuevos en la especificación EJB 3.1 (vista sin interface y llamadas asíncronas), y finalmente un nuevo tipo de componente: Message-Driven Bean. Comencemos.

3.1 Singleton Session Beans: conceptos básicos

El Singleton Session Bean (no existe una traducción literal de la palabra Singleton, pero viene a expresar el concepto de instancia única; desde ahora nos referiremos a este componente como Singleton Session Bean, o Singleton a secas) es un nuevo tipo de Session Bean introducido en la especificación EJB 3.1. Este componente se basa en el patrón de diseño del mismo nombre definido por Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides en su libro Design Patterns: Elements of Reusable Object Oriented Software (Patrones de Diseño: Elementos Reusables de Software Orientado a Objetos; una lectura imprescindible). Este patrón de diseño garantiza que de una clase dada solamente pueda crearse una instancia, con un punto de acceso global para acceder a dicha instancia. La naturaleza única de un componente Singleton conlleva un alto rendimiento dentro del contenedor para este tipo de Session Bean.

Es importante tener en cuenta la sutil diferencia entre un componente de tipo Singleton y cualquiera de los otros dos tipos de Session Bean (SLSB y SFSB): cuando un cliente hace una llamada a un método de un SLSB o SFSB, puesto que la instancia del Session Bean está asociada a ese cliente en concreto (durante la invocación del metodo para un SLSB, y durante toda la duración de la sesión para un SFSB), un único thread (hilo de ejecución) es capaz de acceder a dicho método, y por tanto el componente es seguro en terminos de multi-threading (ejecución de múltiples hilos). Sin embargo, cuando trabajamos con un Singleton, multiples llamadas en paralelo pueden estar produciendose en un momento dado a su única instancia, y por tanto el componente debe garantizar que un hilo de ejecución no está interfiriendo con otro hilo de ejecución, produciendo resultados incorrectos. Dicho de otra manera, un componente Singleton debe ser concurrente.

Singleton Session Beans: concurrencia básica

Debido a la naturaleza de los Session Bean de tipo Singleton, tenemos que tener muy claros ciertos aspectos al diseñar este tipo de componente. Veamos un ejemplo de una clase donde no tenemos en cuenta la cuestión de concurrencia:

class ClaseNoConcurrente { 
    private int numeroDeInvocacionesDeEstaInstancia = 0; 
     
    public int metodoUno() { 
        // lógica de negocio 
        return numeroDeEjecuciones++; 
    } 
     
    public int metodoDos() { 
        // lógica de negocio 
        return numeroDeEjecuciones++; 
    } 
}
	

En la clase del ejemplo anterior, no se ha tenido en cuenta el aspecto de concurrencia (lo cual, dependiendo de las especificaciones del problema de negocio que queremos resolver, puede ser perfectamente legal). Una consecuencia de esto es que, cuando intervienen múltiples hilos de ejecución en paralelo, una llamada al cualquiera de los métodos metodoXxx puede devolver un resultado incorrecto. Veamos una posibilidad (entre muchas permitidas por la JVM) de la ejecución de esta clase por varios clientes:

  1. Un cliente C-1 llama a metodoUno()
  2. El cliente C-1 obtiene el valor 1 para el número de ejecuciones
  3. Un cliente C-2 llama a metodoUno()
  4. Un cliente C-3 llama a metodoDos()
  5. El cliente C-3 obtiene el valor 2 para el número de ejecuciones
  6. El cliente C-2 obtiene el valor 2 para el número de ejecuciones

¿Que ha ocurrido en el ejemplo anterior? El cliente C-2 cree que los métodos metodoXxx han sido invocados dos veces (punto 6), cuando en realidad se han invocado tres veces (puntos 1, 3, y 4). Esto es debido a que la operación numeroDeEjecuciones++ no es atómica: cuando el código Java es convertido en código de bytes (.class), dicha operación se compone de muchas otras, de manera que la JVM puede para el hilo actual en mitad del incremento y dar tiempo de ejecución a otro hilo. Incluso aunque la operación fuera atómica, la JVM podría seguir deteniendo un hilo durante la lógica de negocio (antes de realizar el incremento), dando tiempo de ejecución a otro hilo y produciendo de nuevo resultados incorrectos. Puesto que un Singleton es compartido por muchos clientes (accedido por muchos hilos de ejecución) este comportamiento no es aceptable: el Singleton debe ser concurrente.

La especificación EJB 3.1 nos permite controlar la concurrencia de un Singleton de dos formas:

  • Concurrencia Gestionada por el Contenedor (CMC, Contained-Managed Concurrency)
  • Concurrencia Gestionada por el Bean (BMC, Bean-Managed Concurrency)

Veamos cada una de ellas con cierto detalle.

3.3 Singleton Session Beans: Contained-Managed Concurrency

Mediante CMC, el contenedor es responsable de gestionar toda la concurrencia en el Singleton mediante metadatos, liberando así al programador de la tarea de escribir código concurrente. Por defecto, todos los componentes Singleton son gestionados por el contenedor, aunque podemos especificarlo de manera explicita mediante la anotación @ConcurrencyManagement:

import javax.ejb.ConcurrencyManagement; 
import javax.ejb.ConcurrencyManagementType; 
import javax.ejb.Singleton; 

@Singleton 
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) 
class MiSingleton { 
    // ... 
}
	

En el ejemplo anterior hemos definido un componente Singleton mediante la anotación @Singleton, y hemos indicado al contenedor de forma explícita que gestione por nosotros la concurrencia del componente mediante CMC (aunque, repito, este comportamiento es el ofrecido por defecto para todos los Singleton). Cuando usamos CMC, todos los métodos de la clase tienen por defecto un bloqueo de tipo write (escritura). Este bloqueo es de utilidad cuando uno o varios métodos deben ser accedidos de manera secuencial (hasta que uno no termine, otro no puede comenzar) para evitar que se obtengan resultados incorrectos. Este acceso secuencial es necesario en casos como el del ejemplo de la sección 3.2, en el que varios métodos escribían sobre el valor de una variable que después era devuelta al cliente. Como se puede deducir del comportamiento del bloqueo write, este afecta a todo el objeto, y no a métodos concretos (todos los métodos write del mismo Singleton deben compartir un único bloqueo).

Por otro lado, cuando un método realiza solo operaciones de tipo read (lectura), de manera que no se altera el estado del componente, podemos indicarle al contenedor que no bloquee ninguna invocación a dicho método. De esta manera, el método pueda ser accedido por múltiples hilos de ejecución simultaneamente (puesto que no modificamos el estado del componente, el acceso en paralelo es seguro). Tenemos que indicar que un método es de tipo read mediante la anotación @Lock:

import javax.ejb.Lock; 
import javax.ejb.LockType; 

// ... 

@Lock(LockType.READ) 
public String metodo() { 
    // ... 
} 
	

Aunque, como ya se ha dicho, el tipo de bloqueo write se aplica por defecto, podemos expresarlo de forma explícita mediante @Lock(LockType.WRITE). Otra opción que nos brinda CMC es la liberación de un bloqueo automáticamente si este no ocurre tras un tiempo preestablecido (timeout):

import java.util.concurrent.TimeUnit; 
import javax.ejb.AccessTimeout; 

// ... 

@AccessTimeout(value=5, unit=TimeUnit.SECONDS) 
public String metodo() { 
    // ... 
} 
	

3.4 Singleton Session Beans: Bean-Managed Concurrency

En ocasiones, la concurrencia gestionada por el contenedor no es suficiente (tal vez no nos permite definir con suficiente detalle la manera en la que necesitamos que funcione la concurrencia de nuestra aplicación, por ejemplo). Bean-Managed Concurrency (BMC, Concurrencia Gestionada por el Bean) deja en manos del programador toda la gestión de la concurrencia. Esto puede realizarse utilizando bloques synchronized, variables atómicas como java.util.concurrent.atomic.AtomicInteger, etc. Podemos indicar que la concurrencia de un componente Singleton será gestionada íntegramente por el programador mediante la anotación @ConcurrencyManagement (la cual usamos en la sección anterior para declarar explícitamente el comportamiento contrario, CMC):

import javax.ejb.ConcurrencyManagement; 
import javax.ejb.ConcurrencyManagementType; 
import javax.ejb.Singleton; 

@Singleton 
@ConcurrencyManagement(ConcurrencyManagementType.BEAN) 
class OtraClaseConcurrente { 
    // Gestión explícita de la concurrencia 
}
	

La concurrencia en Java es un tema largo y complejo que no puede ser tratado en este tutorial. Un buen lugar para empezar (en inglés) es The Java Tutorials. Estoy seguro que existen otras buenas fuentes de información en español, y si estás interesado tu buscador favorito te llevará hasta ellas.

3.5 Singleton Session Beans: el ciclo de vida

El ciclo de vida de un Singleton muy parecido al de un SLSB, pues como en este último se compone de los dos mismos estados:

  • No Existe (Does not exists)
  • Preparado (Method-ready)

La diferencia estriba en cuando se crea la única instancia del componente Singleton (tras el despliegue de la aplicación o tras la primera invocación al componente), y en su duración (a lo largo de toda la vida de la aplicación, esto es, hasta que la aplicación sea replegada o el servidor sea parado). Evidentemente, solo el primer comportamiento (el momento de creación) puede ser customizado, y para ello la especificación EJB nos ofrece, como siempre, metadatos. Aunque el contenedor puede decidir inicializar la única instancia de un Singleton en el momento que considere más oportuno, podemos forzar que dicho proceso ocurra durante el despliegue mediante la anotación @Startup:

import javax.ejb.Singleton; 
import javax.ejb.Startup; 

@Singleton 
@Startup 
public class MiSingleton { 
    // ... 
} 
	

El código anterior fuerza al contenedor a crear la única instancia de MiSingleton en el momento del despliegue de la aplicación EJB de la que forma parte. Esto comportamiento es llamado eager initialization (inicialización temprana). Ahora supongamos que un Singleton debe ser inicializado antes que otro Singleton (tal vez este último necesite poner la aplicación en cierto estado necesario para el correcto funcionamiento del primero): podemos indicar esta dependencia en el orden de inicialización mediante la anotación @DependsOn:

@Singleton 
public class MiSingletonA { 
    // ... 
} 

@Singleton 
@DependsOn("MiSingletonA") 
public class MiSingletonB { 
    // ... 
}
	

En el ejemplo anterior, indicamos al contenedor que en el momento de inicializar la única instancia de MiSingletonB, MiSingletonA debe haber sido inicializado. De esta manera, el contenedor forzará la inicialización del Singleton dependiente si aún no se ha producido. Este comportamiento es totalmente ajeno al uso u omisión de @Startup: si un Singleton depende de otro, este último se creará antes que el primero.

Para terminar con el ciclo de vida de los componentes Singleton, indicar que, al igual que con los componentes SLSB y SFSB, disponemos de las anotaciones @PostConstruct y @PreDestroy para responder a los eventos de creación y destrucción (respectivamente) del componente:

@Singleton 
public class MiSingletonA { 
    @PostConstruct 
    public void inicializar() { 
        // Cualquier operación/es necesaria/s para la creación del componente, 
        // como obtención de recursos 
    } 
     
    @PreDestroy 
    public void detener() { 
        // Cualquier operación/es necesaria/s antes de la destrucción del componente, 
        // como liberación de recursos 
    } 
     
    // lógica de negocio del Singleton 
}
	

3.6 Singleton Session Beans: un ejemplo sencillo

La utilidad del componente Singleton está limitada a ciertos problemas de negocio que podemos (o debemos) resolver mediante una única instancia de un objeto. Ejemplos válidos de componentes Singleton serían gestores de ventanas, sistemas de ficheros, colas de impresión, caches, etc. Para nuestro ejemplo de Singleton vamos a desarrollar un sistema de logging, el cual debe ser accedido por toda la aplicación a través de una única instancia:

package es.davidmarco.ejb.singleton; 

import java.io.FileWriter; 
import java.io.IOException; 
import java.text.SimpleDateFormat; 
import java.util.Date; 
import javax.annotation.PostConstruct; 
import javax.annotation.PreDestroy; 
import javax.ejb.Lock; 
import javax.ejb.LockType; 
import javax.ejb.Singleton; 

@Singleton 
public class SistemaDeLog { 
    private FileWriter writer; 

    private enum Nivel { 
        DEBUG, INFO, ERROR 
    } 

    @PostConstruct 
    protected void inicializar() throws IOException { 
        writer = new FileWriter("aplicacion.log", true); 
    } 
     
    @PreDestroy 
    protected void detener() throws IOException { 
        writer.flush(); 
        writer.close(); 
    } 

    @Lock(LockType.WRITE) 
    public void debug(String mensaje) { 
        escribirMensajeEnArchivo(Nivel.DEBUG, mensaje); 
    } 

    @Lock(LockType.WRITE) 
    public void info(String mensaje) { 
        escribirMensajeEnArchivo(Nivel.INFO, mensaje); 
    } 

    @Lock(LockType.WRITE) 
    public void error(String mensaje) { 
        escribirMensajeEnArchivo(Nivel.ERROR, mensaje); 
    } 

    private void escribirMensajeEnArchivo(Nivel nivel, String mensaje) { 
        String cabecera = generarCabecera(nivel); 
        try { 
            writer.write(cabecera + mensaje + "\n"); 
        } catch (IOException ioe) { 
            throw new RuntimeException(ioe); 
        } 
    } 
     
    private String generarCabecera(Nivel nivel) { 
        String fechaMasHoraActual = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()); 
        StringBuilder cabecera = new StringBuilder(); 
        cabecera.append("["); 
        cabecera.append(nivel.name()); 
        cabecera.append("] "); 
        cabecera.append(fechaMasHoraActual); 
        cabecera.append(" - "); 
         
        return cabecera.toString(); 
    } 
}
	

El ejemplo anterior, aunque más desarrollado que los vistos hasta ahora (es completamente funcional), sigue siendo un ejemplo de juguete; sin embargo, es bastante interesante para explicar el componente Singleton. Existen verdaderos frameworks de logging que puedes (¡y debes!) usar en tus aplicaciones.

Las primero que nos interesa del ejemplo son los métodos callback @PostConstruct y @PreDestroy. En ellos se obtiene y libera (respectivamente) un recurso dado, en nuestro caso el acceso a un fichero en disco. Esto nos muestra la utilidad del componente Singleton: únicamente una instancia debe crear, editar, y finalmente cerrar el mismo fichero en disco. Y esta única instancia es la que obtendrán todos los clientes del Singleton. Ambos métodos se han marcado con visibilidad protected para facilitar la tarea de testing (y por un motivo adicional que se explicará en la próxima sección).

Lo segundo que nos interesa del ejemplo son los tres métodos que se exponen al cliente: debug, info, y error. A traves de ellos el cliente puede realizar las operaciones de logging. Los tres han sido marcados como métodos write mediante @Lock(LockType.WRITE), y aunque este comportamiento es definido por defecto para todos los métodos de un Singleton (y por tanto la anotación es redundante), se han incluido para diferenciarlos del resto (siempre es aconsejable escribir código expresivo).

Por último, los métodos de utilidad escribirMensajeEnArchivo y generarCabecera son métodos de utilidad usados por la lógica de negocio de nuestro componente.

3.7 Session Beans en general: No-Interface View

Como se indicó en la sección anterior, el ejemplo del sistema de logging es completamente funcional. Sin embargo, el componente no ha sido declarado local ni remoto (ejemplos anteriores omitían este aspecto intencionadamente para mantener el código simple). En este momento es preciso introducir el concepto de vista sin interface.

Una vista sin interface (no-interface view) es una variación del concepto de componente local, e introducida en la especificación EJB 3.1. Como su nombre indica, se trata de un componente que no está anotado con @Local ni @Remote, ni implementa ninguna interface de negocio donde se hayan aplicado cualquiera de las anotaciones anteriores. Si no implementamos ninguna interface de negocio, ¿como sabe el contenedor que métodos del componente debe exponer a sus clientes? La respuesta es sencilla: todos aquellos declarados public, incluyendo los de sus superclases y los de tipo callback (en el ejemplo de la sección anterior hemos declarado los métodos callback con un nivel de visibilidad menor a public precisamente para evitar que sean expuestos). Aunque a efectos prácticos un componente de este tipo es tratado como uno local, puedes encontrar los detalles concretos sobre la vista sin interface en la especificación EJB 3.1.

3.8 Session Beans en general: llamadas asíncronas

Otra de las novedades de la especificación EJB 3.1 es la posibilidad de llamar a los métodos de nuestros Session Bean de forma asíncrona. Hasta ahora, todas las llamadas que hemos realizado han sido sincronas, de manera que los clientes deben esperar hasta que el método invocado termine de ejecutarse para seguir ejecutando el resto de sus sentencias. Este es el comportamiento normal cuando invocamos un método en Java. Sin embargo, cuando realizamos una llamada asíncrona, el flujo de ejecución vuelve automáticamente a la aplicación que realiza la llamada, sin esperar el resultado de la llamada. Más tarde, podemos comprobar dicho resultado si existe y lo necesitamos.

Las llamadas asíncronas son de gran utilidad en situaciones en las que un método realiza una operación que necesita un tiempo considerable para completarse, y no deseamos bloquear al cliente mientras dicha operación se realiza. Veamos primero el ejemplo más sencillo posible de llamada asíncrona:

package es.davidmarco.ejb.slsb; 

import javax.ejb.Asynchronous; 
import javax.ejb.Stateless; 

@Stateless 
public class ClaseAsincrona { 
     
    @Asynchronous 
    public void metodoLento() { 
        try { 
            Thread.sleep(15000); 
        } catch(InterruptedException ie) { 
            throw new RuntimeException(ie); 
        } 
    } 
}
	

En el ejemplo anterior, hemos declarado el método metodoLento() como un método asíncrono mediante la anotación @Asynchronous. Dicha anotación puede ser aplicada también a nivel de clase, en cuya caso todos los métodos de negocio (los declarados en una interface local, en una interface remota, o los de visibilidad public en una vista sin interface) serán considerados como asíncronos. Dentro del método hemos simulado un proceso relativamente largo deteniendo durante quince segundos el hilo de ejecución que está procesando la instancia del SLSB. Cualquier cliente que llame a este método no tendrá que esperar esos quince segundos, ya que nada más realizar la llamada le será devuelto en control de ejecución, sin esperar a que el método asíncrono termine. Este comportamiento se conoce como fire-and-forget (disparar y olvidar).

Otra posibilidad es que necesitemos el resultado de una invocación asíncrona. Supongamos que necesitamos un método que realiza un cálculo intensivo y, cuando finalmente obtiene el resultado, lo devuelve en una variable de tipo Double. La manera de declarar dicho método sería similar a esta:

import java.util.concurrent.Future; 
import javax.ejb.AsyncResult; 
import javax.ejb.Asynchronous; 

// ... 

@Asynchronous 
public Future metodoLentoConResultado() {         
    try { 
        Thread.sleep(15000); 
             
        return new AsyncResult(Math.PI); 
    } catch(InterruptedException ie) { 
        throw new RuntimeException(ie); 
    } 
}
	

Como puedes ver, el método metodoLentoConResultado() devuelve un objeto de tipo Future (más concretamente de su implementación AsyncResult) parametizado a Double. Es sobre este objeto Future sobre el que el cliente deberá comprobar si el método asíncrono ha terminado su ejecución, para obtener a continuación su resultado:

import java.util.concurrent.ExecutionException; 
import java.util.concurrent.Future; 
import javax.naming.NamingException; 

// ... 

Future resultado = bean.metodoLentoConResultado(); 
System.out.println("Método asíncrono invocado"); 
System.out.println("Realizando otras tareas mientras se ejecuta el método asíncrono..."); 
// ... 
     
while(true) { 
    if(!resultado.isDone()) { 
        Thread.sleep(2000); 
    } else { 
        System.out.println("Método asincrono devuelve el resultado " + resultado.get()); 
        break; 
    } 
}
	

El ejemplo anterior es parte de un cliente del SLSB asíncrono. En el, en un momento dado, se llama al método asíncrono, asignando el objeto Future devuelto por dicho método en una variable. En este momento el resultado no está listo (recuerda que tardará quince segundos en producirse), pero el control de la ejecución del cliente continua sin esperar. Más adelante el cliente comprueba si el resultado está finalmente disponible mediante el método isDone() de la clase Future. Cuando esto ocurra, podemos obtener nuestro ansiado resultado mediante el método get() (también de la clase Future), el cual devuelve un objeto del mismo tipo al que usamos para parametizar la respuesta del método asíncrono (en nuestro caso, un objeto Double). Es importante tener presente que el método get() bloquea el hilo de ejecución del cliente hasta que el resultado esté listo, de manera que podemos omitir por completo el bucle while:

Future resultado = bean.metodoLentoConResultado(); 
System.out.println("Método asíncrono invocado"); 
System.out.println("Realizando otras tareas mientras se ejecuta el método asíncrono..."); 
// ... 
System.out.println("Método asíncrono devuelve resultado " + resultado.get());
	

La interface Future incluye otros métodos con los que podemos cancelar la ejecución del método asíncrono, o comprobar si dicha ejecución ha sido cancelada por el contenedor (por ejemplo en caso de excepción). Es recomendable que visites la API de Future si vas a trabajar con ella.

Antes de terminar con los métodos asíncronos, quiero hacer constar que en la implementación del contenedor EJB que estamos usando en este tutorial (JBoss 6.0.0 Final) las llamadas asíncronas aún no están completamente implementadas (el hilo de ejecución del cliente se bloquea al llamar al método asíncrono hasta que este ha terminado de ejecutarse; en otras palabras, las llamadas asíncronas se comportan como llamadas síncronas). En otros contenedores compatibles con EJB 3.1, como Glassfish v3, el comportamiento de las llamadas asíncronas sí es el esperado.

3.9 Session Beans en general: Resumen

Con esta sección terminamos de ver los componentes de tipo Session Bean, que son aquellos que contienen la lógica de negocio de una aplicación EJB que puede ser invocada por los clientes de dicha aplicación (ya sea en cualquiera de sus tres variaciones: Stateless, Stateful, o Singleton). Ahora es el momento de pasar a un nuevo tipo de componente con una propósito totalmente distinto: Message-Driven Beans.

3.10 Message-Driven Beans: conceptos básicos

Los componentes de tipo Message-Driven Bean (MDB - Bean Dirigido por Mensajes) son componentes asíncronos de tipo listener. Un MDB no es más que un componente que espera a que se le envie un mensaje, y realiza cierta acción cuando finalmente recibe dicho mensaje (el MDB escucha por si alguien le llama, de ahí su nombre). Algunas propiedades de los componentes MDB son:

  • No mantienen estado
  • Son gestionados por el contenedor (transacciones, seguridad, concurrencia, etc)
  • Son clases puras que no implementan interfaces de negocio (puesto que son invocados por un cliente)

Los componentes MDB forman parte de un sistema de mensajería, el cual se compone de los siguientes subsistemas:

Cliente (Productor)  -->   Broker   -->   Cliente (Consumidor)   -->   Message-Driven Bean
	

En lo que respecta a este tutorial, el cliente consumidor será el propio contenedor EJB, el cual distribuirá los mensajes que consuma a los MDB correspondientes.

Los tres primeros subsistemas (clientes y broker) forman parte del servicio de mensajería, que en la especificación EJB es gestionado por defecto mediante JMS. En este momento es preciso desviarnos del camino para explicar con un mínimo de detalle que es y cómo funciona JMS.

3.11 Java Message Service: conceptos básicos

Java Message Service (JMS - Servicio de Mensajería en Java) es una API neutral que puede ser usada para acceder a sistemas de mensajería. Todos los contenedores EJB 3.x deben proporcionar un proveedor de JMS (el cual define su propia implementación de la API), de manera que podamos trabajar con mensajes sin necesidad de añadir librerías externas. Puesto que esta API es neutral, podemos cambiar en cualquier momento el proveedor JMS por uno que se adecue más a nuestras necesidades (o incluso usar un sistema de mensajería diferente a JMS).

Una aplicación JMS se compone, generalmente, de múltiples clientes JMS y un único proveedor JMS. Un cliente JMS puede ser de dos tipos:

  • Productor: su misión es enviar mensajes
  • Consumidor: su misión es recibir mensajes

Por otro lado, la misión del proveedor JMS es dirigir y enviar los mensajes que le llegan a traves de un broker (esto es algo bastante más complejo, pero por simplicidad vamos a pensar que tenemos un subsistema llamado broker donde se almacenan los mensajes enviados hasta que son servidos a todos sus consumidores). JMS proporciona un tipo de mensajería asíncrona: los clientes JMS envían mensajes a traves del broker sin esperar una respuesta. Es responsabilidad del broker hacer llegar el mesaje a los clientes JMS que deban consumir el mensaje. De esta manera JMS proporciona un sistema de comunicación muy poco acoplado, pues el productor y el consumidor no saben el uno del otro en ningún momento.

3.12 Java Message Service: modelos de mensajería

JMS ofrece dos modelos de mensajería, los cuales nos permiten definir el comportamiento de nuestro sistema de mensajería:

  • Publicar y Suscribir (pub/sub, Publish and Subscribe)
  • Punto a Punto (p2p, Point to Point)

En el modelo pub/sub, un cliente JMS de tipo productor publica sus mensajes en un canal virtual llamado topic (tema). A su vez, uno o varios clientes JMS de tipo consumidor se suscriben a dicho topic si desean recibir los mensajes que en él se publiquen. Si un suscriptor decide desconectarse del topic y más tarde reconectarse, recibirá todos los mensajes publicados durante su ausencia (aunque este comportamiento es configurable). Por todo esto, el modelo pub/sub es de tipo uno-a-muchos (un productor, muchos consumidores).

En el modelo p2p, un cliente JMS de tipo productor publica sus mensajes en un canal virtual llamado queue (cola). De manera similar al modelo pub/sub, pueden existir multiples consumidores conectados al queue. Sin embargo, el queue no enviará automáticamente los mensajes que le lleguen a todos los consumidores, si no que son estos últimos los que deben solicitarlos al queue. De manera adicional, solo un consumidor consumirá cada mensaje publicado: el primero en solicitarlo. Cuando esto ocurra, el mensaje se borrará del queue, y el resto de consumidores no será siquiera consciente de la anterior existencia del mensaje. Por todo esto, el modelo p2p es de tipo uno-a-uno (un productor, un consumidor).

En versiones anteriores a JMS 1.1 cada uno de estos modelos usaba su propio conjunto de interfaces y clases para para el envio y recepción de mensajes. Desde la citada versión de JMS, sin embargo, está disponible una API unificada que es válida para ambos modelos de mensajería.

Ahora es el momento de volver al camino que dejamos dos secciones atrás, y seguir con nuestros amados componentes MDB.

3.13 Message-Driven Beans: el ciclo de vida

Tal como vimos en la sección 3.10, los componentes MDB no mantienen estado entre invocaciones (son stateless, pero no confundir con los componentes SLSB). Por tanto, su ciclo de vida es similar a los SLSB, constando de dos estados:

  • No existe (Does not exists)
  • Preparado en pool (Method-ready pool)

Un MDB en el primer estado es aquel que no ha sido creada aún, y por tanto no existe en memoria. Un MDB en el segundo estado representa una instancia que ha sido instanciada e inicializada por el contenedor. Se llega a este estado cuando se inicia el servidor (que puede decidir crear cierto número de instancias del MDB para procesar mensajes), o cuando el número de instancias en el pool sea insuficiente para atender todos los mensajes que se estén recibiendo. La especificación EJB no fuerza a que exista un pool de MDB's, de manera que una implementación concreta de contenedor EJB ppdría decidir crear una instancia cada vez que se reciba un mensaje (y eliminar esta instancia al terminar de procesar el mensaje) en lugar de mantener un pool de instancias ya preparadas. Este último aspecto, sea como sea, no nos afecta como programadores (al diseñar un MDB) ni como clientes; la forma en que sea gestionada nuestra aplicación es, a priori, responsabilidad exclusiva del contenedor.

Durante la transición entre el primer estado y el segundo, el contenedor realizará las siguientes tres operaciones en el siguiente orden:

  1. Instanciación del MDB
  2. Inyección de cualquier recurso necesario y de dependencias
  3. Ejecución de un método dentro del MDB marcado con la anotación @PostConstruct, si existe

La instanciación del MDB se lleva a cabo mediante reflexión, a traves de Class.newInstance(). Por tanto, el MDB debe tener un constructor por defecto (sin argumentos), ya sea de forma implícita o explícita.

Durante la inyección de dependencias, el contenedor inyectará automáticamente cualquier recurso necesario para el MDB en base a los metadatos que hayamos proporcionado (como una anotación @MessageDrivenContext, o una entrada en el descriptor XML de EJB). De manera adicional, cada vez que un MDB procese un nuevo mensaje, todas las dependencias se inyectarán de nuevo.

Por último, el contenedor ejecutará, si existe, un método dentro del MDB anotado con @PostConstruct (Post construcción). En este método podemos obtener recursos adicionales necesarios para el MDB (como conexiones de red, etc), recursos que permanecerán abiertos hasta la destrucción del MDB. Las reglas de declaración de los métodos callback como @PostConstruct se vieron una y otra vez en el artículo anterior.

Cuando el contenedor no necesita una instancia del MDB (ya sea porque decide reducir el número de instancias en el pool, o porque se esta produciendo un shutdown del servidor), se realiza una transición en sentido inverso: del estado preparado en pool al estado no existe. Durante esta transición se ejecutará, si existe, un método anotado con @PreDestroy (Pre destrucción), donde podemos liberar los recursos adquiridos en @PostConstruct. Es importante tener presente que dentro del método @PreDestroy todavía tenemos acceso al contexto del MDB (lo mismo es válido para todos los Session Bean).

3.14 Message-Driven Beans: definición

Todo MDB debe implementar la interface javax.jms.MessageListener, la cual define el método onMessage(). Es dentro de este método donde se desarrolla toda la acción cuando el MDB procesa un mensaje, como veremos en el ejemplo de la próxima sección. De manera adicional, debemos indicar al contenedor que nuestra clase es un componente MDB. Para ello, utilizamos la anotación @MessageDriven:

package es.davidmarco.ejb.mdb; 

import javax.ejb.MessageDriven; 
import javax.jms.Message; 
import javax.jms.MessageListener; 

@MessageDriven() 
public class PrimerMDB implements MessageListener { 
    public void onMessage(Message message) { 
        // procesar el mensaje 
    } 
}
	

El ejemplo anterior aún no es funcional (producirá un error de despliegue), ya que el MDB necesita saber el tipo de canal virtual (topic/queue) al que debe conectarse, así como el nombre de dicho canal virtual. Esta información forma parte de la configuración del MDB, configuración que proporcionamos al componente a través del atributo activationConfig (configuración de activación) de la anteriormente vista anotación @MessageDriven:

@MessageDriven(activationConfig={ 
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"), 
        @ActivationConfigProperty(propertyName="destination", propertyValue="topic/MiTopic")}) 
public class PrimerMDB implements MessageListener { 
    // ... 
} 
	

En el ejemplo anterior, hemos proporcionado al MDB la configuración necesaria mediante un array de anotaciones @ActivationConfigProperty. Cada una de estas anotaciones contiene dos atributos: propertyName y propertyValue, en los cuales indicamos parejas nombre-de-la-propiedad/valor-de-la-propiedad, respectivamente. Volviendo a nuestro último ejemplo, hemos configurado el MDB con las siguientes propiedades:

  • El tipo de canal virtual al que el MDB se conectará (javax.jms.Topic)
  • El nombre JNDI del canal virtual al que el MDB se conectará (topic/MiTopic).

Otra opción de configuración bastante interesante (aunque opcional) es la duración de la subscripción:

@ActivationConfigProperty(propertyName="subscriptionDurability", propertyValue="Durable")
	

Añadiendo la anotación anterior al array de propiedades de configuración del MDB, nuestro componente recibirá todos los mensajes que se envien a su canal virtual mientras el contenedor EJB (y por tanto el propio componente MDB) esté offline. Esta situación puede ocurrir durante un shutdown del servidor, un problema de conectividad por red, etc. En otras palabras, el mensaje estará disponible hasta que todos sus suscriptores de tipo durable lo hayan recibido y procesado. La opción contraria se aplica cambiando el valor del atributo propertyValue a NonDurable (no durable). La durabilidad de los mensajes solo afecta a los componentes MDB que trabajan con topics (modelo pub/sub), pues en un canal de tipo queue no tiene ningún sentido almacenar un mensaje para componentes MDB offline (en cuanto un MDB este online y lo consuma, el mensaje se eliminará).

Al igual que los componentes Session Bean, los componentes MDB funcionan dentro de un contexto de ejecución. Y por tanto, al igual que los componentes Session Bean, nuestros MDB pueden acceder a su contexto de ejecución si necesitan comunicarse con el contenedor:

import javax.annotation.Resource; 
import javax.ejb.MessageDriven; 
import javax.ejb.MessageDrivenContext; 

@MessageDriven(/*...*/) 
public class PrimerMDB implements MessageListener { 
    @Resource 
    private MessageDrivenContext context; 
     
    // ... 
} 
	

En el ejemplo anterior indicamos al contenedor que inyecte una instancia de MessageDrivenContext mediante la anotación @Resource. MessageDrivenContext extiende la interface EJBContext, sin añadir ningún método. Solamente los métodos transaccionales son útiles al MDB, como se verá cuando trabajemos con transacciones en artículos posteriores. El resto de métodos de la interface lanzará una excepción si son invocados, ya que no tiene sentido que un MDB pueda, por ejemplo, acceder al servicio de seguridad.

Por otro lado, un MDB también puede referenciar un Session Bean para realizar el procesamiento del mensaje (mediante la anotación @EJB, sección 2.5 del artículo anterior), así como enviar él mismo sus propios mensajes mediante JMS (al final de la próxima sección veremos un ejemplo de un MDB procesando mensajes, y a su vez enviando nuevos mensajes que serán procesador por otro MDB).

3.15 Message-Driven Beans: un ejemplo sencillo

Ya hemos visto como los servicios de mensajería asíncronos desacoplan de forma completa al productor de un mensaje de su/s receptor/es: ninguno de ellos es consciente de la parte contraria. Esto debido a que, entre ellos, existe un broker donde se realiza todo el proceso de almacenaje (ya sea en canales virtuales de tipo topic o queue) y reparto de los mensajes que se reciben.

  • Creación de un canal virtual
  • Creación de un componente MDB para consumir mensajes
  • Creación de un cliente para enviar mensajes mediante JMS

El primer paso lógico es crear un canal virtual donde poder enviar mensajes (un topic para modelos pub/sub o un queue para modelos p2p). En JBoss 6.0.0 Final (el servidor de aplicaciones que instalamos en el anexo que acompaña este tutorial), el proveedor JMS incorporado es HornetQ 2.1.2 Final. Para declarar un canal virtual de tipo topic, edita el archivo llamado hornetq-jms.xml del directorio server/default/deploy/hornetq/ de tu instalación de JBoss y añade lo siguiente (default es la configuración por defecto al crear el servidor en Eclipse; si seleccionaste otra configuración, modifica la ruta anterior a la que corresponda):

 
             
     

     
         
     


	

En el archivo XML anterior hemos definido un canal virtual de tipo topic (mediante el elemento <topic>) con nombre PrimerTopic, y lo hemos asociado a la dirección JNDI /topic/PrimerTopic (es necesario reiniciar JBoss si se encuentra levantado). La forma de configurar un canal virtual puede ser diferente entre distintos contenedores (incluso entre distintas implementaciones de un mismo contenedor), por lo que si estás usando un servidor de aplicaciones diferente a JBoss 6.0.0 Final o un proveedor JMS diferente a HornetQ 2.1.2 Final, quizá necesites revisar la documentación correspondiente para declarar el canal virtual.

Aunque este ejemplo se basará en un canal virtual de tipo topic, puedes declarar un queue (para montar un modelo de mensajería p2p) de la siguiente manera:

 
             
     

     
         
     

 
	

El siguiente paso lógico sería escribir un componente MDB que procese los mensajes del topic. Para ello, necesitamos crear un proyecto EJB en Eclipse y declarar una clase como la siguiente:

package es.davidmarco.ejb.mdb; 

import javax.ejb.ActivationConfigProperty; 
import javax.ejb.MessageDriven; 
import javax.jms.JMSException; 
import javax.jms.Message; 
import javax.jms.MessageListener; 
import javax.jms.TextMessage; 

@MessageDriven(activationConfig={ 
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"), 
        @ActivationConfigProperty(propertyName="destination", propertyValue="topic/PrimerTopic")}) 
public class PrimerMDB implements MessageListener { 
     
    @Override 
    public void onMessage(Message message) { 
        if(message instanceof TextMessage) { 
            try { 
                String contenidoDelMensaje = ((TextMessage)message).getText(); 
                System.out.println("PrimerMDB ha procesado el mensaje: " + contenidoDelMensaje); 
            } catch (JMSException jmse) { 
                throw new RuntimeException("Error al procesar un mensaje"); 
            }             
        } 
    } 
}
	

En el ejemplo anterior declaramos un componente MDB (con la anotación @MessageDriven), lo configuramos para actuar como consumidor de los mensajes del topic registrado con dirección JNDI topic/PrimerTopic (con las anotaciones @ActivationConfigProperty), y finalmente añadimos dentro del método onMessage() la lógica que procesará los mensajes. Recuerda que este método es el único declarado en la interface MessageListener, la cual deben implementar todos los MDB basados en JMS.

Es importante entender con un mínimo detalle las operaciones que se realizan dentro de nuestro método onMessage. Este método requiere un parámetro de tipo javax.jms.Message, que es una interface base de la cual extienden otras interfaces más específicas. Una de esas subinterfaces es TextMessage, la cual provee de métodos para trabajar con mensajes cuyo contenido es texto. Por tanto, este MDB está diseñado para recibir mensajes desde el topic asociado, y procesarlos si son de tipo TextMessage (en caso afirmativo, se imprime en el log del servidor el contenido del mensaje). Si durante dicho procesamiento se produce un error, el MDB lanzará una excepción. Lo ideal es que el topic asociado solo reciba este tipo de mensajes, y que mensajes con otro tipo de contenido sean almacenados en otros canales virtuales (de esta manera no se crearán instancias que finalmente no procesarán el mensaje).

Tras desplegar el proyecto EJB con nuestro primer componente MDB, el último paso lógico sería crear un cliente JMS que produjera mensajes y los enviara al topic. Este cliente podría ser, por ejemplo, un proyecto Java normal y corriente. Puesto que necesitamos las librerias de JMS (las cuales son parte de EJB 3.x), debemos incluirlas al crear nuestro proyecto. La forma más sencilla sería pulsando Next en la pantalla de creación del proyecto Java en Eclipse, seleccionando la pestaña Libraries y añadiendo las librerias del servidor JBoss:

Add Library > Server Runtime > Botón Next > JBoss 6.0 Runtime > Botón Finish > Botón Finish
	

Ahora ya estamos listos para escribir el cliente productor JMS:

package es.davidmarco.ejb.cliente; 

import java.util.Properties; 
import javax.jms.Connection; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSException; 
import javax.jms.MessageProducer; 
import javax.jms.Session; 
import javax.jms.TextMessage; 
import javax.jms.Topic; 
import javax.naming.Context; 
import javax.naming.InitialContext; 
import javax.naming.NamingException; 

public class ClienteJMS { 
    public static void main(String[] args) throws NamingException, JMSException { 
        Properties propiedades = new Properties(); 
        propiedades.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); 
        propiedades.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
        propiedades.put("java.naming.provider.url", "jnp://localhost:1099");     
         
        Context contexto = new InitialContext(propiedades); 
        ConnectionFactory factoria = (ConnectionFactory)contexto.lookup("ConnectionFactory"); 
         
        Topic topic = (Topic)contexto.lookup("topic/PrimerTopic"); 
        Connection conexion = factoria.createConnection(); 
        Session sesion = conexion.createSession(false, Session.AUTO_ACKNOWLEDGE); 
        MessageProducer productor = sesion.createProducer(topic); 
        conexion.start(); 
         
        TextMessage mensajeDeTexto = sesion.createTextMessage("Mensaje enviado desde Java"); 
        productor.send(mensajeDeTexto); 
         
        conexion.close(); 
    } 
}
	

En nuestro cliente JMS externo al contenedor lo primero que hacemos es crear un objeto de propiedades con los valores necesarios para conectar con el contenedor EJB (como hicimos en el cliente del primer artículo). A continuación creamos un contexto de ejecución desde el que poder acceder al contenedor, y mediante JNDI obtenemos un objeto factoría ConnectionFactory, a través del cual podremos realizar conexiones para el envío de mensajes.

Ahora viene la parte interesante: creamos un objeto Topic que está asociado al canal virtual que hemos declarado en el primer paso lógico de esta sección (mediante su dirección JNDI), creamos una conexión con el broker mediante el objeto factoría, iniciamos una nueva sesión dentro de la conexión recién creada, creamos un objeto MessageProducer asociado al objeto Topic, iniciamos la conexión para poder enviar un mensaje, creamos un mensaje de tipo texto, lo enviamos, y finalmente cerramos la conexión.

Al ejecutar el cliente, el mensaje se enviará al topic, y puesto que existen clientes consumidores asociados a ese topic, el contenedor EJB consumirá el mensaje, extraerá del pool MDB una instancia del componente MDB (o creará la instancia en el aire; a nosotros nos es indiferente), y le pasará el mensaje al método onMessage() de dicha instancia para que procese el mensaje. Si, tras ejecutar el cliente JMS, miras la pestaña Console de Eclipse, verás el mensaje imprimido en el log de JBoss (tal como se definió al escribir el método onMessage).

Recuerdas el sencillo esquema de la sección 3.10?:

Cliente (Productor)  -->   Broker   -->   Cliente (Consumidor)   -->   Message-Driven Bean
	
  • El cliente Java es el cliente productor
  • JMS (más concretamente su implementación HornetQ) es el broker
  • El contenedor EJB es el cliente consumidor
  • Una instancia MDB es quien procesa el mensaje

Lo interesante de un servicio de mensajería es que el cliente productor no sabe quien consumirá su mensaje, ni como lo hará. Podría ser un MDB, o podría ser otro componente en nada relacionado con la especificación EJB (tal vez ejecutándose en una máquina remota con un sistema operativo distinto). Esto permite una alta interoperabilidad entre sistemas con un muy bajo acoplamiento.

Por último, veamos como hacer que un MDB sea también productor:

package es.davidmarco.ejb.mdb; 

import javax.annotation.Resource; 
import javax.ejb.ActivationConfigProperty; 
import javax.ejb.MessageDriven; 
import javax.ejb.MessageDrivenContext; 
import javax.jms.Connection; 
import javax.jms.ConnectionFactory; 
import javax.jms.JMSException; 
import javax.jms.Message; 
import javax.jms.MessageListener; 
import javax.jms.MessageProducer; 
import javax.jms.Session; 
import javax.jms.TextMessage; 
import javax.jms.Topic; 

@MessageDriven(activationConfig={ 
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"), 
        @ActivationConfigProperty(propertyName="destination", propertyValue="topic/PrimerTopic")}) 
public class PrimerMDB implements MessageListener { 
     
    @Resource 
    private MessageDrivenContext contexto; 
     
    @Override 
    public void onMessage(Message message) { 
        if(message instanceof TextMessage) { 
            try { 
                String contenidoDelMensaje = ((TextMessage)message).getText(); 
                System.out.println("PrimerMDB ha procesado el mensaje: " + contenidoDelMensaje); 
                 
                enviarMensaje("Mensaje enviado desde MDB"); 
            } catch (JMSException jmse) { 
                throw new RuntimeException("Error al procesar un mensaje"); 
            }             
        } 
    } 
     
    private void enviarMensaje(String mensaje) throws JMSException {     
        ConnectionFactory factoria = (ConnectionFactory)contexto.lookup("ConnectionFactory");         
        Topic topic = (Topic)contexto.lookup("topic/SegundoTopic"); 
        Connection conexion = factoria.createConnection(); 
        Session sesion = conexion.createSession(false, Session.AUTO_ACKNOWLEDGE); 
        MessageProducer productor = sesion.createProducer(topic); 
        conexion.start(); 
         
        TextMessage mensajeDeTexto = sesion.createTextMessage(mensaje); 
        productor.send(mensajeDeTexto); 
         
        conexion.close(); 
    } 
}
	

En el ejemplo anterior, dentro del método onMessage() se llama a un método de utilidad que envia un nuevo mensaje a un segundo topic (que debemos haber declarado con anterioridad, por supuesto). La única diferencia entre el código del método de utilidad y el método main() del cliente Java es que, puesto que el primero se está ejecutando dentro del contenedor EJB, podemos obtener el contexto de ejecución mediante inyección de dependencia, evitando así la necesidad de crear el objeto de propiedades y conectar por red al contenedor (lo cual sería bastante absurdo).

Ahora podemos declarar un segundo MDB que procese los mensajes del segundo topic:

package es.davidmarco.ejb.mdb; 

import javax.ejb.ActivationConfigProperty; 
import javax.ejb.MessageDriven; 
import javax.jms.JMSException; 
import javax.jms.Message; 
import javax.jms.MessageListener; 
import javax.jms.TextMessage; 

@MessageDriven(activationConfig={ 
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Topic"), 
        @ActivationConfigProperty(propertyName="destination", propertyValue="topic/SegundoTopic")}) 
public class SegundoMDB implements MessageListener { 
     
    @Override 
    public void onMessage(Message message) { 
        if(message instanceof TextMessage) { 
            try { 
                String contenidoDelMensaje = ((TextMessage)message).getText(); 
                System.out.println("SegundoMDB ha procesado el mensaje: " + contenidoDelMensaje); 
            } catch (JMSException jmse) { 
                throw new RuntimeException("Error al procesar un mensaje"); 
            }             
        } 
    } 
}
	

Cuando PrimerMDB reciba un mensaje, la consola de JBoss (pestaña Console de Eclipse) mostrará como ambos MDB han procesado los mensajes de sus topics correspondientes:

19:00:47,453 INFO [STDOUT] PrimerMDB ha procesado el mensaje: Mensaje enviado desde Java 
19:00:47,475 INFO [STDOUT] SegundoMDB ha procesado el mensaje: Mensaje enviado desde MDB 
	

Resumen

En este tercer artículo del tutorial de EJB hemos terminado de ver los componentes de lado del servidor, además de algunas características interesantes que son nuevas en la especificación EJB 3.1: llamadas asíncronas y vista sin interface (ambas son aplicables a componentes Session Bean, pero no a Message-Driven Beans).

En el próximo artículo veremos el último tipo de componente EJB que nos queda por ver (Entity Beans), así como la manera de realizar persistencia con JPA en una aplicación EJB 3.1. En ningún caso se explicará a fondo JPA (ni que es, ni como funciona), así que si no conoces este framework te aconsejo que visites el tutorial sobre JPA que se encuentra publicado en esta misma web.