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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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:
1 2 3 4 |
@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:
1 2 3 4 5 6 7 8 |
@Test public void sendAsynchronousMails() throws Exception { testAsynch.sendMails(100); sleepALittle(10000); stopSendMails(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
1 2 3 4 5 |
<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.