icono_twiter icono LinkedIn
Juan Alonso Ramos

Consultor tecnológico de desarrollo de proyectos informáticos.

Ingeniero en Informática, especialidad en Ingeniería del Software

Puedes encontrarme en Autentia: Ofrecemos de servicios soporte a desarrollo, factoría y formación

Somos expertos en Java/J2EE

Ver todos los tutoriales del autor

Fecha de publicación del tutorial: 2011-12-27

Tutorial visitado 6.158 veces Descargar en PDF
Ejecución de tareas asíncronas y planificadas con Spring

Ejecución de tareas asíncronas y planificadas con Spring

 

Índice de contenidos.

1. Introducción

Una vez más gracias a Spring, de forma muy sencilla podemos ejecutar tareas de forma asíncrona. Tareas que no necesitan de una espera hasta que terminan o tareas que requieren mucho tiempo para ser ejecutadas se deben lanzar en un thread independiente para poder seguir con la ejecución de la aplicación sin esperar a que termine la tarea. Para gestionar la creación y finalización de threads, Spring nos facilita la tarea aportándonos adicionalmente mucha más funcionalidad.

En este tutorial veremos algunos ejemplos de código utilizando varias de las funcionalidades que nos proporciona Spring. Si quieres puedes descargarte el código fuente desde aquí.

2. Entorno

  • MacBook Pro 15' (2.4 GHz Intel Core i5, 4GB DDR3 SDRAM).
  • Sistema Operativo: Mac OS X Snow Leopard 10.6.8
  • JDK 1.6.0_29
  • Spring 3.0.5

3. Tareas asíncronas

Para empezar vamos a programar un servicio que se encargue de realizar un conjunto de tareas que requieran bastante tiempo en completar y que nos de algo de juego para el ejemplo. Como estas tareas no son el objetivo del tutorial, sino que lo interesante es ver la manera que tiene Spring de ejecutarlas de forma asíncrona, la clase simulará envíos de correo. En realidad no se enviarán correos sino que ejecutará la acción simulando que tardará 1 segundo y continuará con el siguiente correo.

Lo primero será crear un proyecto nuevo, en mi caso lo he creado con Maven, añadirle las dependencias de Spring y crear un applicacionContext-test.xml configurando la propiedad Spring task:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:task="http://www.springframework.org/schema/task" 
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">

	<context:annotation-config />
	
	<context:component-scan base-package="com.autentia.tutoriales" />
	
	<task:annotation-driven />
	
</beans>

Las líneas 11 y 13 indican a Spring que la configuración se define mediante anotaciones y que se debe escanear en el paquete indicado. La línea 15 es la que nos sirve para configurar Spring para que levante en hilos diferentes los métodos definidos con la anotación @Async. Lo vemos en la clase AsynchronousService donde se define el método sendMails que es asíncrono:

package com.autentia.tutoriales.spring.asynchronous;

import org.springframework.scheduling.annotation.*;
import org.springframework.stereotype.*;

@Service
public class AsynchronousService {;

	private AsyncResult<String> result = new AsyncResult<String>("0 correos enviados");

	@Async
	public void sendMails(int totalMails) {
		for (int i = 1; i <= totalMails; i++) {
			try {
				sendMail(i);
				result = new AsyncResult<String>("Enviados " + i + " de " + totalMails);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private void sendMail(int num) throws InterruptedException {
		Thread.sleep(1000);
		System.out.println("Mail " + num + " enviado.");
	}

	public AsyncResult<String> getResult() {
		return result;
	}

	public String getMailsSender() {
		return result.get();
	}
}

En la línea 9 definimos un atributo result de tipo AsyncResult. Mediante la clase AsyncResult que trabaja con genéricos por lo que podemos añadirle cualquier objeto. Se utiliza para guardar el resultado del método asíncrono de manera que podemos consultarlo a medida que el método sigue en ejecución. En el ejemplo se simula el envío de correos cada segundo y se va almacenando un contador de los que ya han sido enviados. Imagina esta funcionalidad en una aplicación donde se desea mandar un correo de felicitación navideña a los usuarios y de vez en cuando queremos ver el número de correos enviados para ver los que quedan.

En la línea 11 podemos ver la anotación @Async que indica que este método será asíncrono es decir que se lanzará en un hilo independiente.

Ojo, en el ejemplo se utiliza un servicio que es un Singleton por lo que únicamente habrá una instancia de la clase, compartiendo todo el mundo el atributo result. A mí me vale para el ejemplo pero en un entorno concurrente no estaría bien diseñado.

Para probar el servicio creamos un test para que se encargue de llamar al método sendMails:

package com.autentia.tutoriales.spring.asynchronous;

import org.junit.*;
import org.junit.runner.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.test.context.*;
import org.springframework.test.context.junit4.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:applicationContext-test.xml" })
public class AsynchronousServiceTest {

	@Autowired
	private AsynchronousService testAsynch;

	@Test
	public void sendAsynchronousMails() throws Exception {
		testAsynch.sendMails(100);
		System.out.println(testAsynch.getMailsSender());

		sleepALittle(10000);

		System.out.println(testAsynch.getMailsSender());

		stopSendMails();
	}

	private void stopSendMails() {
		testAsynch.getResult().cancel(true);
	}

	private void sleepALittle(int time) {
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

La clase de test no comprueba nada especialmente, sirve para lanzar la ejecución del servicio. Una vez que se llama al método de envío de 100 correos se duerme 10 segundos para dejar tiempo a que el servicio haga algo, podéis ver la traza de ejecución más abajo. Cuando se despierta el thread se llama al método stopSendMails que se encargará de parar el método asíncrono, de esta manera se comprueba que este tipo de métodos pueden ser detenidos antes de su finalización aunque hay que tener en cuenta que su parada no es inmediata, siempre tarda un poco hasta que se finaliza por completo.

La traza que se va dejando de la ejecución es la siguiente:

0 correos enviados
Mail 1 enviado.
Mail 2 enviado.
Mail 3 enviado.
Mail 4 enviado.
Mail 5 enviado.
Mail 6 enviado.
Mail 7 enviado.
Mail 8 enviado.
Mail 9 enviado.
Enviados 9 de 100
Mail 10 enviado.

En primera línea, como aún no le hemos dado tiempo a que el thread de envío de correos empiece, indica que no se ha enviado ningún correo. Seguidamente va saliendo una traza por cada uno de los correos enviados. En la línea 11 sale la traza de la consulta que hacemos antes de parar el servicio de envío de correo. En la última línea nos indica que se ha mandado un último correo ya que la parada del thread no es inmediata.

4. Tareas planificadas

Otra funcionalidad muy común en el desarrollo de aplicaciones es la ejecución de tareas planificadas o repetitivas. En nuestro ejemplo vamos a consultar cada 3 segundos el número de correos que ha enviado y así no lo tendremos que consultar desde la clase de test. Para ello añadimos el método logSendMailsState y lo anotamos @Scheduled configurando el tiempo entre llamadas al método:

	@Scheduled(fixedDelay = 3000)
	public void logSendMailsState() {
		System.out.println(result.get());
	}

Una vez hecho esto, podemos borrar parte del código del cliente que llama al servicio ya que obtendremos cada 3 segundos una traza del servicio de correo:

	@Test
	public void sendAsynchronousMails() throws Exception {
		testAsynch.sendMails(100);

		sleepALittle(10000);

		stopSendMails();
	}
	0 correos enviados
	Mail 1 enviado.
	Mail 2 enviado.
	Mail 3 enviado.
	Enviados 3 de 100
	Mail 4 enviado.
	Mail 5 enviado.
	Mail 6 enviado.
	Enviados 6 de 100
	Mail 7 enviado.
	Mail 8 enviado.
	Enviados 8 de 100
	Mail 9 enviado.
	Mail 10 enviado.

Esta opción también podremos configurarla desde el fichero applicationContext-test.xml

	<task:scheduled-tasks scheduler="myScheduler">
		<task:scheduled ref="asynchronousService" method="logSendMailsState" fixed-rate="3000" />
	</task:scheduled-tasks>
	
	<task:scheduler id="myScheduler" />

También podremos añadirle un cron para que sean lanzadas de forma periódica al estilo: cron="*/5 * * * * MON-FRI".

5.Conclusiones

En este tutorial hemos visto la forma de lanzar tareas en segundo plano o asíncronas de forma sencilla con Spring. Tareas muy comunes como el envío masivo de correos bajo demanda, indexación de contenidos, generación de documentos pesados, consultas muy largas, etc. Si tienes alguna de estas tareas en tu aplicación y el usuario de la misma no debe recibir una respuesta inmediata, es preferible hacerlas de forma asíncrona para no perjudicar el rendimiento de la aplicación y que el usuario no esté un tiempo indefinido esperando la respuesta.

Espero que te haya servido de ayuda.

Un saludo.

Juan.

A continuación puedes evaluarlo:

Regístrate para evaluarlo

Por favor, vota +1 o compártelo si te pareció interesante

Share |
Anímate y coméntanos lo que pienses sobre este TUTORIAL: