Guía de Quartz con Spring Boot - Programación y automatización de trabajos

Aprenda a crear su propia Consola de administración de Quartz con Java y Spring Boot: programe y automatice trabajos con uno de los potentes programadores.

Introducción

El tiempo es precioso y delegar tiempo y recursos para realizar tareas menores tiende a desperdiciar recursos y dinero. Por lo tanto, las organizaciones se esfuerzan por lograr la automatización a gran escala en sus sistemas, ya que es mucho más escalable y significativamente más económico.

A medida que aumenta la complejidad de los procesos comerciales, también aumenta el beneficio de la automatización en el sistema.

Automatización es donde prevalecen los conceptos de Trabajos o Programadores. La programación de trabajos suele denominarse cualquier tipo de lote de procesos (trabajos), ejecutados en un momento determinado. Dado que la mayoría de estos trabajos no exigen una ejecución inmediata, se pueden programar para que se procesen en un futuro cercano o en intervalos repetitivos.

En general, la automatización de cualquier tipo de proceso en lugar de un enfoque manual conduce a:

  • Eficiencia de recursos
  • Menos errores
  • Más escalabilidad

Uno de los marcos de trabajo de programación más potentes y elásticos utilizados para aplicaciones Java a gran escala se conoce como Quartz.

En esta guía, implementaremos Quartz y sus componentes en una aplicación Spring Boot, creando nuestra propia Consola de administración de Quartz para Trabajos y Activadores personalizados.

Nota: Una copia completa y ejecutable de Quartz Management Console que construiremos está disponible en [nuestro GitHub](https://github.com/wikihtp/spring-boot-quartz/ blob/master/src/main/resources/db/quartz_tables_h2.sql).

El programador de trabajos de cuarzo

Quartz es un marco de programación de trabajos de código abierto y con muchas funciones escrito en Java y ha sido diseñado para integrarse con cualquier tipo de marco J2EE o J2SE. Ofrece una gran flexibilidad sin sacrificar la complejidad o la escalabilidad.

El nombre, presumiblemente, proviene del Cristal de cuarzo utilizado en relojes y relojes extremadamente precisos, que bajo oscilación eléctrica, mueve las manecillas del reloj en un marco de tiempo regular.

Si se espera que una aplicación realice tareas a intervalos programados o debido a algunos eventos, Quartz es ideal:

  • ** Activar recordatorios o alertas por correo electrónico **: puede activar fácilmente correos electrónicos de caducidad de contraseña u otros tipos de alertas de recordatorio para diferentes usuarios según la actividad de la cuenta.
  • Realizar operaciones de transferencia de archivos o mensajería: los trabajos se pueden programar fácilmente a ciertos intervalos para publicar/consumir mensajes/datos/archivos de varios intermediarios o sitios FTP.
  • Generar informes automáticamente: las empresas a menudo prefieren generar informes nocturnos/semanales para mostrar el rendimiento empresarial. Estos trabajos pueden generar fácilmente informes y enviar correos electrónicos a los empleados en un momento programado.
  • Impulsar el flujo de trabajo de tareas: las grandes organizaciones de comercio electrónico pueden programar un trabajo para que se active exactamente a ciertos intervalos para seleccionar un pedido de un canal y procesarlo para su cumplimiento o manifestación.

Algunas de las características más destacadas de Quartz son:

  • Se puede instanciar dentro de un servidor de aplicaciones o contenedor de servlet y puede participar en transacciones XA.
  • Se puede alojar como un grupo de programas independientes (con capacidades de equilibrio de carga y conmutación por error) para la ejecución de trabajos.
  • Los trabajos están programados para ejecutarse cuando ocurre un desencadenante, como una determinada hora del día, ciertos días de semanas, meses o años, omitir la ejecución en días festivos, repetir hasta una fecha o indefinidamente, etc.
  • Los trabajos se pueden conservar en la memoria o en cualquier almacén de datos JDBC.
  • Puede participar en transacciones JTA.

Componentes clave del modelo de planificador de cuarzo

Para mejorar la escalabilidad, Quartz se ejecuta en un entorno multiproceso. Esto ayuda al marco a ejecutar trabajos simultáneamente.

El corazón del marco general es la interfaz Scheduler. El Scheduler realiza un seguimiento de todos los JobDetails y Triggers para ellos. Representan qué debe ejecutarse (qué ‘Trabajo’) y cuándo (qué ‘Desencadenador’ es ese trabajo).

Por lo tanto, forman los componentes principales del marco. El resto de todos los demás componentes se aseguran de que suceda con la debida diligencia y eficacia.

Echemos un vistazo a vista de águila a los componentes clave que usaremos:

  • Scheduler Factory – El bean de fábrica que es responsable de construir el modelo Scheduler y conectar todos los componentes dependientes, según el contenido del archivo de propiedades de cuarzo.
  • Programador: mantiene el registro JobDetail/Trigger. También es responsable de ejecutar los trabajos asociados cuando se dispara un activador.
  • Subproceso del programador – El subproceso responsable de realizar el trabajo de activar los disparadores. Se pone en contacto con JobStore para obtener el siguiente conjunto de disparadores que se dispararán.
  • Trabajo: una interfaz que debe implementar la tarea que se va a ejecutar.
  • Disparador: indica al programador la hora a la que debe activarse el trabajo asociado.
  • Almacén de trabajos: una interfaz que implementarán las clases que proporciona un mecanismo de almacenamiento para trabajos y disparadores.
  • ThreadPool – Un trabajo que se va a ejecutar se transfiere al grupo de subprocesos, representado por ThreadPool.
  • Subprocesos de trabajo: subprocesos individuales que crean el ThreadPool y ejecutan trabajos.

Quartz Components

Creación de una consola de gestión de cuarzo

Construiremos nuestra propia Consola de administración de cuarzo, para comprender y apreciar los ciclos de vida dentro de un programador de cuarzo.

Con ese fin, crearemos una consola de administración basada en la interfaz de usuario simple que puede realizar dos tipos de tareas:

  • Programar y administrar trabajos simples
  • Programar y administrar un trabajo cron

Se vería algo así después de la implementación:

Job Management Console

Configuración del proyecto

Vamos a crear un proyecto Spring Boot e implementar cada componente de Quartz uno por uno. La forma más fácil de comenzar con un proyecto básico es a través de Spring Initializr:

Spring Initializr

Hemos agregado Spring Web para funcionalidades MVC, Spring Data JPA para almacenar datos en un datastore, H2 como base de datos en memoria, [Lombok](/proyecto-lombok-reduciendo-el-codigo-repetitivo- de-java/) (biblioteca reductora de repeticiones opcional) y hoja de tomillo (Motor de plantillas para aplicaciones Spring/MVC). También hemos incluido el paquete spring-boot-starter-quartz para incluir a Quartz en nuestro proyecto.

Conexión e inicialización de la base de datos {#conexión e inicialización de la base de datos}

Quartz trae sus propios JobStores integrados. En Spring Boot podemos elegir entre:

  • Tiendas de trabajos en memoria: mantenga todos los datos en la RAM, de modo que cuando la aplicación se detenga o falle, todos los datos se descarguen y toda la información de programación se pierda. Para esto, usamos un RAMJobStore.
  • JDBC JobStores: Conserva todos los datos en el almacén de datos para que no se pierdan. La configuración es un poco más complicada que las tiendas de trabajos en memoria (RAM).

Nota: Puede elegir estos tipos de JobStore independientemente de su base de datos.

Usaremos H2 como nuestro almacén de datos y configuraremos Quartz para conservar los datos.

Quartz requiere que inicialice las tablas de la base de datos para JDBC JobStores, ya que no se crean automáticamente. Con ese fin, usaremos un script SQL para ejecutar al inicializar la base de datos. Puede encontrar el script de inicialización en nuestro GitHub.

Comencemos el desarrollo de nuestra aplicación de gestión definiendo los parámetros de conexión a la base de datos para H2. En su archivo application.properties, definamos init.schema (script de configuración inicial), así como los parámetros datasource:

1
2
3
4
5
6
7
8
9
server.port=8080

spring.sql.init.schema-locations=classpath:db/quartz_tables_h2.sql
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

logging.level.org.hibernate.SQL=debug

El script quartz_tables_h2.sql consta de un extenso conjunto de comandos SQL que se utilizan para configurarlo inicialmente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- Note, Quartz depends on row-level locking which means you must use the MVC=TRUE
-- setting on your H2 database, or you will experience dead-locks
--
-- In your Quartz properties file, you'll need to set
-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

CREATE TABLE QRTZ_CALENDARS (
  SCHED_NAME VARCHAR(120) NOT NULL,
  CALENDAR_NAME VARCHAR (200)  NOT NULL ,
  CALENDAR IMAGE NOT NULL
);

...
-- Download the entire script from our GitHub repository
...

COMMIT;

Propiedades del cuarzo

Una vez que se pueda establecer la conexión con la base de datos y tengamos nuestro script de inicialización listo, querremos configurar el Quartz Scheduler y sus componentes.

La mayoría de los aspectos y componentes son personalizables, hasta cierto punto, como qué controladores deben usar los JobStores, cuántos subprocesos hay en el ThreadPool y qué prioridad tienen, etc.

Todos estos están definidos en un archivo quartz.properties, que debe estar ubicado en /src/resources/. Este es el directorio en el que la clase QuartzProperties busca las propiedades requeridas por defecto.

Nota: Si desea definirlo en otro archivo de propiedades, deberá señalar la propiedad del sistema org.quartz.properties para señalar ese archivo.

Definamos algunas de las propiedades ahora:

 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
#============================================================================
# Configure Main Scheduler Properties
#============================================================================
org.quartz.scheduler.instanceName=spring-boot-quartz
org.quartz.scheduler.instanceId=AUTO 

#============================================================================
# Configure ThreadPool
#============================================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 25
org.quartz.threadPool.threadPriority = 5

#============================================================================
# Configure JobStore
#============================================================================
org.quartz.jobStore.misfireThreshold=1000
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.tablePrefix=QRTZ_


#============================================================================
# Configure Cluster properties
#============================================================================
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.clusterCheckinInterval=1000

Definición de un bean de fábrica de trabajos del programador

Todas estas propiedades no significan mucho si no las usamos en una clase @Configuration para personalizar cómo funciona Quartz. Inyectemos las propiedades de quartz.properties en una clase SchedulerConfig, donde inicializaremos la clase SchedulerJobFactoryBean, pasando las propiedades.

Implementaremos nuestro propio SchedulerJobFactoryBean como SpringBeanJobFactory del proyecto Quartz:

 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
@Configuration
public class SchedulerConfig {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private QuartzProperties quartzProperties;

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {

        SchedulerJobFactory jobFactory = new SchedulerJobFactory();
        jobFactory.setApplicationContext(applicationContext);

        Properties properties = new Properties();
        properties.putAll(quartzProperties.getProperties());

        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setOverwriteExistingJobs(true);
        factory.setDataSource(dataSource);
        factory.setQuartzProperties(properties);
        factory.setJobFactory(jobFactory);
        return factory;
    }
}

La clase QuartzProperties contiene las propiedades definidas en el archivo quartz.properties. Podemos recuperarlos a través de getProperties() y agregarlos a SchedulerFactoryBean, junto con DataSource y SchedulerJobFactory.

SchedulerJobFactory es nuestra implementación personalizada de SpringBeanJobFactory que nos proporciona Quartz. Vamos a extenderlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class SchedulerJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {

    private AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

Ahora, podemos crear trabajos a través de nuestra fábrica y autoconectarlos cuando sea necesario. En esta etapa, podemos abrir una instancia del programador de cuarzo en ejecución. Si ejecutamos nuestra aplicación, seremos recibidos con algo como:

Spring Quartz Log

Definición de un creador de planificador de tareas genérico

Hay dos tipos de activadores en Quartz: CronTrigger y SimpleTrigger. Estos corresponden a CronScheduler y SimpleScheduler respectivamente, y podemos crear los disparadores usando sus respectivas fábricas.

A CronTrigger triggers based on a expresión cron while a SimpleTrigger trigers on an interval.

Para crear disparadores de trabajo, definamos un par de métodos de conveniencia que los instancian y los devuelven a través de sus respectivas fábricas. Estos métodos estarán situados en un JobSchedulerCreator - un @Component que usaremos para crear trabajos y disparadores:

1
2
3
4
@Component
public class JobScheduleCreator {
    // Creation methods
}

Comencemos con un método creador CronTrigger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public CronTrigger createCronTrigger(String triggerName, Date startTime, String cronExpression, int misFireInstruction) {
    CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
    factoryBean.setName(triggerName);
    factoryBean.setStartTime(startTime);
    factoryBean.setCronExpression(cronExpression);
    factoryBean.setMisfireInstruction(misFireInstruction);
    try {
      factoryBean.afterPropertiesSet();
    } catch (ParseException e) {
      log.error(e.getMessage(), e);
    }
    return factoryBean.getObject();
}

Usando el CronTriggerFactoryBean, pasamos la información requerida sobre un Trigger, como su nombre, la hora de inicio, así como la expresión cron y la instrucción de fallo de encendido. Una vez generado, se devuelve el objeto.

Se aplica mucho el mismo proceso para crear objetos SimpleTrigger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public SimpleTrigger createSimpleTrigger(String triggerName, Date startTime, Long repeatTime, int misFireInstruction) {
    SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
    factoryBean.setName(triggerName);
    factoryBean.setStartTime(startTime);
    factoryBean.setRepeatInterval(repeatTime);
    factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
    factoryBean.setMisfireInstruction(misFireInstruction);
    factoryBean.afterPropertiesSet();
    return factoryBean.getObject();
}

Con una forma sencilla de crear disparadores, también podemos crear un método de conveniencia para crear trabajos, confiando en JobDetailFactoryBean:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public JobDetail createJob(Class<? extends QuartzJobBean> jobClass, boolean isDurable,
                           ApplicationContext context, String jobName, String jobGroup) {
    JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
    factoryBean.setJobClass(jobClass);
    factoryBean.setDurability(isDurable);
    factoryBean.setApplicationContext(context);
    factoryBean.setName(jobName);
    factoryBean.setGroup(jobGroup);

    // Set job data map
    JobDataMap jobDataMap = new JobDataMap();
    jobDataMap.put(jobName + jobGroup, jobClass.getName());
    factoryBean.setJobDataMap(jobDataMap);
    factoryBean.afterPropertiesSet();
    return factoryBean.getObject();
}

Definición de una entidad de información de trabajo del programador

Para realizar un seguimiento de los detalles y la información del trabajo, podemos usar la clase JobDetails. Para eso está destinado. Sin embargo, podemos beneficiarnos al definir un proxy para esa clase propia.

Es [no recomendado escribir en las tablas de cuarzo directamente nosotros] (http://www.quartz-scheduler.org/documentation/2.3.1-SNAPSHOT/best-practices.html), por lo que los trabajos y sus los detalles, aunque persistentes, son fijos. Podemos definir una nueva entidad para realizar un seguimiento de estos trabajos en una tabla separada y hacer con ellos lo que queramos, y también usar estos objetos como [Objetos de transferencia de datos (DTO)](/patrón-de-objetos -transferencia de datos -in-the-java-implementation-and-mapping/) al mismo tiempo.

Esto nos permite realizar la validación de los datos entrantes y nos permite tener un control más granular sobre cómo se conservan los trabajos en la base de datos.

Aunque es opcional, se recomienda usar un proxy como este: lo llamaremos SchedulerJobInfo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Lombok annotations for getters, setters and constructor
public class SchedulerJobInfo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private String jobId;
    private String jobName;
    private String jobGroup;
    private String jobStatus;
    private String jobClass;
    private String cronExpression;
    private String desc;    
    private String interfaceName;
    private Long repeatTime;
    private Boolean cronJob;
}

Para una funcionalidad CRUD simple, crearemos un JpaRepository simple para esta entidad:

1
2
3
4
@Repository
public interface SchedulerRepository extends JpaRepository<SchedulerJobInfo, Long> {
    SchedulerJobInfo findByJobName(String jobName);
}

Implementación de trabajos en Quartz - Job y QuartzJobBean

Cada trabajo tiene que extender la clase QuartzJobBean o implementar la interfaz Job.

QuartzJobBean implementa Job y la única diferencia es que QuartzJobBean aplica el JobDataMap y el SchedulerContext pasados ​​como valores de propiedad del bean, mientras que Job no lo hace.

Además, Job requiere que implementes el método execute() mientras que QuartzJobBean requiere que implementes el método executeInternal().

Vamos a crear un SimpleJob, que extienda QuartzJobBean e imprima números enteros de 0 a 5:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class SimpleJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        log.info("SimpleJob Start................");
        IntStream.range(0, 5).forEach(i -> {
            log.info("Counting - {}", i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error(e.getMessage(), e);
            }
        });
        log.info("SimpleJob End................");
    }
}

De manera similar, podemos crear un SimpleCronJob que se activa en una expresión cron específica:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@DisallowConcurrentExecution
public class SimpleCronJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        log.info("SimpleCronJob Start................");
        IntStream.range(0, 10).forEach(i -> {
            log.info("Counting - {}", i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error(e.getMessage(), e);
            }
        });
        log.info("SimpleCronJob End................");
    }
}

Nota: Estamos aplicando @DisallowConcurrentExecution para que este trabajo no sea ejecutado por varios programadores al mismo tiempo en una configuración en clúster.

Definición de utilidades personalizadas para acciones

En Quartz Management Console, tendremos algunas opciones para trabajos:

  • Crear
  • Editar
  • Corre una vez
  • Pausa
  • Reanudar
  • Borrar

Estos nos permiten crear, editar, pausar, reanudar, eliminar y ejecutar un trabajo una vez. Para facilitar esto, escribiremos métodos para cada tarea, lo que nos permitirá controlar los trabajos desde una interfaz de usuario realmente simple e intuitiva.

Para unir todo esto, crearemos una nueva clase: [SchedulerJobService](https://github.com/wikihtp/spring-boot-quartz/blob/master/src/main/java/com/wikihtp/ service/SchedulerJobService.java) para realizar estas acciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Transactional
@Service
public class SchedulerJobService {

    @Autowired
    private Scheduler scheduler;

    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;

    @Autowired
    private SchedulerRepository schedulerRepository;

    @Autowired
    private ApplicationContext context;

    @Autowired
    private JobScheduleCreator scheduleCreator;
    
    // Create, edit, pause jobs, etc...
Crear un trabajo de cuarzo

Para crear trabajos, un método patentado saveOrUpdate() determina si la instancia creada a través de un DTO SchedulerJobInfo específico debe guardarse en una entidad existente o si se debe crear un nuevo trabajo. Basándonos en los parámetros de la carga útil, crearemos un SimpleCronJob o SimpleJob:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void saveOrUpdate(SchedulerJobInfo scheduleJob) throws Exception {
    if (scheduleJob.getCronExpression().length() > 0) {
        scheduleJob.setJobClass(SimpleCronJob.class.getName());
        scheduleJob.setCronJob(true);
    } else {
        scheduleJob.setJobClass(SimpleJob.class.getName());
        scheduleJob.setCronJob(false);
        scheduleJob.setRepeatTime((long) 1);
    }
    if (StringUtils.isEmpty(scheduleJob.getJobId())) {
        log.info("Job Info: {}", scheduleJob);
        scheduleNewJob(scheduleJob);
    } else {
        updateScheduleJob(scheduleJob);
    }
    scheduleJob.setDesc("i am job number " + scheduleJob.getJobId());
    scheduleJob.setInterfaceName("interface_" + scheduleJob.getJobId());
    log.info(">>>>> jobName = [" + scheduleJob.getJobName() + "]" + " created.");
}

Si el trabajo no existe, llamamos a scheduleNewJob(), que programa uno nuevo, utilizando nuestro componente JobScheduleCreator autocableado de antes:

 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
private void scheduleNewJob(SchedulerJobInfo jobInfo) {
    try {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();

        JobDetail jobDetail = JobBuilder
                .newJob((Class<? extends QuartzJobBean>) Class.forName(jobInfo.getJobClass()))
                .withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup()).build();
        if (!scheduler.checkExists(jobDetail.getKey())) {

            jobDetail = scheduleCreator.createJob(
                    (Class<? extends QuartzJobBean>) Class.forName(jobInfo.getJobClass()), false, context,
                    jobInfo.getJobName(), jobInfo.getJobGroup());

            Trigger trigger;
            if (jobInfo.getCronJob()) {
                trigger = scheduleCreator.createCronTrigger(
                        jobInfo.getJobName(), 
                        new Date(),
                        jobInfo.getCronExpression(),
                        SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
            } else {
                trigger = scheduleCreator.createSimpleTrigger(
                        jobInfo.getJobName(), 
                        new Date(),
                        jobInfo.getRepeatTime(), 
                    
    SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
            }
            scheduler.scheduleJob(jobDetail, trigger);
            jobInfo.setJobStatus("SCHEDULED");
            schedulerRepository.save(jobInfo);
            log.info(">>>>> jobName = [" + jobInfo.getJobName() + "]" + " scheduled.");
        } else {
            log.error("scheduleNewJobRequest.jobAlreadyExist");
        }
    } catch (ClassNotFoundException e) {
        log.error("Class Not Found - {}", jobInfo.getJobClass(), e);
    } catch (SchedulerException e) {
        log.error(e.getMessage(), e);
    }
}

Al crear un disparador, pasamos una MISFIRE_INSTRUCTION. A veces, a Quartz se le puede pasar por alto despedir un determinado trabajo. Esto puede suceder si los subprocesos de trabajo están ocupados, si el programador está inactivo o si un trabajo estaba programado para ejecutarse en el pasado, entre problemas similares.

Hemos configurado nuestros activadores en MISFIRE_INSTRUCTION_FIRE_NOW, que se dispara de nuevo si se produce un fallo de encendido. Si no se define MISFIRE_INSTRUCTION, Quartz adopta una Política inteligente - MISFIRE_INSTRUCTION__SMART_POLICY.

Editar un trabajo de cuarzo

Para editar trabajos, podemos usar el mismo método que para crearlos; sin embargo, debemos informar al Programador para reprogramar el trabajo una vez que se actualicen sus instrucciones:

 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
private void updateScheduleJob(SchedulerJobInfo jobInfo) {
    Trigger newTrigger;
    if (jobInfo.getCronJob()) {
    
        newTrigger = scheduleCreator.createCronTrigger(
                jobInfo.getJobName(), 
                new Date(), 
                jobInfo.getCronExpression(), 
                simpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
    } else {
    
        newTrigger = scheduleCreator.createSimpleTrigger(
                jobInfo.getJobName(), 
                new Date(), 
                jobInfo.getRepeatTime(),
                SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
    }
    try {
        schedulerFactoryBean.getScheduler().rescheduleJob(TriggerKey.triggerKey(jobInfo.getJobName()), newTrigger);
        jobInfo.setJobStatus("EDITED & SCHEDULED");
        schedulerRepository.save(jobInfo);
        log.info(">>>>> jobName = [" + jobInfo.getJobName() + "]" + " updated and scheduled.");
    } catch (SchedulerException e) {
        log.error(e.getMessage(), e);
    }
}
Ejecutar un trabajo de cuarzo una vez

A veces le gustaría disparar un gatillo en una situación ad-hoc. También puede despedir un trabajo para probar si funciona bien o no antes de comprometerse con un cronograma.

Podemos usar el método triggerJob() para activarlo inmediatamente sin esperar un cron o tiempo programado. Esto nos permite crear una tecla de acceso rápido de prueba:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public boolean startJobNow(SchedulerJobInfo jobInfo) {
    try {
        SchedulerJobInfo getJobInfo = schedulerRepository.findByJobName(jobInfo.getJobName());
        getJobInfo.setJobStatus("SCHEDULED & STARTED");
        schedulerRepository.save(getJobInfo);
        schedulerFactoryBean.getScheduler().triggerJob(new JobKey(jobInfo.getJobName(), jobInfo.getJobGroup()));
        log.info(">>>>> jobName = [" + jobInfo.getJobName() + "]" + " scheduled and started now.");
        return true;
    } catch (SchedulerException e) {
        log.error("Failed to start new job - {}", jobInfo.getJobName(), e);
        return false;
    }
}
Pausar un trabajo de cuarzo

Si desea pausar un Cron Job o Simple Job en ejecución, podemos usar el método pauseJob(), que pausará un trabajo hasta que lo reanude:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public boolean pauseJob(SchedulerJobInfo jobInfo) {
    try {
        SchedulerJobInfo getJobInfo = schedulerRepository.findByJobName(jobInfo.getJobName());
        getJobInfo.setJobStatus("PAUSED");
        schedulerRepository.save(getJobInfo);
        schedulerFactoryBean.getScheduler().pauseJob(new JobKey(jobInfo.getJobName(), jobInfo.getJobGroup()));
      log.info(">>>>> jobName = [" + jobInfo.getJobName() + "]" + " paused.");
        return true;
    } catch (SchedulerException e) {
        log.error("Failed to pause job - {}", jobInfo.getJobName(), e);
        return false;
    }
}
Reanudar un trabajo de cuarzo

Naturalmente, podemos reanudar un trabajo en pausa simplemente usando el método resumeJob():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public boolean resumeJob(SchedulerJobInfo jobInfo) {
    try {
      SchedulerJobInfo getJobInfo = schedulerRepository.findByJobName(jobInfo.getJobName());
      getJobInfo.setJobStatus("RESUMED");
      schedulerRepository.save(getJobInfo);
      schedulerFactoryBean.getScheduler().resumeJob(new JobKey(jobInfo.getJobName(), jobInfo.getJobGroup()));
      log.info(">>>>> jobName = [" + jobInfo.getJobName() + "]" + " resumed.");
      return true;
    } catch (SchedulerException e) {
      log.error("Failed to resume job - {}", jobInfo.getJobName(), e);
      return false;
    }
}
Eliminar un trabajo de cuarzo

Finalmente, podemos eliminar un trabajo llamando al método deleteJob():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public boolean deleteJob(SchedulerJobInfo jobInfo) {
    try {
        SchedulerJobInfo getJobInfo = schedulerRepository.findByJobName(jobInfo.getJobName());
        schedulerRepository.delete(getJobInfo);
        log.info(">>>>> jobName = [" + jobInfo.getJobName() + "]" + " deleted.");
        return schedulerFactoryBean.getScheduler().deleteJob(new JobKey(jobInfo.getJobName(), jobInfo.getJobGroup()));
    } catch (SchedulerException e) {
      log.error("Failed to delete job - {}", jobInfo.getJobName(), e);
      return false;
    }
}

Interfaz de usuario de la consola de gestión de cuarzo

Ahora, tenemos toda la funcionalidad requerida para traer nuestra Consola de administración de cuarzo junto con una interfaz de usuario de aplicación web donde podemos probar las funciones.

Nota: La interfaz de usuario de esta aplicación es para demostrar la gestión del ciclo de vida de un programador y las interfaces de usuario son mucho más variables que el back-end. No nos centraremos mucho en su implementación debido a esto. Sin embargo, puede acceder al código completo del front-end en nuestro repositorio GitHub.

Cualquier REST API que active los métodos que hemos definido antes funcionará bien. Nuestra implementación utiliza Thymeleaf como motor de renderizado.

If you'd like to learn more about Thymeleaf, read Primeros pasos con Thymeleaf en Java y Spring.

Si desea obtener más información sobre cómo construir una API REST, lea nuestra [Guía para construir una API REST con Spring Boot](/construir-una-api-spring-boot-rest-con-la-guia-completa-de -Java/).

Una vez que ejecute su aplicación, naveguemos a http://localhost:8080/índice, también vea el tablero.

Primero, seleccionemos el botón Crear para crear un nuevo trabajo. Se abrirá una ventana emergente y nos pedirá que completemos los detalles del trabajo.

Vamos a crear un trabajo cron y un trabajo simple:

Simple Job

Cron Job

Podemos ver todos los trabajos agregados en JobStore:

Job Management Console

También puede ver los registros de trabajo: cuándo se activan los trabajos, uno por uno, en función de sus criterios de activación.

Conclusión

En esta guía, nos presentaron a Quartz, un potente programador, y lo implementamos en una aplicación Spring Boot.

Analizamos la gestión general del ciclo de vida de un planificador de cuarzo con una demostración en una interfaz de usuario simple. Hemos utilizado algunos trabajos minimalistas, pero puede intentar definir trabajos complejos como alertas de correo electrónico de activación o procesamiento de mensajes asincrónicos, etc., y programarlos con Quartz.

Como siempre, puede encontrar el código fuente completo en GitHub.