Tareas programadas de forma periódica con Quartz y Spring en Java

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Java

En las aplicaciones web basadas en el protocolo HTTP la petición al servidor es el desencadenante de la ejecución de la acción que le da respuesta. Algunas acciones no dependen de la solicitud de un usuario o de la recepción de un mensaje sino que se han de ejecutar de forma periódica cada cierto tiempo o de forma planificada en tiempos determinados. Por ejemplo, una tarea que necesite ejecutarse todos los días a las 3 de la mañana o cada 5 minutos.

Quartz es una de las librerías en la plataforma Java que proporciona la funcionalidad de planificador de tareas, permite ejecutar tareas de forma periódica o de forma planificada en determinados tiempos utilizando expresiones cron.

Spring también integra una solución sencilla para ejecutar tareas de forma programada disponible para las aplicaciones que usen Spring Boot sin necesidad de dependencias adicionales.

El propio JDK desde la versión 5 incorpora varias clases para ejecutar tareas programadas sin ninguna dependencia en el proyecto ni siquiera de Spring.

La programación de las tareas también se puede realizar a nivel de sistema operativo. En GNU/Linux con la utilidad cron y con systemd se puede programar tareas. Sin embargo, realizar la programación a nivel de sistema operativo se crean nuevos procesos y la configuración está externalizada de la aplicación. Utilizar Quartz, Spring o las clases del JDK tiene la ventaja de que la configuración de la programación de las tareas está más en el ámbito de la programación que en la de configuración de sistemas, el primer caso los cambios los hace el programador, en el segundo los hace la persona a cargo de los sistemas.

Tareas programadas con Quartz y Spring Boot

Entre las muchas itegraciones que ofrece Spring una de ellas es para Quartz. Las clases importantes que ofrece Quartz son:

  • Job: es la tarea a ejecutar.
  • JobDetail: es una instancia de una tarea.
  • Trigger: es el disparador que determina los momentos de ejecución de los Jobs, cuando una tarea se ha de ejecutar se crea una instancia de JobDetail de la tarea a ejecutar.
  • JobListener: recibe eventos sobre las ejecuciones de las tareas.
  • JobBuilder: una clase que implementa el patrón factoría que facilita la definición de las clases anteriores.
  • JobStore: una clase que permite guardar las definiciones de las tareas en la base de datos.
  • JobDetailFactoryBean, SimpleTriggerFactoryBean: clases alternativas proporcionadas por Spring para la configuración de las tareas, por ejemplo para añadir listeners. Los listeners permiten recibir notificaciones de los eventos de ejecución de las tareas, por ejemplo cuando una tarea se va a ejecutar y cuando se ha ejecutado. En el ejemplo e listeners emite unas trazas en la salida.

Esta es la configuración para definir los jobs con Spring, los triggers que disparan los jobs cada cierto tiempo con una expresión cron y los listeners que reciben los eventos de ejecución de los jobs.

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package io.github.picodotdec.blogbitix.quartzspring;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobDetail;
import org.quartz.JobListener;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.springframework.beans.factory.annotation.Autowired;
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.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;

import org.quartz.JobBuilder;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.scheduling.annotation.EnableScheduling;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@EnableScheduling
public class Main implements ApplicationListener {

    private static final Logger logger = LogManager.getLogger(Main.class);

    ...

    @Bean(name = "QuartzJob")
    public JobDetail quartzJob() {
        return JobBuilder.newJob(QuartzJob.class)
                .withIdentity("QuartzJob", "QuartzJobs")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger quartzTrigger(@Qualifier("QuartzJob") JobDetail job) {
        return TriggerBuilder.newTrigger().forJob(job)
            .withIdentity("QuartzTrigger", "QuartzJobs")
            .withDescription("Quartz trigger")
            .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatForever().withIntervalInSeconds(10))
            .build();
    }

    @Bean
    public Trigger cronQuartzTrigger(@Qualifier("QuartzJob") JobDetail job) {
        return TriggerBuilder.newTrigger().forJob(job)
                .withIdentity("CronQuartzTrigger", "QuartzJobs")
                .withDescription("Cron Quartz trigger")
                .withSchedule(CronScheduleBuilder.cronSchedule("0 * * * * ?"))
                .build();
    }

    @Bean(name = "QuartzJobListener")
    public JobListener quartzListener() {
        return new QuartzJobListener();
    }

    @Bean
    public SchedulerFactoryBeanCustomizer schedulerConfiguration(@Qualifier("QuartzJobListener") JobListener listener) {
        return bean -> {
            bean.setGlobalJobListeners(listener);
        };
    }

    ...

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main-1.java

Para usar Quartz con Spring Boot hay que incluir su dependencia en el archivo de construcción del proyecto.

 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
plugins {
	id 'java'
	id 'application'
}

group = 'io.github.picodotdec.blogbitix.quartzspring'
version = '1.0'

java {
    sourceCompatibility = JavaVersion.VERSION_11
}

application {
    mainClass = 'io.github.picodotdec.blogbitix.quartzspring.Main'
}

repositories {
    mavenCentral()
}

dependencies {
    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }

    implementation platform('org.springframework.boot:spring-boot-dependencies:2.3.1.RELEASE')

    implementation('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    implementation('org.springframework.boot:spring-boot-starter-quartz', excludeSpringBootStarterLogging)
    implementation 'org.springframework.boot:spring-boot-starter-log4j2'

    implementation 'org.apache.logging.log4j:log4j-api:2.13.3'
    implementation 'org.apache.logging.log4j:log4j:2.13.3'
    implementation 'org.apache.logging.log4j:log4j-core:2.13.3'
    runtimeOnly 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
    runtimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.1'
}
build.gradle

Con Quartz no existe la opción de planificar una tarea pasado un tiempo desde la última ejecución. Solo se pueden planificar los Jobs en tiempos regulares, esto provoca que si la tarea tarda en ejecutarse más que el intervalo entre ejecuciones haya dos instancias de la tarea en ejecución de forma paralela. Hay dos opciones para evitar ejecuciones paralelas: una de ellas es utilizar la anotación DisallowConcurrentExecution, la otra forma es planificar otra ejecución de la tarea cuando la anterior ejecución haya terminando proporcionando una implementación de JobListener.

En las clases de las tareas se pueden inyectar beans de Spring con la anotación @Autowired.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdec.blogbitix.quartzspring;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class QuartzJob implements Job {

    private static final Logger logger = LogManager.getLogger(QuartzJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("Job: QuartzJob");
    }
}
QuartzJob.java
 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
package io.github.picodotdec.blogbitix.quartzspring;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;

public class QuartzJobListener implements JobListener {

    private static final Logger logger = LogManager.getLogger(QuartzJobListener.class);

    @Override
    public String getName() {
        return "QuartzJobListener";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        logger.info("QuartzJobListener: jobToBeExecuted. Job: {}, Trigger: {}", context.getJobDetail().getKey().getName(), context.getTrigger().getKey().getName());
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        logger.info("QuartzJobListener: jobExecutionVetoed. Job: {}, Trigger: {}", context.getJobDetail().getKey().getName(), context.getTrigger().getKey().getName());
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        logger.info("QuartzJobListener: jobWasExecuted. Job: {}, Trigger: {}", context.getJobDetail().getKey().getName(), context.getTrigger().getKey().getName());
    }
}
QuartzJobListener.java

Tareas programadas con Spring

Las tareas programadas con Spring son una opción sencilla, basta con anotar un método con la anotación @Scheduled e indicar los parámetros de la anotación el mecanismo que dispara la tarea y los periodos de tiempo. Las planificaciones pueden ser:

  • initialDelay: las tareas se ejecutan con un retraso desde el inicio de la aplicación.
  • fixedRate: ejecución cierto tiempo fijo e independiente de la duración de la tarea.
  • fixedDelay: con una diferencia de tiempo desde la última ejecución.
  • cron: con una expresión cron que permite planificar los periodos de ejecución de la tarea.
 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
package io.github.picodotdec.blogbitix.quartzspring;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SpringJobs {

    private static final Logger logger = LogManager.getLogger(SpringJobs.class);

    @Scheduled(fixedRate = 2000)
    public void scheduleJobWithFixedRate() {
        logger.info("SpringJob: scheduleJobWithFixedRate");
    }


    @Scheduled(fixedDelay = 2000)
    public void scheduleJobWithDelay() {
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        logger.info("SpringJob: scheduleJobWithDelay");
    }

    @Scheduled(cron = "0 * * * * ?")
    public void scheduleJobWithCron() {
        logger.info("SpringJob: scheduleJobWithCron");
    }
}
springJobs.java

Tareas programadas con las clases del JDK

Otra tercera forma de ejecutar tareas periódicas es con las clases Executors y ScheduledExecutorService que están disponibles desde la versión 5 de Java. Proporcionan una funcionalidad similar a las tareas programadas de Spring sin la funcionalidad de expresiones cron.

 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
package io.github.picodotdec.blogbitix.quartzspring;

...

@SpringBootApplication
@EnableScheduling
public class Main implements ApplicationListener {

    private static final Logger logger = LogManager.getLogger(Main.class);

    @Autowired
    private JavaJob javaJob;

    ...

    @Bean
    public JavaJob javaJob() {
        return new JavaJob();
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationStartedEvent) {
            ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
            scheduler.scheduleAtFixedRate(javaJob::jobWithFixedRate, 0, 2, TimeUnit.SECONDS);
            scheduler.scheduleWithFixedDelay(javaJob::jobWithDelay, 0, 2, TimeUnit.SECONDS);
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main-2.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.github.picodotdec.blogbitix.quartzspring;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class JavaJob {

    private static final Logger logger = LogManager.getLogger(JavaJob.class);

    public void jobWithFixedRate() {
        logger.info("JavaJob: jobWithFixedRate");
    }

    public void jobWithDelay() {
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        logger.info("JavaJob: jobWithDelay");
    }
}
JavaJob.java

Elegir entre usar Quartz, usar Spring o usar las clases del JDK

La ventaja de usar las clases del JDK es que ya están incluidas en el JDK y no se necesita incluir ninguna dependencia en el proyecto. Si se usa Spring tampoco se necesitan dependencias adicionales y además proporciona la funcionalidad de expresiones cron que no tienen las clases del JDK. La desventaja de usar el JDK y Spring está en que no tienen todas las opciones de Quartz como la persistencia en la base de datos, la ejecución de tareas que reciban parámetros a modo de contexto o la característica de los listeners que en Spring habría que implementar con alguna otra solución como usar Guava para publicar y suscribirse a eventos.

Dependiendo de las nacesidades de la aplicación será más adecuado usar las clases del JDK, Spring o Quartz.

Ejemplo de tareas programadas con Quartz, Spring y las clases del JDK

El ejemplo incluye varias tareas definidas con Quartz y con Spring. En las trazas se observan los tiempos de ejecución de cada tarea. La tarea de Quartz tiene dos triggers, uno que se ejecuta cada 10 segundos y otro cada minuto. Los jobs de Spring scheduleJobWithFixedRate se ejecuta cada dos segundos, scheduleJobWithDelay se ejecuta cada dos segundos después de haber terminado la anterior ejecución que como tarda dos segundos en ejecutarse se ejecuta en realidad cada cuatro segundos y finalmente scheduleJobWithCron se ejecuta cada minuto. Las tareas planificadas con las clases del JDK se ejecutan igual que las tareas de Spring cada dos y cada cuatro segundos.

 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
...
2020-07-03 14:41:00,004  INFO  ...QuartzJobListener QuartzJobListener: jobToBeExecuted. Job: QuartzJob, Trigger: CronQuartzTrigger
2020-07-03 14:41:00,005  INFO          ...QuartzJob Job: QuartzJob
2020-07-03 14:41:00,005  INFO  ...QuartzJobListener QuartzJobListener: jobWasExecuted. Job: QuartzJob, Trigger: CronQuartzTrigger
2020-07-03 14:41:01,069  INFO         ...SpringJobs SpringJob: scheduleJobWithDelay
2020-07-03 14:41:01,069  INFO         ...SpringJobs SpringJob: scheduleJobWithCron
2020-07-03 14:41:01,070  INFO         ...SpringJobs SpringJob: scheduleJobWithFixedRate
2020-07-03 14:41:01,075  INFO            ...JavaJob JavaJob: jobWithDelay
2020-07-03 14:41:01,077  INFO            ...JavaJob JavaJob: jobWithFixedRate
2020-07-03 14:41:03,068  INFO         ...SpringJobs SpringJob: scheduleJobWithFixedRate
2020-07-03 14:41:03,075  INFO            ...JavaJob JavaJob: jobWithFixedRate
2020-07-03 14:41:05,070  INFO         ...SpringJobs SpringJob: scheduleJobWithDelay
2020-07-03 14:41:05,070  INFO         ...SpringJobs SpringJob: scheduleJobWithFixedRate
2020-07-03 14:41:05,074  INFO            ...JavaJob JavaJob: jobWithFixedRate
2020-07-03 14:41:05,077  INFO            ...JavaJob JavaJob: jobWithDelay
2020-07-03 14:41:07,068  INFO         ...SpringJobs SpringJob: scheduleJobWithFixedRate
2020-07-03 14:41:07,074  INFO            ...JavaJob JavaJob: jobWithFixedRate
...
2020-07-03 14:42:00,003  INFO  ...QuartzJobListener QuartzJobListener: jobToBeExecuted. Job: QuartzJob, Trigger: CronQuartzTrigger
2020-07-03 14:42:00,004  INFO          ...QuartzJob Job: QuartzJob
2020-07-03 14:42:00,005  INFO  ...QuartzJobListener QuartzJobListener: jobWasExecuted. Job: QuartzJob, Trigger: CronQuartzTrigger
2020-07-03 14:42:01,074  INFO            ...JavaJob JavaJob: jobWithFixedRate
2020-07-03 14:42:01,085  INFO         ...SpringJobs SpringJob: scheduleJobWithDelay
2020-07-03 14:42:01,086  INFO         ...SpringJobs SpringJob: scheduleJobWithCron
2020-07-03 14:42:01,087  INFO         ...SpringJobs SpringJob: scheduleJobWithFixedRate
...
System.out
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: