Tareas programadas en Spring

logo spring

Es posible que al desarrollar una aplicación la mejor solución para ciertas funcionalidades sea la implementación de tareas programables, esto es, métodos que se ejecuten periódicamente en momentos determinados o bien cada cierto periodo de tiempo. El caso más típico consiste en la revisión de la base de datos para la generación de informes de seguimiento, el envío de notificaciones, realizar tareas de sicronización con otros sistemas, etc. Asimismo, estas tareas suelen formar parte de la propia aplicación ya que así se pueden utilizar los servicios, capa DAO, etc de la misma.

En Java se suele utilizar Quartz, sin embargo si utilizamos Spring y la casuística es sencilla no tenemos que recurrir a ninguna librería de terceros ya que con Spring 3 es sencillo definir ejecuciones períódicas de un método de un bean y es precisamente lo que se verá de forma práctica en este artículo.

Entorno de pruebas:

Requisitos: Conocimientos básicos de Maven y Spring IoC.

Proyecto para pruebas

Se va a utilizar una clase Main para realizar las pruebas con un bean de Spring que contiene métodos que se ejecutan de forma periódica. Puesto que todo lo necesario ya viene incluído en el “core” de Spring no se necesita ninguna dependencia adicional. El pom.xml completo es el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.danielme.blog.spring</groupId>
    <artifactId>schedule</artifactId>
    <version>1.0</version>    
    <packaging>jar</packaging>
    
    <name>Spring - Scheduled tasks</name>
    <description>Spring - Scheduled tasks</description>
    <url>https://danielme.com/2014/05/12/tareas-programadas-en-spring</url>
    <inceptionYear>2014</inceptionYear>
    
    <scm>
    	<url>https://github.com/danielme-com/spring-scheduled-tasks</url>
    </scm>
    
    <licenses>
		<license>
			<name>GPL 3</name>
			<url>http://www.gnu.org/licenses/gpl-3.0.html</url>
		</license>
	</licenses>

    <properties>
		<java.version>1.6</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		
		<maven.compiler.plugin.version>3.1</maven.compiler.plugin.version>	
		<maven.eclipse.plugin.version>2.9</maven.eclipse.plugin.version>		
		<maven.shade.plugin.version>2.1</maven.shade.plugin.version>
		<maven.jar.plugin.version>2.4</maven.jar.plugin.version>
		<project.build.mainClass>com.danielme.blog.spring.scheduled.Main</project.build.mainClass>
		<maven.exec.plugin.version>1.2.1</maven.exec.plugin.version>
		
		<spring.version>4.0.4.RELEASE</spring.version>	
		<slf4j.log4j12.version>1.7.6</slf4j.log4j12.version>
		
	</properties>
    
    
	<build>
		<plugins>
		
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>${maven.compiler.plugin.version}</version>
				<configuration>
					<encoding>${project.build.sourceEncoding}</encoding>
					<source>${java.version}</source>
					<target>${java.version}</target>
					<compilerArgument>-proc:none</compilerArgument>
				</configuration>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-eclipse-plugin</artifactId>
				<version>${maven.eclipse.plugin.version}</version>
				<configuration>
					<projectNameTemplate>${project.name}</projectNameTemplate>
					<wtpmanifest>false</wtpmanifest>
					<wtpapplicationxml>false</wtpapplicationxml>
				</configuration>
			</plugin>
			
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jar-plugin</artifactId>
				<version>${maven.jar.plugin.version}</version>
				<configuration>
					<archive>
						<manifest>
							<addClasspath>true</addClasspath>
							<mainClass>${project.build.mainClass}</mainClass>
						</manifest>
						<manifestEntries>
							<Implementation-Version>${project.version}</Implementation-Version>
							<Implementation-Title>${project.artifactId}</Implementation-Title>
							<Extension-Name>${project.artifactId}</Extension-Name>
							<Built-By>danielme.com</Built-By>
						</manifestEntries>
					</archive>
				</configuration>
			</plugin>

			<!-- mvn clean package exec:java -->
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>${maven.exec.plugin.version}</version>
				<executions>
					<execution>
						<goals>
							<goal>java</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<mainClass>${project.build.mainClass}</mainClass>
				</configuration>
			</plugin>


			<!-- package all dependencies into an executable jar -->
			<!-- mvn clean package shade:shade -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-shade-plugin</artifactId>
				<version>${maven.shade.plugin.version}</version>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>shade</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<finalName>${project.artifactId}-${project.version}-full</finalName>
					<transformers>
						<transformer
							implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
							<mainClass>${project.build.mainClass}</mainClass>
						</transformer>
					</transformers>
						<!-- http://zhentao-li.blogspot.com.es/2012/06/maven-shade-plugin-invalid-signature.html -->
					<filters>
						<filter>
							<artifact>*:*</artifact>
							<excludes>
								<exclude>META-INF/*.SF</exclude>
								<exclude>META-INF/*.DSA</exclude>
								<exclude>META-INF/*.RSA</exclude>
							</excludes>
						</filter>
					</filters>
				</configuration>
			</plugin>

		</plugins>	
	</build>

    <dependencies>
	
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring.version}</version>
		</dependency>

       <dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>${slf4j.log4j12.version}</version>
		</dependency>

    </dependencies>

   
</project>

Se puede generar un proyecto para Eclipse con el siguiente comando:

$mvn clean eclipse:eclipse

El proyecto completo presenta la siguiente estructura:

eclipse-project

Definiendo tareas mediante anotaciones

Primero veamos cómo programar tareas utilizando anotaciones. Para ello, vamos a “activar” las anotaciones tanto para los beans como para las tareas.

<?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-4.0.xsd  
	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-4.0.xsd  
	http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd">
	
	<context:annotation-config />	

	<context:component-scan base-package="com.danielme.blog" />
	    
	<task:annotation-driven />
		
</beans>

Para “programar” la ejecución automática de un método de un bean se utiliza la anotación Scheduled. Los parámetros que admiten son los siguientes (comprobar en el javadoc su disponibilidad según la versión de Spring):

  • initialDelay: milisegundos desde la instanciación del bean hasta la primera ejecución programada del método.
  • fixedDelay: milisegundos tras el fin de la última ejecución del método que deben pasar hasta una nueva ejecución del método. Lo usaremos para métodos que se deban ejecutar con una frecuencia muy alta y que sean susceptibles de durar bastante.
  • fixedRate: el método se ejecutará según el periodo indicado en milisegundos, sin tenerse en cuenta si la última ejecución ya terminó.
  • cron:Permite definir una regla cron estilo Linux, estableciendo un periodo regular de ejecución de forma muy precisa, por ejemplo a cierta hora de un día de la semana.

Para probar estas opciones crearemos un bean llamado ScheduledTasks. Los métodos programados simplemente imprimirán en el log su ejecución.

package com.danielme.blog.spring.scheduled;

import org.apache.log4j.Logger;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class ScheduledTasks 
{
	private static final Logger LOG = Logger.getLogger(ScheduledTasks.class);
	
	@Scheduled(initialDelay=30000, fixedDelay = 1500)
	public void task1()
	{
		LOG.info("task1");
	}
	
	@Scheduled(cron="*/10 * * * * ?")
	public void task2()
	{
		LOG.info("task2");
	}
	
}

Para que estos métodos se ejecuten lo más sencillo es crear un Main que arranque el contexto de Spring.

package com.danielme.blog.spring.scheduled;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main 
{

	public static void main(String[] args)
	{
		 new ClassPathXmlApplicationContext("/applicationContext.xml");
	}
}

Ejecutando el Main veremos en la salida estándar configurada en el log4j.xml (System.out) la traza con la ejecución de las tareas programadas. La tarea dos se ejecuta cada diez segundos y la uno cada segundo y medio a partir del segundo 30, tal y como se ha indicado y se puede comprobar en el log:

05-10-2014 09:33:30,001 PM  INFO ScheduledTasks:22 - task2
05-10-2014 09:33:40,000 PM  INFO ScheduledTasks:22 - task2
05-10-2014 09:33:50,000 PM  INFO ScheduledTasks:22 - task2
05-10-2014 09:33:55,133 PM  INFO ScheduledTasks:16 - task1
05-10-2014 09:33:56,633 PM  INFO ScheduledTasks:16 - task1
05-10-2014 09:33:58,134 PM  INFO ScheduledTasks:16 - task1
05-10-2014 09:33:59,635 PM  INFO ScheduledTasks:16 - task1
05-10-2014 09:34:00,000 PM  INFO ScheduledTasks:22 - task2

Personalmente considero una buena práctica definir los intervalos de tiempo fuera de las clases y en un mismo fichero para tener esta configuración centralizada y de fácil acceso y modificación. Esta estrategia permite modificar los intervalos sin tener que recompilar y empaquetar toda la aplicación lo que es especialmente útil para hacer cambios de emergencia en aplicaciones en producción😉. Para conseguir esto, voy a aplicar lo visto en el artículo Ficheros .properties en Spring IoC y crear el archivo /src/main/resources/tasks.properties.

task1.initialDelay=30000
task1.fixedDelay=1500
task2.cron="*/10 * * * * ?"

Se importa este fichero en el contexto de Spring en el applicationContext:

<context:property-placeholder location="classpath:tasks.properties"/>

La clase quedará tal que así, obsérvese que ahora se utilizan los atributos initialDelayString y fixedDelayString.

package com.danielme.blog.spring.scheduled;

import org.apache.log4j.Logger;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class ScheduledTasks {

	private static final Logger LOG = Logger.getLogger(ScheduledTasks.class);
	
	@Scheduled(initialDelayString="${task1.initialDelay}", fixedDelayString = "${task1.fixedDelay}")
	public void task1()
	{
		LOG.info("task1");
	}
	
	@Scheduled(cron="${task2.cron}")
	public void task2()
	{
		LOG.info("task2");
	}
	
}

Configuración en XML

La definición de tareas programadas se puede realizar de forma “tradicional” directamente en XML sin utilizar anotaciones ya sea por deseo o necesidad. El siguiente snippet de código permite replicar la última configuración que hemos visto en el applicationContext:


  	<context:property-placeholder location="classpath:tasks.properties"/>
  	
  	<bean id="scheduledTasks" class="com.danielme.blog.spring.scheduled.ScheduledTasks" />
 
     <task:scheduled-tasks>
        <task:scheduled ref="scheduledTasks" method="task1" initial-delay="${task1.initialDelay}" fixed-delay="${task1.fixedDelay}"/>
        <task:scheduled ref="scheduledTasks" method="task2" cron="${task2.cron}"/>
    </task:scheduled-tasks>

Código de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: