4. Beans asíncronos, temporizadores e interceptores

4.1. Acceso asíncrono a los beans

Por defecto, todas las llamadas a los beans son síncronas. El cliente realiza la petición y se queda en espera hasta que el bean le manda la respuesta. La especificación 3.1 de EJB introduce la novedad de permitir llamadas asíncronas a los beans de sesión, en las que el cliente lanza la petición al bean y continua haciendo otros trabajos hasta que el bean tiene disponible la respuesta.

Para marcar un método o un bean como asíncrono se utiliza la anotación @Asynchronous. El método tiene que ser void o devolver un objeto del tipo Future<V>.

Si el método asíncrono no devuelve nada, se está utilizando un patrón denominado fire and forget, en el que el cliente lanza la llamada al bean y continua su ejecución sin necesitar los resultados.

La interfaz Future, parametrizada con el tipo de dato que se devolverá cuando el método asíncrono devuelva un resultado, es una interfaz del API de concurrencia de Java. Define los siguientes métodos:

boolean cancel(boolean mayInterruptIfRunning)

Intenta cancelar la ejecución de la tarea.

V get()

Espera si es necesario a que la computación se complete y luego devuelve el resultado.

V get(long timeout, TimeUnit unit)

Igual que el método anterior, pero con un timeout.

boolean isCancelled()

Devuelve true si la tarea ha sido cancelada antes de ser completada normalmente.

` boolean isDone()`

Devuelve true si la tarea ha terminado.

El cliente que llama a un método asíncrono y espera un resultado debe realizar un poolig, preguntando al método isDone si se ha terminado la computación. Cuando se devuelva true el resultado estará disponible en el propio objeto Future y se podrá obtener inmediatamente con el método get.

Por su parte, el cliente deberá devolver un objeto en el método creando un nuevo objeto de tipo AsyncResult:

@Stateless
@Asynchronous
public class MyAsyncBean {
	public Future<Integer> addNumbers(int n1, int n2) {
		Integer result;
		result = n1 + n2;
		// simulamos una consulta muy larga ...
		return new AsyncResult(result);
	}
}

La clase AsyncResult se introduce en la especificación EJB 3.1 para envolver el resultado del método en un objeto Future.

Como cualquier otro bean enterprise, el bean puede inyectarse en cualquier componente Java EE:

@EJB MyAsyncBean asyncBean;
Future<Integer> future = asyncBean.addNumbers(10, 20);

Después se usan los métodos del API Future para consultar si está disponible el resultado con isDone o se cancela su ejecución con el método cancel.

El contexto de transacción del cliente no se propaga al método de negocio asíncrono. El método se comporta como si tuviera la semántica del REQUIRES_NEW.

El rol de seguridad del objeto principal se propaga al método de negocio asíncrono. En este aspecto el método asíncrono se comporta de la misma forma que el síncrono.

4.2. Temporizadores

Las aplicaciones que implementan flujos de trabajo de negocio tienen que tratar frecuentemente con notificaciones periódicas. El servicio temporizador del contenedor EJB nos permite planificar notificaciones para todos los tipos de enterprise beans excepto para los beans de sesión con estado. Podemos planificar una notificación para una hora específica, para después de un lapso de tiempo o a intervalos temporales. Por ejemplo, podríamos planificar los temporizadores para que lancen una notificación a las 10:30 AM del 23 de Mayo, en 30 días o cada 12 horas.

Los temporizadores van descontando el tiempo definido. Cuando llegan a cero (saltan), el contenedor llama al método anotado con la anotación @Timeout en la clase de implementación del bean. El método @Timeout contiene la lógica de negocio que maneja el evento temporizado.

El API está definida por la interfaz TimerService.

4.2.1. El método Timeout

Los métodos anotados con @Timeout en la clase de implementación del bean deben devolver void y tomar un objeto javax.ejb.Timer como su único parámetro. No pueden lanzar excepciones capturadas por la aplicación.

@Timeout
public void timeout(Timer timer) {
    System.out.println("TimerBean: timeout occurred");
}

4.2.2. Creando temporizadores

Para crear un temporizador, el bean debe invocar uno de los métodos createTimer() de la interfaz TimerService. Cuando el bean invoca el método createTimer(), el servicio temporizador comienza a contar hacia atrás la duración del timer.

Como ejemplo veamos el siguiente bean de sesión:

@Stateless
public class TimerSessionBean implements TimerSessionLocal {
    @Resource
    TimerService timerService;

    public void setTimer(long intervalDuration) {
        Timer timer = timerService.createTimer(
                    intervalDuration,
                    "Creado un nuevo timer");
    }

    @Timeout
    public void timeout(Timer timer) {
        System.out.println("Se ha lanzado el timeout");
    }
}

Vemos que el bean define un método setTimer() en el que se usa el objeto timerService obtenido en línea 3 con la anotación @Resource. En este método definimos un temporizador inicializándolo a los milisegundos determinados por intervalDuration.

El temporizador puede ser un temporizador de parada única, en el que podemos indicar el momento de la parada o el tiempo que debe transcurrir antes de la misma, o un temporizador periódico, que se lanza a intervalos regulares. Básicamente son posibles cuatro tipos de temporizadores:

  • Parada única con tiempo: createTimer(long timeoutDuration, Serializable info)

  • Parada única con fecha: createTimer(Date firstDate, Serializable info)

  • Periódico con intervalos regulares y tiempo inicial: createTimer(long timeoutDuration, long timeoutInterval, Serializable info)

  • Periódico con intervalos regulares y fecha inicial: createTimer(Date firstDate, long timeoutInterval, Serializable info)

Veamos un ejemplo de utilización de temporizadores periódico con fecha inicial. Supongamos que queremos un temporizador que expire el 1 de Mayo de 2008 a las 12h. y que, a partir de esa fecha, expire cada tres días:

Calendar unoDeMayo = Calendar.getInstance();
unoDeMayo.set(2008, Calendar.MAY, 1, 12, 0);
long tresDiasEnMilisecs = 1000 * 60 * 60 * 24 * 3;
timerService.createTimer(unoDeMayo.getTime(),
                         tresDiasEnMilisecs,
                         "Mi temporizador");

Los parámetros Date y long representan el tiempo del temporizador con una resolución de milisegundos. Sin embargo, debido a que el servicio de temporización no está orientado hacia aplicaciones de tiempo real, el callback al método @Timeout podría no ocurrir con precisión de milisegundos. El servicio de temporización es para aplicaciones de negocios, que miden el tiempo normalmente en horas, días o incluso duraciones más largas.

Los temporizadores son persistentes. Si el servidor se apaga (o incluso se cae), los temporizadores se graban y se activarán de nuevo en el momento en que el servidor vuelve a poner en marcha. Si un temporizador expira en el momento en que el servidor está caído, el contenedor llamará al método @Timeout cuando el servidor se vuelva a comenzar.

4.2.3. Cancelando y grabando temporizadores

Los temporizadores pueden cancelarse con los siguientes eventos:

  • Cuando un temporizador de tiempo expira, el contenedor EJB llama al método @Timeout y cancela el temporizador.

  • Cuando el bean invoca el método cancel de la interfaz Timer, el contenedor cancela el timer.

Para grabar un objeto Timer para rerferencias futuras, podemos invocar su método getHandle() y almacenar el objeto TimerHandle en una base de datos (un objeto TimerHandle es serializable). Para reinstanciar el objeto Timer, podemos recuperar el TimerHandle de la base de datos e invocar en él el método getTimer(). Un objeto TimerHandle no puede pasarse como argumento de un método definido en un interfaz remoto. Esto es, los clientes remotos no pueden acceder al TimerHandle del bean. Los clientes locales, sin embargo, no sufren esta restricción.

4.2.4. Obteniendo información del temporizador

La interfaz Timer define también métodos para obtener información sobre los temporizadores:

public long getTimeRemaining();
   public java.util.Date getNextTimeout();
   public java.io.Serializable getInfo();

El método getInfo() devuelve el objeto que se pasó como último parámetro de la invocación createTimer. Por ejemplo, en el código anterior esta información es la cadena “Creado un nuevo timer”.

También podemos recuperar todos los temporizadores activos de un bean con el método getTimers(), que devuelve una colección de objetos Timer.

4.2.5. Temporizadores y transacciones

Los enterprise bean normalmente crean temporizadores dentro de transacciones. Si la transacción es anulada, también se anula automáticamente la creación del temporizador. De forma similar, si un bean cancela un temporizador dentro de una transacción que termina siendo anulada, la cancelación del temporizador también se anula.

En los beans que usan transacciones gestionadas por el contenedor, el método @Timeout tiene normalmente el atributo de transacción REQUIRES o REQUIRES_NEW para preservar la integridad de la transacción. Con estos atributos, el contenedor EJB comienza una nueva transacción antes de llamar al método @Timeout. De esta forma, si la transacción se anula, el contenedor volverá a llamar al método @Timeout de nuevo con los valores iniciales del temporizador.

4.2.6. Ventajas y limitaciones

Entre las ventajas de los temporizadores frente a la utilización de algún otro tipo de planficadores podemos destacar:

  • Los temporizadores son parte del estándar Java EE con lo que la aplicación será portable y no dependerá de APIs propietarias del servidor de aplicaciones.

  • La utilización del servicio de temporización de EJB viene incluido en Java EE y no tiene ningún coste adicional. No hay que realizar ninguna configuración de un planificador externo y el desarrollador no tiene que preocuparse de buscar uno.

  • El temporizador es un servicio gestionado por el contenedor, y no se requiere un thread separado como en un planificador externo.

  • Las transacciones se soportan completamente.

  • Por defecto, los temporizadores son objetos persistentes que sobreviven a caídas del contenedor.

Limitaciones

  • La mayoría de planificadores proporcionan la posibilidad de utilizar clases Java estándar; sin embargo los temporizadores requieren el uso de enterprise beans.

  • Los temporizadores EJB adolecen del soporte de temporizadores al estilo cron, fechas de bloqueo, etc. disponibles en muchos planificadores de tareas.

4.3. Interceptores

Los interceptores (interceptors) son objetos que son capaces de interponerse en las llamadas a los métodos en los eventos de ciclo de vida de los beans de sesión y de mensaje. Nos permiten encapsular conductas comunes a distintas partes de la aplicación que normalmente no tienen que ver con la lógica de negocio. Los interceptores son una característica avanzada de la especificación EJB que nos permite modularizar la aplicación o incluso extender el funcionamiento del contenedor EJB. En esta sesión veremos una introducción a la definición y al funcionamiento de los interceptores para interponerse en las llamadas a los métodos de negocio.

Toda su API está definida en el package javax.interceptor.

4.3.1. La clase Interceptor

Comencemos con un ejemplo. Supongamos que queremos analizar el tiempo que tarda en ejecutarse un determinado método de negocio de un bean.

   public void addMensajeAutor(String nombre, String texto) {
      long startTime = System.currentTimeMillis();
      Autor autor = findAutor(nombre);
      if (autor == null) {
         autor = new Autor();
         autor.setNombre(nombre);
         em.persist(autor);
      }
      mensajeService.addMensaje(texto, nombre);
      long endTime = System.currentTimeMillis() - startTime;
      System.out.println("addMensajeAutor() ha tardado: " +
              endTime + " (ms)");*
   }

Aunque el método compilará y se ejecutará correctamente, el enfoque tiene serios problemas de diseño:

  • Se ha añadido en el método addMensajeAutor() código que no tiene nada que ver con la lógica de negocio de la aplicación. El código se ha hecho más complicado de leer y de mantener.

  • El código de análisis no se puede activar y desactivar a conveniencia. Hay que comentarlo y volver a recompilar la aplicación.

  • El código de análisis es una plantilla que podría reutilizarse en muchos métodos de la aplicación. Pero escrito de esta forma habría que escribirlo en todos los métodos en los que queramos aplicarlo.

Los interceptores proporcionan un mecanismo para encapsular este tipo de código de una forma sencilla y aplicarlo a nuestros métodos sin interferir directamente. Los interceptores proporcionan una estructura para este tipo de conducta de forma que puedan ser ampliados y extendidos fácilmente en una clase. Por último, proporcionan un mecanismo simple y configurable para aplicar la conducta en el lugar que deseemos.

Este tipo de código que envuelve los métodos de la aplicación se suele denominar también aspecto y es la base de una técnica de programación denominada AOP (Aspect Oriented Programming). Un framework muy popular de programación dirigida por aspectos en Java es AspectJ.

Vamos ya a detallar cómo implementar los interceptores. Es muy sencillo. Basta con crear una clase Java con un método precedido por la anotación @javax.interceptor.AroundInvoke y con la siguiente signatura:

@AroundInvoke
Object <nombre-metodo>(javax.interceptor.InvocationContext invocation)
   throws Exception;

El método @AroundInvoke en una clase de intercepción envuelve la llamada al método de negocio y es invocado en la misma pila de llamada, en la misma transacción y en el mismo contexto de seguridad que el método que se está interceptando. El parámetro javax.interceptor.InvocationContext es una representación genérica del método de negocio que el cliente está invocando. A través de él podemos obtener información como el bean al que se está llamando, los parámetros que se están pasando en forma de un array de objetos, y una referencia al objeto java.lang.reflect.Method que contiene la representación del método invocado. InvocationContext también se usa para dirigir realmente la invocación. Veamos cómo utilizar este enfoque para hacer el análisis del tiempo anterior:

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

public class Profiler {

   @AroundInvoke (1)
   public Object profile(InvocationContext invocation)
           throws Exception {
      long startTime = System.currentTimeMillis();
      try {
         return invocation.proceed(); (2)
      } finally {
         long endTime = System.currentTimeMillis() - startTime; (3)
         System.out.println("El método " + invocation.getMethod() +
                 " ha tardado " + endTime + " (ms)");
      }
   }
}
1 Marcamos el método profile como @AroundInvoke
2 Llamada al método que estamos interceptando
3 Cálculo final del tiempo de ejecución

El método anotado con @AroundInvoke en nuestra clase interceptora es el método profile. Tiene un aspecto muy parecido al código que escribimos en el método addMensajeAutor, con la excepción de que no incluimos la lógica de negocio. Se invoca a este método con InvocationContext.proceed(). Si se debe llamar a otro interceptor el método proceed() realiza esa llamada. Si no hay ningún otro interceptor en espera, el contenedor EJB llama entonces al método del bean que está siendo invocado por el cliente.

En el finally se calcula el tiempo de ejecución y se imprime en la salida estándar. El método InvocationContext.getMethod() proporciona acceso al objeto java.lang.reflect.Method que representa el método real que se está invocando. Se usa en la línea 14 para imprimir el nombre del método al que se está llamando.

La estructura del código permite capturar las posibles excepciones que pudiera generar la invocación del método del bean. Al poner el cálculo del tiempo final en la parte finally nos aseguramos de que siempre se ejecuta.

Además del método getMethod(), la interface InvocationContext tiene otros métodos interesantes. En el siguiente enlace se puede consultar su API en Java EE 7.

package javax.interceptor;

public interface InvocationContext {
   public Object getTarget();
   public Method getMethod();
   public Object[] getParameters();
   public void setParameters(Object[] newArgs);
   public java.util.Map<String, Object> getContextData();
   public Object proceed() throws Exception;
}

El método getTarget() devuelve una referencia a la instancia del bean objetivo. Podríamos cambiar nuestro método profile para que imprimiera los parámetros del método al que se está invocando utilizando el método getParameters(). El método setParameters() permite modificar los parámetros de la invocación al método del bean, de forma que podemos hacer que el bean ejecute el código con los parámetros proporcionados por el interceptador. El método getContextData() devuelve un objeto Map que está activo durante toda la invocación al método. Los interceptores pueden utilizar este objeto para pasarse datos de contexto entre ellos durante la misma invocación.

Una vez que hemos escrito la clase de intercepción, es hora de aplicarlo a la llamada al bean. Podemos hacerlo utilizando anotaciones o utilizando descriptores de despliegue XML. Veremos ambas opciones en los siguientes apartados.

4.3.2. Aplicando interceptores con anotaciones

La anotación @javax.interceptor.Interceptors se puede usar para aplicar interceptores a un método particular de un bean o todos los métodos de un bean. Para aplicar el método anterior de profiling basta con aplicar la anotación en su definición:

  @Interceptors(Profiler.class)
   public void addMensajeAutor(String nombre, String texto) {
      // ...
   }

La anotación @Interceptors también puede aplicarse a todos los métodos de negocio de un bean realizando la anotación en la clase:

@Stateless
@Interceptors(Profiler.class)
public class AutorServiceBean implements AutorServiceLocal {

   @PersistenceContext(unitName = "ejb-jpa-ejbPU")
   EntityManager em;
   @EJB
   MensajeServiceDetachedLocal mensajeService;

   public Autor findAutor(String nombre) {
      return em.find(Autor.class, nombre);
   }
   // ...

4.3.3. Aplicando interceptores con XML

Aunque la anotación @Interceptors nos permite aplicar fácilmente los interceptores, tiene el inconveniente de que nos obliga a modificar y recompilar el código cada vez que queremos añadirlos o deshabilitarlos. Es más interesante definir los interceptores mediante XML, en el fichero descriptor de despliegue ejb-jar.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee"
         version = "3.0"
         xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
<assembly-descriptor>
   <interceptor-binding>
      <ejb-name>AutorServiceBean</ejb-name>
      <interceptor-class>es.ua.jtech.ejb.interceptor.Profiler</interceptor-class>
      <method>
         <method-name>addMensajeAutor</method-name>
      </method>
   </interceptor-binding>
</assembly-descriptor>
</ejb-jar>

Una de las ventajas de utilizar el descriptor de despliegue XML es que es posible aplicar el interceptor a todos los métodos de todos los beans desplegados en el módulo:

<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee"
         version = "3.0"
         xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
<assembly-descriptor>
   <interceptor-binding>
      <ejb-name>*</ejb-name>
      <interceptor-class>es.ua.jtech.ejb.interceptor.Profiler</interceptor-class>
   </interceptor-binding>
</assembly-descriptor>
</ejb-jar>

4.4. Ejercicios

4.4.1. (0,75 puntos) Bean temporizador

En el módulo saludo añade un ejemplo de bean temporizador, junto con un servlet que lo lance. Actualiza en index.jsp para poder invocar el servlet que lanza el bean.

4.4.2. (0,75 puntos) Bean interceptor

Define también en el módulo saludo un bean interceptor que escriba en la salida estándar todas las peticiones de saludo realizadas y todos los saludos devueltos.