Guia Prático de Agendamento com Apache Quartz na Java

Arquitetura e Componentes Nucleares

O Apache Quartz opera mediante a coordenação de objetos acoplados dinamicamente. Para dominar sua implementação, é necessário compreender a função de cada peça no fluxo de execução:

  • Tarefa (Job): Interface padrão que obriga a implementação do método execute(JobExecutionContext contexto). Nesse ponto reside a lógica operacional a ser executada conforme a periodicidade definida.
  • Metadados da Tarefa (JobDetail): Como o motor não reaproveita instâncias de Job entre disparos, utiliza-se esta classe para descrever configurações imutáveis: nome, grupo, classe concreta e um mapa de atributos (JobDataMap) injetáveis.
  • Gatilho (Trigger): Define a regra temporal. As implementações SimpleTrigger cobrem intervalos fixos ou execuções pontuais, enquanto CronTrigger permite sintaxes avançadas baseadas em cron.
  • Calendário de Restrições (Calendar): Agrupa datas específicas para filtragem. Diferente do java.util.Calendar, esta classe do Quartz atua como negacionista de janelas (ex.: excluir feriados corporativos ou finais de semana).
  • Agendador (Scheduler): Motor central responsável por associar gatilhos a tarefas, gerenciar estados e expor operações de ciclo de vida (start(), pause(), shutdown()). Possui contexto global acessível via SchedulerContext.
  • Pool de Threads: Conjunto de linhas assíncronas pré-alocadas. Garante que tarefas concurrentes sejam atendidas sem sobrecarga de criação de processos.

Diferença entre Jobs com Estado: Jobs StatelessJob (padrão) isolam dados por execução. Jobs marcados com StatefulJob compartilham JobDataMap, persistindo alterações entre rodadas e bloqueando concorrência até o término. Priorize sempre o modelo stateless para evitar deadlocks internos.

Estrutura e Sintaxe Cron

As expressões temporiais seguem seis campos obrigatórios, sendo o ano opcional:

Campo Valores Válidos Caracteres Especiais
Segundo 0-59 ,, -, *, /
Minuto 0-59 ,, -, *, /
Hora 0-23 ,, -, *, /
Dia do Mês 1-31 ,, -, *, ?, L, W
Mês 1-12 / JAN-DEZ ,, -, *, /
Dia da Semana 1-7 / DOM-SÁB ,, -, *, ?, L, #

Operadores Principais:

  • ?: Ausência de especificação (usado em dias quando se restringe mês ou semana).
  • /: Incremento. 0/15 significa começar em 0 e somar 15.
  • L: Último dia/semana do período corrente.
  • W: Dia útil (segunda a sexta) mais próximo da data informada.
  • #: Posição ordinal. 2#3 equivale ao terceiro segunda-feira do mês.

Exemplos Práticos:

  • A cada 5 segundos: 0/5 * * * * ?
  • Domingos às 02h00: 0 0 2 ? * SUN
  • Último dia do mês à meia-noite: 0 0 0 L * ?
  • Dias úteis entre 09h e 17h: 0 0 9-17 ? * MON-FRI

Implementação Padrão em Java

Inclua a bbilioteca oficial no manifesto:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

Declare a classe executável:

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Map;

public class ProcessadorBatches implements Job {
    @Override
    public void execute(JobExecutionContext ctx) throws JobExecutionException {
        String identificador = ctx.getJobDetail().getKey().getName();
        Map<String, Object> params = ctx.getJobDetail().getJobDataMap();
        
        System.out.printf("[TASK %s] Executando lógica de processamento...%n", identificador);
        // Operações de negócio aqui
    }
}

Orquestre a primeira rodada em memória:

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class OrchestradorInical {
    public static void main(String[] args) {
        try {
            Scheduler motor = StdSchedulerFactory.getDefaultScheduler();
            
            JobDefinition job = JobBuilder.newJob(ProcessadorBatches.class)
                .withIdentity("batch_principal", "financeiro")
                .usingJobData("modo", "producao")
                .build();
                
            Trigger disparador = TriggerBuilder.newTrigger()
                .withIdentity("disp_horario", "financeiro")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(2)
                    .repeatForever())
                .build();
                    
            motor.scheduleJob(job, disparador);
            motor.start();
            
        } catch (SchedulerException falha) {
            throw new RuntimeException("Erro crítico ao inicializar schedulers", falha);
        }
    }
}

Gerenciamento Dinâmico de Cronogramas

Para cenários administrativos que requerem mutação runtime de schedules, encapsule as APIs nativas:

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.List;

public class GestorDeRotinas {
    private final Scheduler engine;

    public GestorDeRotinas() throws SchedulerException {
        this.engine = StdSchedulerFactory.getDefaultScheduler();
    }

    public void inscrever(ConfigTarefa conf) throws SchedulerException {
        if (!engine.isStarted()) engine.start();

        JobDefinition job = JobBuilder.newJob(conf.tipo())
            .withIdentity(conf.nome(), conf.grupoJob())
            .build();

        Trigger gatilho = TriggerBuilder.newTrigger()
            .withIdentity(conf.trigger(), conf.grupoTrigger())
            .startNow()
            .withSchedule(CronScheduleBuilder.cronSchedule(conf.regra()))
            .build();

        engine.scheduleJob(job, gatilho);
    }

    public void resincronizar(String nomeTask, String grupoTask, String novaRegra) throws SchedulerException {
        TriggerKey referencia = TriggerKey.triggerKey("trigger_" + nomeTask, "gt_padrao");
        CronTrigger atual = (CronTrigger) engine.getTrigger(referencia);
        if (atual == null) return;

        Trigger modificado = TriggerBuilder.newTrigger()
            .withIdentity(referencia.getName(), referencia.getGroup())
            .withSchedule(CronScheduleBuilder.cronSchedule(novaRegra))
            .build();

        engine.rescheduleJob(referencia, modificado);
    }

    public void descartar(String nomeTask, String grupoTask, String triggerNome, String grupoTrig) throws SchedulerException {
        TriggerKey tChave = TriggerKey.triggerKey(triggerNome, grupoTrig);
        engine.pauseTrigger(tChave);
        engine.unscheduleJob(tChave);
        engine.deleteJob(JobKey.jobKey(nomeTask, grupoTask));
    }
    
    public void interromper() throws SchedulerException {
        if (!engine.isShutdown()) engine.shutdown(false);
    }
}

Teste de fluxo alterado:

public class SimulacaoRuntime {
    public static void main(String[] args) throws Exception {
        var gerenciador = new GestorDeRotinas();
        var spec = new RecordConfig("alerta_servidor", "infra", "alerta_t", "infra_gt", 
                                   ProcessadorBatches.class, "0/3 * * * * ?");
        
        System.out.println("Inscrevendo rotina base...");
        gerenciador.inscrever(spec);

        Thread.sleep(9000);
        System.out.println("Alterando cron para 10s...");
        gerenciador.resincronizar("alerta_servidor", "infra", "0/10 * * * * ?");

        Thread.sleep(20000);
        System.out.println("Removendo inscrição...");
        gerenciador.descartar("alerta_servidor", "infra", "alerta_t", "infra_gt");
        gerenciador.interromper();
    }
}

record RecordConfig(String nome, String grupoJob, String trigger, String grupoTrigger, Class<?> tipo, String regra) {}

Adaptação ao Container Spring

O ecossistema Spring delega a lifecycle management via XML/Annotations. Configure manualmente:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.3.30</version>
</dependency>

Arquivo quartz-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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.xsd">

    <!-- Bind da tarefa ao contexto -->
    <bean id="executerInterno" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <property name="name" value="rotina_diaria"/>
        <property name="group" value="jobs_pool"/>
        <property name="jobClass" value="br.com.projeto.jobs.ProcessadorBatches"/>
        <property name="durability" value="true"/>
        <property name="applicationContextJobDataKey" value="ctxApp"/>
    </bean>

    <!-- Trigger Cron vinculado -->
    <bean id="trigger_noturno" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="name" value="trigger_dia"/>
        <property name="group" value="cron_gpus"/>
        <property name="jobDetail" ref="executerInterno"/>
        <property name="cronExpression" value="0 0 02 * * ?"/>
    </bean>

    <!-- Factory mestre -->
    <bean id="schedulerMotor" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list><ref bean="trigger_noturno"/></list>
        </property>
        <property name="autoStartup" value="false"/>
    </bean>
</beans>

Validação pontual:

import org.quartz.Scheduler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class VerificadorSpring {
    public static void main(String[] args) throws Exception {
        ApplicationContext container = new ClassPathXmlApplicationContext("quartz-config.xml");
        Scheduler plataforma = (Scheduler) container.getBean("schedulerMotor");
        plataforma.start();
        System.in.read(); // Bloqueio mantenedor de daemon
    }
}

Tags: ApacheQuartz java SpringFramework CronExpressions AgendamentoConcurrente

Publicado em 10-7 01:42