1. Información general
Quartz es un marco de programación de trabajos de código abierto escrito completamente en Java y diseñado para su uso en aplicaciones J2SE y J2EE . Ofrece una gran flexibilidad sin sacrificar la simplicidad.
Puede crear programas complejos para ejecutar cualquier trabajo. Algunos ejemplos son, por ejemplo, las tareas que se ejecutan a diario, cada dos viernes a las 7:30 pm o solo el último día de cada mes.
En este artículo, veremos los elementos para crear un trabajo con la API Quartz. Para una introducción en combinación con Spring, recomendamos programar en Spring con Quartz.
2. Dependencias de Maven
Necesitamos agregar la siguiente dependencia al pom.xml:
org.quartz-scheduler quartz 2.3.0
La última versión se puede encontrar en el repositorio de Maven Central.
3. La API Quartz
El corazón del marco es el Programador . Es responsable de administrar el entorno de ejecución de nuestra aplicación.
Para garantizar la escalabilidad, Quartz se basa en una arquitectura de subprocesos múltiples. Cuando se inicia, el marco inicializa un conjunto de subprocesos de trabajo que utiliza el Programador para ejecutar trabajos .
Así es como el marco puede ejecutar muchos trabajos al mismo tiempo. También se basa en un conjunto de componentes de administración de ThreadPool débilmente acoplados para administrar el entorno de subprocesos.
Las interfaces clave de la API son:
- Programador: la API principal para interactuar con el programador del marco
- Trabajo: una interfaz que se implementará con los componentes que deseamos ejecutar.
- JobDetail: se utiliza para definir instancias de Job s
- Desencadenador: un componente que determina el programa en el que se realizará un trabajo determinado
- JobBuilder: se utiliza para crear instancias de JobDetail , que definen instancias de trabajos
- TriggerBuilder: se utiliza para crear instancias de Trigger
Echemos un vistazo a cada uno de esos componentes.
4. Programador
Antes de que podamos usar el Programador , es necesario crear una instancia. Para hacer esto, podemos usar el SchedulerFactory de fábrica :
SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler();
El ciclo de vida de un Scheduler está limitado por su creación, a través de SchedulerFactory y una llamada a su método shutdown () . Una vez creada, la interfaz del Programador se puede usar para agregar, eliminar y listar trabajos y desencadenadores , y realizar otras operaciones relacionadas con la programación (como pausar un desencadenador).
Sin embargo, el Programador no actuará sobre ningún activador hasta que se haya iniciado con el método start () :
scheduler.start();
5. Empleos
Un trabajo es una clase que implementa la interfaz de trabajo . Tiene un solo método simple:
public class SimpleJob implements Job { public void execute(JobExecutionContext arg0) throws JobExecutionException { System.out.println("This is a quartz job!"); } }
Cuando se activa el disparador del trabajo, uno de los subprocesos de trabajo del programador invoca el método execute () .
El objeto JobExecutionContext que se pasa a este método proporciona la instancia de trabajo, con información sobre su entorno de ejecución, un identificador para el Programador que lo ejecutó, un identificador para el Trigger que desencadenó la ejecución, el objeto JobDetail del trabajo y algunos otros elementos .
El cliente Quartz crea el objeto JobDetail en el momento en que se agrega el trabajo al Programador. Es esencialmente la definición de la instancia de trabajo :
JobDetail job = JobBuilder.newJob(SimpleJob.class) .withIdentity("myJob", "group1") .build();
Este objeto también puede contener varias configuraciones de propiedad para el trabajo , así como un JobDataMap , que puede usarse para almacenar información de estado para una instancia determinada de nuestra clase de trabajo.
5.1. JobDataMap
El JobDataMap se utiliza para contener cualquier cantidad de objetos de datos que se desea poner a disposición de la instancia de trabajo cuando se ejecuta. JobDataMap es una implementación de la interfaz de Java Map y tiene algunos métodos de conveniencia adicionales para almacenar y recuperar datos de tipos primitivos.
A continuación, se muestra un ejemplo de cómo poner datos en JobDataMap mientras se crea JobDetail , antes de agregar el trabajo al programador:
JobDetail job = newJob(SimpleJob.class) .withIdentity("myJob", "group1") .usingJobData("jobSays", "Hello World!") .usingJobData("myFloatValue", 3.141f) .build();
Y aquí hay un ejemplo de cómo acceder a estos datos durante la ejecución del trabajo:
public class SimpleJob implements Job { public void execute(JobExecutionContext context) throws JobExecutionException { JobDataMap dataMap = context.getJobDetail().getJobDataMap(); String jobSays = dataMap.getString("jobSays"); float myFloatValue = dataMap.getFloat("myFloatValue"); System.out.println("Job says: " + jobSays + ", and val is: " + myFloatValue); } }
El ejemplo anterior imprimirá "El trabajo dice ¡Hola mundo! Y val es 3.141".
También podemos agregar métodos de establecimiento a nuestra clase de trabajo que correspondan a los nombres de las claves en JobDataMap.
Si hacemos esto, la implementación predeterminada de JobFactory de Quartz llama automáticamente a esos establecedores cuando se crea una instancia del trabajo, evitando así la necesidad de obtener explícitamente los valores del mapa dentro de nuestro método de ejecución.
6. Desencadenantes
Los objetos desencadenantes se utilizan para desencadenar la ejecución de trabajos .
Cuando deseamos programar un trabajo , necesitamos crear una instancia de un disparador y ajustar sus propiedades para configurar nuestros requisitos de programación:
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build();
A Trigger may also have a JobDataMap associated with it. This is useful for passing parameters to a Job that are specific to the executions of the trigger.
There are different types of triggers for different scheduling needs. Each one has different TriggerKey properties for tracking their identities. However, some other properties are common to all trigger types:
- The jobKey property indicates the identity of the job that should be executed when the trigger fires.
- The startTime property indicates when the trigger’s schedule first comes into effect. The value is a java.util.Date object that defines a moment in time for a given calendar date. For some trigger types, the trigger fires at the given start time. For others, it simply marks the time that the schedule should start.
- The endTime property indicates when the trigger’s schedule should be canceled.
Quartz ships with a handful of different trigger types, but the most commonly used ones are SimpleTrigger and CronTrigger.
6.1. Priority
Sometimes, when we have many triggers, Quartz may not have enough resources to immediately fire all of the jobs are scheduled to fire at the same time. In this case, we may want to control which of our triggers gets available first. This is exactly what the priority property on a trigger is used for.
For example, when ten triggers are set to fire at the same time and merely four worker threads are available, the first four triggers with the highest priority will be executed first. When we do not set a priority on a trigger, it uses a default priority of five. Any integer value is allowed as a priority, positive or negative.
In the example below, we have two triggers with a different priority. If there aren't enough resources to fire all the triggers at the same time, triggerA will be the first one to be fired:
Trigger triggerA = TriggerBuilder.newTrigger() .withIdentity("triggerA", "group1") .startNow() .withPriority(15) .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); Trigger triggerB = TriggerBuilder.newTrigger() .withIdentity("triggerB", "group1") .startNow() .withPriority(10) .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(20) .repeatForever()) .build();
6.2. Misfire Instructions
A misfire occurs if a persistent trigger misses its firing time because of the Scheduler being shut down, or in case there are no available threads in Quartz’s thread pool.
The different trigger types have different misfire instructions available. By default, they use a smart policy instruction. When the scheduler starts, it searches for any persistent triggers that have misfired. After that, it updates each of them based on their individually configured misfire instructions.
Let's take a look at the examples below:
Trigger misFiredTriggerA = TriggerBuilder.newTrigger() .startAt(DateUtils.addSeconds(new Date(), -10)) .build(); Trigger misFiredTriggerB = TriggerBuilder.newTrigger() .startAt(DateUtils.addSeconds(new Date(), -10)) .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withMisfireHandlingInstructionFireNow()) .build();
We have scheduled the trigger to run 10 seconds ago (so it is 10 seconds late by the time it is created) to simulate a misfire, e.g. because the scheduler was down or didn't have a sufficient amount of worker threads available. Of course, in a real-world scenario, we would never schedule triggers like this.
In the first trigger (misFiredTriggerA) no misfire handling instructions are set. Hence a called smart policy is used in that case and is called: withMisfireHandlingInstructionFireNow(). This means that the job is executed immediately after the scheduler discovers the misfire.
The second trigger explicitly defines what kind of behavior we expect when misfiring occurs. In this example, it just happens to be the same smart policy.
6.3. SimpleTrigger
SimpleTrigger is used for scenarios in which we need to execute a job at a specific moment in time. This can either be exactly once or repeatedly at specific intervals.
An example could be to fire a job execution at exactly 12:20:00 AM on January 13, 2018. Similarly, we can start at that time, and then five more times, every ten seconds.
In the code below, the date myStartTime has previously been defined and is used to build a trigger for one particular timestamp:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startAt(myStartTime) .forJob("job1", "group1") .build();
Next, let's build a trigger for a specific moment in time, then repeating every ten seconds ten times:
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("trigger2", "group1") .startAt(myStartTime) .withSchedule(simpleSchedule() .withIntervalInSeconds(10) .withRepeatCount(10)) .forJob("job1") .build();
6.4. CronTrigger
The CronTrigger is used when we need schedules based on calendar-like statements. For example, we can specify firing-schedules such as every Friday at noon or every weekday at 9:30 am.
Cron-Expressions are used to configure instances of CronTrigger. These expressions consist of Strings that are made up of seven sub-expressions. We can read more about Cron-Expressions here.
In the example below, we build a trigger that fires every other minute between 8 am and 5 pm, every day:
CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger3", "group1") .withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 8-17 * * ?")) .forJob("myJob", "group1") .build();
7. Conclusion
En este artículo, mostramos cómo crear un programador para activar un trabajo . También vimos algunas de las opciones de activación más comunes utilizadas: SimpleTrigger y CronTrigger .
Quartz se puede utilizar para crear programas simples o complejos para ejecutar docenas, cientos o incluso más trabajos. Puede encontrar más información sobre el marco en el sitio web principal.
El código fuente de los ejemplos se puede encontrar en GitHub.