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!
« ¿Cómo vender mis productos en Amazon? Oportunidades de financiación I+D empresarial »
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
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.
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.
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.
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.
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
Buenas tardes
Estoy trabajando con MVC Struts y requiero utilizar Quartz tendrás un ejemplo?