En este artículo, veremos cómo programar tareas utilizando el marco de cuarzo . Quartz es el estándar de facto de programación de bibliotecas para aplicaciones Java. Quartz admite la ejecución de trabajos en un momento determinado, la repetición de ejecuciones de trabajos, el almacenamiento de trabajos en una base de datos y la integración con Spring.
La forma más fácil de usar las aplicaciones de Quartz en Spring es usar la anotación @Scheduled
. A continuación, consideraremos un ejemplo de una aplicación Spring Boot. Agreguemos la dependencia necesaria en build.gradle
implementation 'org.springframework.boot:spring-boot-starter-quartz'
y consideremos un ejemplo
package quartzdemo.tasks; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Date; @Component public class PeriodicTask { @Scheduled(cron = "0/5 * * * * ?") public void everyFiveSeconds() { System.out.println("Periodic task: " + new Date()); } }
Además, para que la anotación @Scheduled
funcione, debe agregar una configuración con la anotación @EnableScheduling
.
package quartzdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling public class QuartzDemoApplication { public static void main(String[] args) { SpringApplication.run(QuartzDemoApplication.class, args); } }
El resultado será una salida de texto en la consola cada cinco segundos.
Periodic task: Thu Jul 07 18:24:50 EDT 2022 Periodic task: Thu Jul 07 18:24:55 EDT 2022 Periodic task: Thu Jul 07 18:25:00 EDT 2022 ...
La anotación @Scheduled
admite los siguientes parámetros:
fixedRate
: le permite ejecutar una tarea en un intervalo fijo especificado.
fixedDelay
: ejecuta una tarea con un retraso fijo entre la finalización de la última invocación y el inicio de la siguiente.
initialDelay
: el parámetro se usa con fixedRate
y fixedDelay
para esperar antes de la primera ejecución de la tarea con el retraso especificado.
cron
: establezca el cronograma de ejecución de la tarea usando la cadena cron. También admite macros @yearly
(o @annually
), @monthly
, @weekly
, @daily
(o @midnight
) y @hourly
.
De forma predeterminada, fixedRate
, fixedDelay
e initialDelay
se establecen en milisegundos. Esto se puede cambiar usando el parámetro timeUnit
, configurando el valor de NANOSECONDS
a DAYS
.
Además, puede usar propiedades en la anotación @Scheduled:
aplicación.propiedades
cron-string=0/5 * * * * ?
TareaPeriódica.java
@Component public class PeriodicTask { @Scheduled(cron = "${cron-string}") public void everyFiveSeconds() { System.out.println("Periodic task: " + new Date()); } }
Para usar propiedades, puede utilizar fixedRateString
, fixedDelayString
e initialDelayString
en lugar de fixedRate
, fixedDelay
e initialDelay
según corresponda.
En el ejemplo anterior, ejecutamos tareas programadas, pero al mismo tiempo, no pudimos establecer dinámicamente la hora de inicio del trabajo ni pasarle parámetros. Para resolver estos problemas, puedes usar directamente Quartz.
Las principales interfaces de Quartz se enumeran a continuación:
Job
es una interfaz a ser implementada por las clases que contienen la lógica de negocio que deseamos que se ejecute
JobDetails
define instancias de Job
y datos relacionados con él
Trigger
describe la programación de la ejecución del trabajo.
Scheduler
es la interfaz principal de Quartz que proporciona todas las operaciones de manipulación y búsqueda de trabajos y disparadores.
Para trabajar con Quartz directamente, no es necesario definir una configuración con la anotación @EnableScheduling
, y en lugar de la org.springframework.boot:spring-boot-starter-quartz
, puede usar org.quartz-scheduler:quartz
.
Definamos la clase SimpleJob:
package quartzdemo.jobs; import org.quartz.Job; import org.quartz.JobExecutionContext; import java.text.MessageFormat; public class SimpleJob implements Job { @Override public void execute(JobExecutionContext context) { System.out.println(MessageFormat.format("Job: {0}", getClass())); } }
Para implementar la interfaz de Job
, debe implementar solo un método de execute
que acepte un parámetro del tipo JobExecutionContext
. JobExecutionContext
contiene información sobre la instancia del trabajo, el activador, el programador y otra información sobre la ejecución del trabajo.
Ahora definamos una instancia del trabajo:
JobDetail job = JobBuilder.newJob(SimpleJob.class).build();
Y cree un activador que se activará después de cinco segundos:
Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5) .atZone(ZoneId.systemDefault()).toInstant()); Trigger trigger = TriggerBuilder.newTrigger() .startAt(afterFiveSeconds) .build();
Además, cree un programador:
SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler();
El Scheduler
se inicializa en modo "en espera", por lo que debemos invocar el método de start
:
scheduler.start();
Ahora podemos programar la ejecución del trabajo:
scheduler.scheduleJob(job, trigger);
Además, al crear JobDetails
, agreguemos datos adicionales y usémoslos al ejecutar un trabajo:
QuartzDemoApplication.java
@SpringBootApplication public class QuartzDemoApplication { public static void main(String[] args) { SpringApplication.run(QuartzDemoApplication.class, args); onStartup(); } private static void onStartup() throws SchedulerException { JobDetail job = JobBuilder.newJob(SimpleJob.class) .usingJobData("param", "value") // add a parameter .build(); Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5) .atZone(ZoneId.systemDefault()).toInstant()); Trigger trigger = TriggerBuilder.newTrigger() .startAt(afterFiveSeconds) .build(); SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.start(); scheduler.scheduleJob(job, trigger); } }
SimpleJob.java
public class SimpleJob implements Job { @Override public void execute(JobExecutionContext context) { JobDataMap dataMap = context.getJobDetail().getJobDataMap(); String param = dataMap.getString("param"); System.out.println(MessageFormat.format("Job: {0}; Param: {1}", getClass(), param)); } }
Es importante tener en cuenta que todos los valores que se agregan a JobDataMap
deben ser serializables.
Quartz almacena datos sobre JobDetail
, Trigger
y otra información en JobStore
. De forma predeterminada, se utiliza JobStore
en memoria. Esto significa que si tenemos tareas programadas y cerramos la aplicación (por ejemplo, al reiniciar o fallar) antes de que se disparen, entonces nunca más se ejecutarán. Quartz también es compatible con JDBC-JobStore para almacenar información en una base de datos.
Antes de utilizar JDBC-JobStore, es necesario crear tablas en la base de datos que utilizará Quartz. De forma predeterminada, estas tablas tienen el prefijo QRTZ_
.
El código fuente de Quartz contiene secuencias de comandos SQL para crear tablas para varias bases de datos, como Oracle, Postgres, MS SQL Server, MySQL y otras, y también tiene un archivo XML listo para usar para Liquibase.
Además, las tablas QRTZ se pueden crear automáticamente al iniciar la aplicación especificando la spring.quartz.jdbc.initialize-schema=always
.
Para simplificar, usaremos el segundo método y la base de datos H2. Configuremos una fuente de datos, usemos JDBCJobStore y creemos tablas QRTZ en application.properties:
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.quartz.job-store-type=jdbc spring.quartz.jdbc.initialize-schema=always
Para que se tengan en cuenta estas configuraciones, el Programador debe crearse como un bean Spring:
package quartzdemo; import org.quartz.*; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import quartzdemo.jobs.SimpleJob; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; @SpringBootApplication @EnableScheduling public class QuartzDemoApplication { public static void main(String[] args) { SpringApplication.run(QuartzDemoApplication.class, args); } @Bean() public Scheduler scheduler(SchedulerFactoryBean factory) throws SchedulerException { Scheduler scheduler = factory.getScheduler(); scheduler.start(); return scheduler; } @Bean public CommandLineRunner run(Scheduler scheduler) { return (String[] args) -> { JobDetail job = JobBuilder.newJob(SimpleJob.class) .usingJobData("param", "value") // add a parameter .build(); Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5) .atZone(ZoneId.systemDefault()).toInstant()); Trigger trigger = TriggerBuilder.newTrigger() .startAt(afterFiveSeconds) .build(); scheduler.scheduleJob(job, trigger); }; } }
Quartz ejecuta cada tarea en un subproceso separado y puede configurar un grupo de subprocesos para programadores. También se debe tener en cuenta que, de forma predeterminada, las tareas iniciadas a través de la anotación @Scheduled
y directamente a través de Quartz se inician en diferentes grupos de subprocesos. Podemos asegurarnos de esto:
TareaPeriódica.java
@Component public class PeriodicTask { @Scheduled(cron = "${cron-string}") public void everyFiveSeconds() { System.out.println(MessageFormat.format("Periodic task: {0}; Thread: {1}", new Date().toString(), Thread.currentThread().getName())); } }
SimpleJob.java
public class SimpleJob implements Job { @Override public void execute(JobExecutionContext context) { JobDataMap dataMap = context.getJobDetail().getJobDataMap(); String param = dataMap.getString("param"); System.out.println(MessageFormat.format("Job: {0}; Param: {1}; Thread: {2}", getClass(), param, Thread.currentThread().getName())); } }
la salida será:
Periodic task: Thu Jul 07 19:22:45 EDT 2022; Thread: scheduling-1 Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: quartzScheduler_Worker-1 Periodic task: Thu Jul 07 19:22:50 EDT 2022; Thread: scheduling-1 Periodic task: Thu Jul 07 19:22:55 EDT 2022; Thread: scheduling-1 Periodic task: Thu Jul 07 19:23:00 EDT 2022; Thread: scheduling-1
El grupo de subprocesos para las tareas @Scheduled
contiene solo un subproceso.
Cambiemos la configuración del programador para las tareas @Scheduled:
package quartzdemo; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskRegistrar; @Configuration public class SchedulingConfiguration implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(10); threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-"); threadPoolTaskScheduler.initialize(); taskRegistrar.setTaskScheduler(threadPoolTaskScheduler); } }
la salida ahora será así:
Periodic task: Thu Jul 07 19:44:10 EDT 2022; Thread: my-scheduled-task-pool-1 Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: quartzScheduler_Worker-1 Periodic task: Thu Jul 07 19:44:15 EDT 2022; Thread: my-scheduled-task-pool-1 Periodic task: Thu Jul 07 19:44:20 EDT 2022; Thread: my-scheduled-task-pool-2
Como puede ver, esta configuración afectó solo a las tareas establecidas mediante anotaciones.
Ahora cambiemos la configuración del programador con el que trabajamos directamente con Quartz. Esto se puede hacer de dos maneras: a través del archivo de propiedades o creando un bean SchedulerFactoryBeanCustomizer
.
Usemos el primer método. Si no hubiéramos inicializado Quartz a través de Spring, tendríamos que registrar propiedades en el archivo quartz.properties. En nuestro caso, necesitamos registrar propiedades en application.properties, agregando el prefijo spring.quartz.properties.
a ellos
application.properties
spring.quartz.properties.org.quartz.threadPool.threadNamePrefix=my-scheduler_Worker spring.quartz.properties.org.quartz.threadPool.threadCount=25
Iniciemos la aplicación. Ahora la salida será así:
Periodic task: Sat Jul 23 10:45:55 MSK 2022; Thread: my-scheduled-task-pool-1 Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: my-scheduler_Worker-1
Ahora el hilo en el que se inicia la tarea se llama my-scheduler_Worker-1
.
Si necesita crear varios programadores con diferentes parámetros, debe definir varios SchedulerFactoryBeans
. Veamos un ejemplo.
package quartzdemo; import quartzdemo.jobs.SimpleJob; import org.quartz.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import javax.sql.DataSource; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.Properties; @SpringBootApplication @EnableScheduling public class QuartzDemoApplication { public static void main(String[] args) { SpringApplication.run(QuartzDemoApplication.class, args); } @Bean("customSchedulerFactoryBean1") public SchedulerFactoryBean customSchedulerFactoryBean1(DataSource dataSource) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); Properties properties = new Properties(); properties.setProperty("org.quartz.threadPool.threadNamePrefix", "my-custom-scheduler1_Worker"); factory.setQuartzProperties(properties); factory.setDataSource(dataSource); return factory; } @Bean("customSchedulerFactoryBean2") public SchedulerFactoryBean customSchedulerFactoryBean2(DataSource dataSource) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); Properties properties = new Properties(); properties.setProperty("org.quartz.threadPool.threadNamePrefix", "my-custom-scheduler2_Worker"); factory.setQuartzProperties(properties); factory.setDataSource(dataSource); return factory; } @Bean("customScheduler1") public Scheduler customScheduler1(@Qualifier("customSchedulerFactoryBean1") SchedulerFactoryBean factory) throws SchedulerException { Scheduler scheduler = factory.getScheduler(); scheduler.start(); return scheduler; } @Bean("customScheduler2") public Scheduler customScheduler2(@Qualifier("customSchedulerFactoryBean2") SchedulerFactoryBean factory) throws SchedulerException { Scheduler scheduler = factory.getScheduler(); scheduler.start(); return scheduler; } @Bean public CommandLineRunner run(@Qualifier("customScheduler1") Scheduler customScheduler1, @Qualifier("customScheduler2") Scheduler customScheduler2) { return (String[] args) -> { Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5).atZone(ZoneId.systemDefault()).toInstant()); JobDetail jobDetail1 = JobBuilder.newJob(SimpleJob.class).usingJobData("param", "value1").build(); Trigger trigger1 = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).build(); customScheduler1.scheduleJob(jobDetail1, trigger1); JobDetail jobDetail2 = JobBuilder.newJob(SimpleJob.class).usingJobData("param", "value2").build(); Trigger trigger2 = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).build(); customScheduler2.scheduleJob(jobDetail2, trigger2); }; } }
Producción:
Job: class quartzdemo.jobs.SimpleJob; Param: value2; Thread: my-custom-scheduler2_Worker-1 Job: class quartzdemo.jobs.SimpleJob; Param: value1; Thread: my-custom-scheduler1_Worker-1
El código fuente completo está disponible en GitHub .
Quartz es un marco poderoso para automatizar tareas programadas. Se puede usar tanto con la ayuda de anotaciones Spring simples e intuitivas como con una personalización y ajuste finos, lo que brinda una solución a problemas complejos.