Tareas cron en Java con Quartz Scheduler y Spring MVC

Tareas cron en Java con Quartz Scheduler y Spring MVC

Existen numerosas posibilidades para ejecutar procesos Java programados en un servidor. Desde hace algún tiempo nosotros estamos utilizando Quartz Scheduler integrado con Spring MVC, que es el framework con el que más trabajamos últimamente.

Este tipo de integraciones permite que los procesos se ejecuten dentro del propio contexto del proyecto (más o menos), algo muy diferente, por ejemplo, a compilar pequeños programas jar y lanzarlos mediante herramientas cron externas en el sistema operativo de nuestro servidor.

En este artículo trataremos de explicar como integrar Quartz Scheduler con Spring MVC y como configurar nuestras tareas para ejecuciones programadas.

Configurar Quartz en Spring MVC

Partimos de la base de un proyecto Maven, por lo que en primer lugar será necesario modificar el pom.xml añadiendo las dependencias de Quartz.
En nuestro caso, por compatibilidad con la versión de Spring, trabajamos con Quartz 1.8.5. Si decides utilizar alguna versión posterior es posible que algunas de las configuraciones que damos no sean compatibles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- ... cosas antes -->
<!-- Necesitaremos incluir el soporte para contextos de Spring, si no lo tenemos -->
<dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-context-support</artifactId>
     <version>${spring.version}</version>
</dependency>
 
<!-- Añadimos las dependencias de Quartz -->
<dependency>
     <groupId>org.quartz-scheduler</groupId>
     <artifactId>quartz</artifactId>
     <version>1.8.5</version>
</dependency>
<!-- ... cosas después -->

Una vez tengamos las librerías, creamos el fichero spring-quartz.xml en las fuentes del proyecto (en nuestro caso suele ser src/main/resources), y añadimos este fichero en la lista de ficheros de contexto del web.xml:

1
2
3
4
5
6
7
8
9
10
<!-- ... cosas antes -->
<context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
            <!-- ... otros ficheros del classpath ... -->
            <!-- Añadimos nuestro fichero de configuración Quartz -->
            classpath:spring-quartz.xml
      </param-value>
</context-param>
<!-- ... cosas después -->

Ahora vamos a ver el aspecto del spring-quartz.xml.

Básicamente, este fichero contendrá la configuración de las tareas que queramos programar y se divide en 3 secciones:

  • Definición de tareas: clase del job y mapa de dependencias.
  • Definición de triggers: job asociado al cron y configuración de las ejecuciones.
  • Programador: combina e inicializa las definiciones anteriores.

Para que resulte más sencillo, veamos un ejemplo:

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
40
41
42
43
44
45
46
47
48
49
50
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
<!-- Definición de jobs -->
<!-- Un job necesita de un identificador único -->
<bean id="uniqueJobIdentifier" class="org.springframework.scheduling.quartz.JobDetailBean">
      <!-- Es necesario indicar la clase que contienen tu tarea -->
      <property name="jobClass" value="com.package.example.jobs.MyJobClassName" />
      <!-- También indicaremos aquellos servicios que se usarán en la tarea, en caso de que se requiera de alguno, para que Spring los incluya en el contexto -->
      <property name="jobDataAsMap">
            <map>
                  <entry key ="customService" value-ref="customServiceImpl"/>
                  <entry key ="exampleService" value-ref="exampleServiceImpl"/>
            </map>
      </property>
</bean>
<!-- ... otros jobs -->
 
 
<!-- Definición de Triggers -->
<!-- Un trigger necesita de un identifiacor único -->
<bean id="uniqueTriggerIdentifier" class="org.springframework.scheduling.quartz.CronTriggerBean">
      <!-- Un trigger se asocia a un job mediante su identificador único -->
      <property name="jobDetail" ref="uniqueJobIdentifier"/>
      <!-- Existen varias posibilidades en este punto. Nosotros trabajamos con expresiones cron definidas en un fichero properties para usar diferentes configuraciones según el entorno de ejecución -->
      <property name="cronExpression" value="${properties.uniqueTriggerIdentifier.cronExpression}" />
</bean>
<!-- ... otros triggers -->
 
 
<!-- Configuración del sistema -->
<!-- Finalmente usaremos la clase SchedulerFactoryBean de Quartz para programar e iniciar el sistema de tareas -->
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
      <property name="jobDetails">
            <!-- En esta lista añadiremos todos los jobs definidos previamente -->
            <list>                  
                  <ref bean="uniqueJobIdentifier"/>
            </list>
      </property>
      <property name="triggers">
            <!-- En esta lista añadiremos todos los triggers definidos previamente -->
            <list>
                  <ref bean="uniqueTriggerIdentifier"/>
            </list>
      </property>
</bean>
 
</bean>

De este modo, con el arranque de la aplicación se programarán nuestras tareas y, llegado el momento definido para la ejecución, se ejecutarán correctamente.

Ahora solo faltaría revisar la estructura java de un job de Quartz, lo que sería nuestra clase MyJobClassName.java, que no es más que una extensión de la clase QuartzJobBean con un método que tendremos que redefinir, llamado executeInternal.

Mejor con un ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ... cosas antes
public class MyJobClassName extends QuartzJobBean {
 
      //Servicios que se usarán en la tarea. En un Job no es posible usar la funcionalidad de Spring para inyectar las dependencias.
      private CustomService customService;
      private ExampleService exampleService;
 
      @Override
      protected void executeInternal(JobExecutionContext jobContext) throws JobExecutionException {
 
            //Recuperamos las dependencias de servicios necesarios del contexto de Spring (porque fueron previamente definidos en spring-quartz.xml)
            this.customService = (CustomService) jobContext.getJobDetail().getJobDataMap().get("customService");
            this.exampleService = (ExampleService) jobContext.getJobDetail().getJobDataMap().get("exampleService");
 
            // ... cosas después
 
      }
 
}

Y esto sería todo. Es importante saber que los jobs se ejecutan en threads diferentes al contexto de la aplicación con los inconvenientes que esto conlleva: posibles accesos concurrentes, «desconocimiento» del estado de ejecución del thread… De todos modos, es algo inevitable en este tipo de procesos. Hay que tener cuidado y definir sistemas de bloqueos y alertas para evitar sustos innecesarios.

¡A programar!

Publicado en abril 8, 2015

,

,

7 comentarios en Tareas cron en Java con Quartz Scheduler y Spring MVC
  1. Erik dice:

    Agradezco mucho tu ayuda… quizas te hare una pregunta muy tonta, existe la forma de usar injeccion de dependencia directamente en la clase MyJobClassName sin usar un archivo de configuracion spring-quartz.xml porque yo necesito inyectar una clase de este tipo
    public interface ProcesoMallaRepository extends JpaRepository

    que es donde obtengo todos los procesos almacenados de la base de datos para ser croneado…

    Ojala puedas ayudarme.,.

    gracias

    • xrodriguez dice:

      Hola Erik,

      Muchas gracias por tu comentario.

      Si entiendo bien tu pregunta, creo que la respuesta sería no.

      En un principio, no sería posible utilizar la inyección de dependencias dentro de un Job debido a que trabaja en un contexto diferente al principal de la aplicación, y no tiene referencias a otras entidades definidas en Spring, salvo las que configures en el propio XML, y que habría que recuperar con el objeto jobContext.

      Si quieres, puedes mandarnos un correo para profundizar en este tema.

      Un saludo.

  2. jose dice:

    Hola, me gustaría saber a que te refieres con «Hay que tener cuidado y definir sistemas de bloqueos y alertas para evitar sustos innecesarios.» podrías dar un link a un ejemplo o algo para saber de que hablas exactamente.

  3. dfernandez dice:

    Hola Jose,

    en primer lugar, gracias por tu comentario y sentimos la demora en la respuesta.

    Con sistemas de bloqueos nos referimos a, por ejemplo en java, semáforos, regiones críticas o métodos y variables synchronized (https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html). Estos sistemas suelen ser necesarios en aplicaciones multi-hilo para evitar situaciones indeseadas al poder suceder que varios hilos accedan y ejecuten simultáneamente algunas regiones de código.

    Un saludo.

  4. omar dice:

    Hola. Gracias por tu explicación, creo que es muy buena….
    Pero.
    Tengo una duda: Mi clase SercviceImpl tiene más de un método definido para conectarse a la BD, y cuando quiero llemar a uno de ellos me dice que hay un error de null.

  5. omar dice:

    Mira…

    /*
    * To change this license header, choose License Headers in Project Properties.
    * To change this template file, choose Tools | Templates
    * and open the template in the editor.
    */
    package mx.gob.bancomext.garantia.quartz;
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;

    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    import java.util.logging.Level;
    import java.util.logging.Logger;

    import mx.gob.bancomext.garantia.dto.ResultadoCargaDTO;
    import mx.gob.bancomext.garantia.dto.TgGarantiaDTO;
    import mx.gob.bancomext.garantia.dto.TgGarantiaGenericoDTO;
    import mx.gob.bancomext.garantia.dto.TgGarantiaIdDTO;
    import mx.gob.bancomext.garantia.modelo.TgGarantia;
    import mx.gob.bancomext.garantia.reporte.ReporteGarantia;
    import mx.gob.bancomext.garantia.reporte.ReportePropositoDTO;
    import mx.gob.bancomext.garantia.service.TgGarantiaService;
    import mx.gob.bancomext.garantia.service.impl.TgGarantiaServiceImpl;
    import mx.gob.bancomext.garantia.util.exception.GarantiaException;

    import org.springframework.dao.DataAccessException;
    import org.springframework.scheduling.quartz.QuartzJobBean;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.web.context.support.SpringBeanAutowiringSupport;

    /**
    *
    * @author e_ocerva
    */
    public class garantiaVencida extends QuartzJobBean{

    public List lstTgGarantia = new ArrayList();
    public TgGarantiaDTO garantiaDTO = new TgGarantiaDTO();
    private TgGarantiaService tgGarantiaService;

    @Override
    protected void executeInternal(JobExecutionContext jec) throws JobExecutionException {

    try {

    SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

    System.out.println(«Hola! XD»);
    this.tgGarantiaService = (TgGarantiaService) jec.getJobDetail().getJobDataMap().get(«tgGarantiaService»);
    System.out.println(«Hola!»);

    lstTgGarantia = tgGarantiaService.selectByExampleTgGarantia(garantiaDTO);

    int tam = lstTgGarantia.size();

    for(int x=0; x<tam; x++){
    System.out.println("Registro "+x+" : "+lstTgGarantia.get(x));
    }

    } catch (Exception ex) {
    Logger.getLogger(garantiaVencida.class.getName()).log(Level.SEVERE, null, ex);
    }
    }
    /**
    * @return the lstTgGarantia
    */
    public List getLstTgGarantia() {
    return lstTgGarantia;
    }

    /**
    * @param lstTgGarantia the lstTgGarantia to set
    */
    public void setLstTgGarantia(List lstTgGarantia) {
    this.lstTgGarantia = lstTgGarantia;
    }

    }
    ———————————————————————————
    ————————————————————————–

    /*
    * To change this license header, choose License Headers in Project Properties.
    * To change this template file, choose Tools | Templates
    * and open the template in the editor.
    */
    package mx.gob.bancomext.garantia.quartz;

    import org.codehaus.groovy.tools.groovydoc.Main;
    import org.quartz.JobDetail;
    import org.quartz.Scheduler;
    import org.quartz.Trigger;
    import org.quartz.impl.JobDetailImpl;
    import org.quartz.impl.StdSchedulerFactory;

    /**
    *
    * @author e_ocerva
    */
    public class EjecutaQuartz {

    public static void main(String[] args){
    try {
    // Creacion de una instacia de Scheduler
    Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
    System.out.println(«niciando Scheduler…»);
    scheduler.start();
    // Creacion una instacia de JobDetail
    JobDetail jobDetail = new JobDetailImpl(«garantiaVencida», Scheduler.DEFAULT_GROUP, garantiaVencida.class);

    // Creacion de un Trigger donde indicamos
    //que el Job se
    // ejecutara de inmediato y a partir de ahi en lapsos
    // de 5 segundos por 10 veces mas.

    Trigger trigger = new org.quartz.impl.triggers.SimpleTriggerImpl(«garantiaVencida», Scheduler.DEFAULT_GROUP, 0, 1000);

    // Registro dentro del Scheduler
    scheduler.scheduleJob(jobDetail, trigger);

    // Damos tiempo a que el Trigger registrado
    //termine su periodo
    // de vida dentro del scheduler
    Thread.sleep(5000);

    // Detenemos la ejecución de la
    // instancia de Scheduler

    scheduler.shutdown();

    }catch (Exception e) {
    System.out.println(«Ocurrió una excepción»);
    }

    }
    }
    ——————————————————————————
    —————————————————————————–

    Agregue esto al pomp.xml

    org.quartz-scheduler
    quartz
    2.2.1

    —————————————————————————————-
    —————————————————————————————-

    spring-quartz.xml—




    ————————————————————————————–
    ————————————————————————————–

    Y agregue esto al web.xml

    contextConfigLocation



    classpath:spring-quartz.xml

  6. MyrCa dice:

    Buenas tardes

    Estoy trabajando con MVC Struts y requiero utilizar Quartz tendrás un ejemplo?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

« »