Lo Spring Batch Scheduler è il meccanismo con cui possiamo eseguire job batch in modo automatico, periodico o programmato. Un job Spring Batch, infatti, non parte da solo: deve essere lanciato da qualcosa. Quel “qualcosa” può essere un metodo main, un endpoint REST, un comando da terminale, un orchestratore esterno, un cron di sistema, Kubernetes, oppure uno scheduler interno all’applicazione.

In questo articolo vedremo come schedulare un job Spring Batch usando gli strumenti più comuni dell’ecosistema Spring: @Scheduled, TaskScheduler, JobLauncher, JobParameters e Quartz Scheduler.

L’obiettivo è capire non solo come avviare un job a intervalli regolari, ma anche come evitare errori frequenti come:

  • A job instance already exists and is complete;
  • job lanciati due volte contemporaneamente;
  • parametri batch non univoci;
  • scheduler non configurati correttamente;
  • uso improprio di Quartz quando basta @Scheduled;
  • mancata gestione di retry, restart e concorrenza.

In breve: Spring Batch esegue job batch, ma non decide automaticamente quando eseguirli. Per schedulare un job serve integrare un meccanismo di scheduling come @Scheduled, TaskScheduler, Quartz o uno scheduler esterno.

Indice dei contenuti

Cosa significa Spring Batch Scheduler

Con l’espressione Spring Batch Scheduler si indica il modo in cui un’applicazione Spring avvia automaticamente uno o più job Spring Batch in base a una pianificazione.

Per esempio, potremmo voler eseguire un job:

  • ogni 5 minuti;
  • ogni notte alle 02:00;
  • ogni lunedì mattina;
  • una volta al giorno dopo la chiusura contabile;
  • ogni ora per importare file;
  • solo in determinati ambienti;
  • con parametri diversi a ogni esecuzione.

Spring Batch si occupa di definire e gestire il job: step, reader, processor, writer, repository, stato di esecuzione, restart e metadata. Lo scheduler, invece, si occupa di decidere quando avviare il job.

Scheduler
   ↓
JobLauncher
   ↓
Spring Batch Job
   ↓
Step
   ↓
Reader → Processor → Writer
   ↓
JobRepository

Concetto chiave: Spring Batch non è uno scheduler. Spring Batch è un framework batch. Lo scheduling deve essere gestito con Spring Scheduling, Quartz o strumenti esterni.

Spring Batch e scheduling: due responsabilità diverse

Uno degli errori più comuni è pensare che Spring Batch includa automaticamente un sistema di pianificazione dei job. In realtà Spring Batch fornisce tutto ciò che serve per eseguire e monitorare un job batch, ma non decide da solo quando farlo partire.

Responsabilità Strumento Cosa fa
Definire il job Spring Batch Definisce job, step, reader, processor e writer.
Lanciare il job JobLauncher Avvia un job con determinati parametri.
Memorizzare lo stato JobRepository Salva metadata, esecuzioni, stati e parametri.
Programmare l’esecuzione @Scheduled, TaskScheduler, Quartz Decide quando lanciare il job.

Questa separazione è utile perché permette di lanciare lo stesso job in modi diversi. Per esempio, un job può essere eseguito da:

  • uno scheduler interno;
  • un endpoint REST;
  • un comando da terminale;
  • un sistema esterno come Control-M, Airflow o Jenkins;
  • un cron di sistema;
  • Kubernetes CronJob;
  • Quartz Scheduler.

Componenti principali: Job, JobLauncher e JobParameters

Prima di parlare di scheduling, è importante capire i componenti principali coinvolti nell’avvio di un job Spring Batch.

Job

Il Job rappresenta il batch da eseguire. È composto da uno o più step.

@Bean
public Job importProductsJob(JobRepository jobRepository, Step importProductsStep) {
    return new JobBuilder("importProductsJob", jobRepository)
            .start(importProductsStep)
            .build();
}

Step

Uno Step rappresenta una fase del job. Può leggere dati, elaborarli e scriverli.

@Bean
public Step importProductsStep(JobRepository jobRepository,
                               PlatformTransactionManager transactionManager) {
    return new StepBuilder("importProductsStep", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("Esecuzione dello step");
                return RepeatStatus.FINISHED;
            }, transactionManager)
            .build();
}

JobLauncher

Il JobLauncher è il componente usato per avviare un job.

JobExecution execution = jobLauncher.run(job, jobParameters);

JobParameters

I JobParameters identificano una specifica istanza del job. Questo punto è fondamentale: Spring Batch considera lo stesso job con gli stessi parametri come la stessa JobInstance.

Se un job con gli stessi parametri è già stato completato, un nuovo lancio può generare l’errore:

A job instance already exists and is complete for parameters={}

Per questo motivo, quando scheduliamo un job periodico, spesso dobbiamo aggiungere un parametro univoco, come un timestamp.

Schedulare un job con @Scheduled

Il modo più semplice per creare uno Spring Batch scheduled job è usare l’annotazione @Scheduled.

Questo approccio è adatto quando:

  • hai una schedulazione semplice;
  • il job gira nella stessa applicazione Spring Boot;
  • non ti serve persistenza avanzata dello scheduler;
  • non hai bisogno di clustering dello scheduler;
  • vuoi una soluzione semplice e leggibile.

Abilitare lo scheduling

Per usare @Scheduled, devi abilitare lo scheduling nell’applicazione.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class BatchApplication {

    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

Scheduler semplice con fixedDelay

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ProductBatchScheduler {

    private final JobLauncher jobLauncher;
    private final Job importProductsJob;

    public ProductBatchScheduler(JobLauncher jobLauncher, Job importProductsJob) {
        this.jobLauncher = jobLauncher;
        this.importProductsJob = importProductsJob;
    }

    @Scheduled(fixedDelay = 5000)
    public void runJob() throws Exception {
        JobParameters parameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();

        JobExecution execution = jobLauncher.run(importProductsJob, parameters);

        System.out.println("Exit Status: " + execution.getStatus());
    }
}

In questo esempio, il job viene lanciato ogni 5 secondi dopo la fine dell’esecuzione precedente.

Nota importante: il parametro time serve a creare una nuova istanza del job a ogni esecuzione. Senza parametri univoci potresti ottenere l’errore “job instance already complete”.

TaskScheduler in Spring

TaskScheduler è l’astrazione di Spring per schedulare task in modo programmatico. È più flessibile di @Scheduled quando vuoi creare o modificare pianificazioni a runtime.

Puoi usare TaskScheduler quando:

  • la schedulazione non è statica;
  • la frequenza arriva dal database;
  • vuoi registrare task dinamicamente;
  • devi controllare manualmente l’avvio dello scheduler;
  • vuoi usare trigger personalizzati.

Configurazione di ThreadPoolTaskScheduler

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class SchedulerConfig {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("batch-scheduler-");
        return scheduler;
    }
}

Schedulare un job programmaticamente

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
import java.time.Instant;
import java.util.Date;

@Component
public class DynamicBatchScheduler {

    private final TaskScheduler taskScheduler;
    private final JobLauncher jobLauncher;
    private final Job importProductsJob;

    public DynamicBatchScheduler(TaskScheduler taskScheduler,
                                 JobLauncher jobLauncher,
                                 Job importProductsJob) {
        this.taskScheduler = taskScheduler;
        this.jobLauncher = jobLauncher;
        this.importProductsJob = importProductsJob;
    }

    @PostConstruct
    public void scheduleJob() {
        taskScheduler.scheduleAtFixedRate(this::launchJob, Date.from(Instant.now()), 10000);
    }

    private void launchJob() {
        try {
            jobLauncher.run(
                    importProductsJob,
                    new JobParametersBuilder()
                            .addLong("time", System.currentTimeMillis())
                            .toJobParameters()
            );
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Questo approccio è utile quando vuoi controllare lo scheduling dal codice, ma per la maggior parte dei casi semplici @Scheduled è più leggibile.

JobParameters e job già completato

Uno dei problemi più comuni quando si usa uno Spring Batch Scheduler è l’errore:

A job instance already exists and is complete for parameters={}.
If you want to run this job again, change the parameters.

Questo errore non è un bug. È il comportamento normale di Spring Batch.

Spring Batch identifica una JobInstance con:

Job name + JobParameters

Se lanci lo stesso job con gli stessi parametri e quella job instance è già completata, Spring Batch impedisce una nuova esecuzione identica.

Esempio sbagliato

JobParameters parameters = new JobParameters();

jobLauncher.run(importProductsJob, parameters);

Se questo codice viene eseguito più volte, i parametri sono sempre vuoti. Quindi, dopo la prima esecuzione completata, Spring Batch riconosce che quella stessa istanza del job è già stata completata.

Esempio corretto con timestamp

JobParameters parameters = new JobParametersBuilder()
        .addLong("time", System.currentTimeMillis())
        .toJobParameters();

jobLauncher.run(importProductsJob, parameters);

Esempio con data logica

In alcuni casi è meglio usare una data di business invece di un timestamp tecnico.

JobParameters parameters = new JobParametersBuilder()
        .addString("businessDate", "2026-07-08")
        .toJobParameters();

La scelta dipende dal tipo di batch. Se il job deve essere eseguito una volta per ogni giornata contabile, ha senso usare una business date. Se invece deve girare ogni pochi minuti, un timestamp può essere sufficiente.

Regola pratica: non aggiungere parametri casuali senza pensarci. I JobParameters definiscono l’identità dell’esecuzione batch e incidono su restart, recovery e tracciabilità.

Spring Batch con cron expression

Per eseguire un job secondo una pianificazione simile a cron, puoi usare l’attributo cron di @Scheduled.

Esecuzione ogni giorno alle 02:00

@Scheduled(cron = "0 0 2 * * *")
public void runNightlyJob() throws Exception {
    JobParameters parameters = new JobParametersBuilder()
            .addLong("time", System.currentTimeMillis())
            .toJobParameters();

    jobLauncher.run(importProductsJob, parameters);
}

Esecuzione ogni 5 minuti

@Scheduled(cron = "0 */5 * * * *")
public void runEveryFiveMinutes() throws Exception {
    JobParameters parameters = new JobParametersBuilder()
            .addLong("time", System.currentTimeMillis())
            .toJobParameters();

    jobLauncher.run(importProductsJob, parameters);
}

Cron con timezone

@Scheduled(cron = "0 0 2 * * *", zone = "Europe/Rome")
public void runAtTwoInRome() throws Exception {
    JobParameters parameters = new JobParametersBuilder()
            .addLong("time", System.currentTimeMillis())
            .toJobParameters();

    jobLauncher.run(importProductsJob, parameters);
}

Specificare il timezone è utile in applicazioni internazionali o in ambienti cloud dove il server potrebbe usare UTC.

Spring Boot Batch Scheduler

In Spring Boot, uno Spring Boot Batch Scheduler si costruisce spesso combinando:

  • spring-boot-starter-batch;
  • @EnableScheduling;
  • @Scheduled;
  • JobLauncher;
  • JobParametersBuilder.

Dipendenze Maven di esempio

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

Disabilitare il lancio automatico dei job

Spring Boot può avviare automaticamente i job batch all’avvio dell’applicazione. Se vuoi che il job parta solo tramite scheduler, puoi disabilitare l’esecuzione automatica.

spring.batch.job.enabled=false

In questo modo il job non parte allo startup, ma solo quando lo lancia il tuo scheduler.

Struttura consigliata

BatchApplication
   ↓
@EnableScheduling
   ↓
BatchScheduler
   ↓
JobLauncher
   ↓
Job
   ↓
Step

Quartz Scheduler con Spring Batch

Quartz Scheduler è una libreria open source molto usata per schedulare job in applicazioni Java. Rispetto a @Scheduled, Quartz è più potente e adatto a scenari complessi.

Ha senso usare Quartz con Spring Batch quando hai bisogno di:

  • scheduling persistente;
  • trigger complessi;
  • misfire handling;
  • cluster dello scheduler;
  • gestione avanzata dei calendari;
  • controllo più fine delle esecuzioni;
  • integrazione con tabelle Quartz nel database.

Dipendenza Maven

In un progetto Spring Boot moderno puoi usare lo starter Quartz:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

Job Quartz che lancia un job Spring Batch

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class BatchQuartzJob extends QuartzJobBean {

    private JobLauncher jobLauncher;
    private Job importProductsJob;

    public void setJobLauncher(JobLauncher jobLauncher) {
        this.jobLauncher = jobLauncher;
    }

    public void setImportProductsJob(Job importProductsJob) {
        this.importProductsJob = importProductsJob;
    }

    @Override
    protected void executeInternal(JobExecutionContext context)
            throws JobExecutionException {

        try {
            jobLauncher.run(
                    importProductsJob,
                    new JobParametersBuilder()
                            .addLong("time", System.currentTimeMillis())
                            .toJobParameters()
            );
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }
}

Configurazione Quartz con JobDetail e Trigger

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.quartz.CronScheduleBuilder.cronSchedule;

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail batchJobDetail() {
        return JobBuilder.newJob(BatchQuartzJob.class)
                .withIdentity("importProductsQuartzJob")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger batchJobTrigger(JobDetail batchJobDetail) {
        return TriggerBuilder.newTrigger()
                .forJob(batchJobDetail)
                .withIdentity("importProductsTrigger")
                .withSchedule(cronSchedule("0 */5 * * * ?"))
                .build();
    }
}

Questa configurazione crea un job Quartz e un trigger che lo esegue ogni 5 minuti.

Attenzione: Quartz Job e Spring Batch Job non sono la stessa cosa. Il job Quartz serve a pianificare l’esecuzione. Il job Spring Batch contiene la logica batch vera e propria.

Quartz con Spring Boot

Spring Boot semplifica l’uso di Quartz. Se Quartz è presente nel classpath, Spring Boot può autoconfigurare uno Scheduler tramite SchedulerFactoryBean.

Configurazione base in application.properties

spring.quartz.job-store-type=memory

Questa configurazione usa uno store in memoria. È semplice, ma non persistente.

Configurazione JDBC store

spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=always

Con JDBC store, Quartz può salvare job, trigger e stato dello scheduler nel database. Questo è utile per applicazioni più robuste o ambienti cluster.

Quando usare memory store

  • ambiente locale;
  • test;
  • scheduling non critico;
  • applicazione singola e semplice.

Quando usare JDBC store

  • produzione;
  • necessità di persistenza;
  • scheduler cluster;
  • recupero dopo riavvio;
  • job critici.

TaskScheduler vs Quartz

Una domanda molto comune è: meglio usare @Scheduled, TaskScheduler o Quartz?

Strumento Quando usarlo Complessità
@Scheduled Schedulazioni semplici e statiche. Bassa
TaskScheduler Schedulazioni programmatiche o dinamiche. Media
Quartz Scheduling avanzato, persistente o clusterizzato. Alta
Scheduler esterno Ambienti enterprise, Kubernetes, Airflow, Control-M, Jenkins. Variabile

Usa @Scheduled se

  • devi eseguire un job ogni tot minuti;
  • la pianificazione è fissa;
  • l’applicazione non è clusterizzata;
  • non hai bisogno di persistenza dello scheduler.

Usa Quartz se

  • hai molte schedulazioni;
  • devi modificare trigger a runtime;
  • ti serve uno scheduler persistente;
  • devi gestire misfire;
  • hai più istanze applicative;
  • hai bisogno di clustering.

Usa uno scheduler esterno se

  • il batch fa parte di una pipeline più grande;
  • deve essere orchestrato con altri sistemi;
  • hai flussi dati complessi;
  • usi Kubernetes CronJob;
  • vuoi separare scheduling e applicazione.

Come evitare esecuzioni concorrenti

Un problema importante nello Spring Batch scheduling è evitare che lo stesso job venga lanciato mentre una precedente esecuzione è ancora in corso.

Questo può accadere quando:

  • il job dura più dell’intervallo di scheduling;
  • usi fixedRate invece di fixedDelay;
  • hai più istanze dell’applicazione attive;
  • lo scheduler è eseguito in cluster senza coordinamento;
  • il job non controlla lo stato delle esecuzioni precedenti.

fixedDelay vs fixedRate

Attributo Comportamento
fixedDelay Aspetta un intervallo dopo la fine dell’esecuzione precedente.
fixedRate Prova a rispettare un intervallo fisso tra gli avvii.

Per job batch lunghi, spesso fixedDelay è più sicuro.

Esempio con controllo manuale dello stato

import java.util.concurrent.atomic.AtomicBoolean;

@Component
public class SafeBatchScheduler {

    private final AtomicBoolean running = new AtomicBoolean(false);

    @Scheduled(fixedDelay = 60000)
    public void run() {
        if (!running.compareAndSet(false, true)) {
            System.out.println("Job già in esecuzione, salto il lancio");
            return;
        }

        try {
            // lancio del job
        } finally {
            running.set(false);
        }
    }
}

Questo controllo funziona in una singola istanza applicativa. Se hai più nodi, serve un lock distribuito o uno scheduler cluster-aware.

Soluzioni per ambienti cluster

  • Quartz con JDBC store e clustering;
  • ShedLock;
  • lock su database;
  • Kubernetes CronJob con policy adeguata;
  • scheduler esterno centralizzato.

Nota pratica: se l’applicazione gira su più istanze, @Scheduled verrà eseguito su ogni istanza, salvo meccanismi di coordinamento esterno.

Restart, retry e job falliti

Spring Batch è pensato per gestire job robusti, riavviabili e tracciabili. Quando un job fallisce, lo scheduler non dovrebbe limitarsi a rilanciarlo senza capire lo stato precedente.

Job fallito

Se un job fallisce con determinati parametri, Spring Batch può permettere il restart della stessa job instance, a seconda della configurazione del job e degli step.

Job completato

Se un job è già completato con determinati parametri, Spring Batch non lo rilancia con gli stessi parametri.

Retry interno allo step

Per errori temporanei durante la lettura, elaborazione o scrittura, puoi configurare retry a livello di step.

@Bean
public Step importStep(JobRepository jobRepository,
                       PlatformTransactionManager transactionManager,
                       ItemReader<Product> reader,
                       ItemProcessor<Product, Product> processor,
                       ItemWriter<Product> writer) {

    return new StepBuilder("importStep", jobRepository)
            .<Product, Product>chunk(100, transactionManager)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .faultTolerant()
            .retry(RuntimeException.class)
            .retryLimit(3)
            .build();
}

Restart e parametri

Per permettere un restart corretto, i parametri devono rappresentare una vera istanza logica del job. Per esempio, se un job processa la giornata 2026-07-08, usare quella data come parametro può essere meglio di un timestamp casuale.

JobParameters parameters = new JobParametersBuilder()
        .addString("businessDate", "2026-07-08")
        .toJobParameters();

Se il job fallisce, puoi rilanciarlo con gli stessi parametri per completare la stessa istanza logica.

Attenzione: usare sempre System.currentTimeMillis() crea sempre nuove istanze. È comodo per job dimostrativi, ma non sempre è corretto per job di produzione.

Configurazione XML legacy

Nei vecchi progetti Spring era comune configurare lo scheduler tramite XML. Questo approccio è ancora utile da conoscere quando si lavora su applicazioni legacy.

Namespace task

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/task
           http://www.springframework.org/schema/task/spring-task.xsd">

</beans>

Scheduled task XML

<task:scheduled-tasks>
    <task:scheduled ref="helloWorldLauncher"
                    method="run"
                    fixed-delay="5000" />
</task:scheduled-tasks>

Bean launcher

<bean id="helloWorldLauncher"
      class="it.javaboss.batch.HelloWorldLauncher">
    <property name="jobLauncher" ref="jobLauncher" />
    <property name="job" ref="helloWorldJob" />
</bean>

Classe launcher legacy

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;

public class HelloWorldLauncher {

    private JobLauncher jobLauncher;
    private Job job;

    public void setJobLauncher(JobLauncher jobLauncher) {
        this.jobLauncher = jobLauncher;
    }

    public void setJob(Job job) {
        this.job = job;
    }

    public void run() {
        try {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addLong("time", System.currentTimeMillis())
                    .toJobParameters();

            JobExecution execution = jobLauncher.run(job, jobParameters);

            System.out.println("Exit Status: " + execution.getStatus());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Questo approccio riflette lo stile delle applicazioni Spring più vecchie. In un progetto nuovo è preferibile usare configurazione Java, Spring Boot e annotazioni.

Configurazione Quartz XML legacy

Anche Quartz veniva spesso configurato tramite XML nei progetti Spring legacy.

JobRegistry

<bean id="jobRegistry"
      class="org.springframework.batch.core.configuration.support.MapJobRegistry" />

<bean class="org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor">
    <property name="jobRegistry" ref="jobRegistry" />
</bean>

Quartz JobBean

import java.util.Map;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class HelloWorldJobLauncherDetails extends QuartzJobBean {

    private JobLocator jobLocator;
    private JobLauncher jobLauncher;

    public void setJobLocator(JobLocator jobLocator) {
        this.jobLocator = jobLocator;
    }

    public void setJobLauncher(JobLauncher jobLauncher) {
        this.jobLauncher = jobLauncher;
    }

    @Override
    protected void executeInternal(JobExecutionContext context)
            throws JobExecutionException {

        Map<String, Object> jobDataMap = context.getMergedJobDataMap();
        String jobName = (String) jobDataMap.get("jobName");

        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters();

        try {
            JobExecution execution = jobLauncher.run(
                    jobLocator.getJob(jobName),
                    jobParameters
            );

            System.out.println("Exit Status: " + execution.getStatus());
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }
}

SchedulerFactoryBean XML

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <bean id="cronTrigger"
              class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
            <property name="jobDetail" ref="jobDetail" />
            <property name="cronExpression" value="*/5 * * * * ?" />
        </bean>
    </property>
</bean>

<bean id="jobDetail"
      class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass"
              value="it.javaboss.batch.HelloWorldJobLauncherDetails" />
    <property name="group" value="quartz-batch" />
    <property name="jobDataAsMap">
        <map>
            <entry key="jobName" value="helloWorldJob" />
            <entry key="jobLocator" value-ref="jobRegistry" />
            <entry key="jobLauncher" value-ref="jobLauncher" />
        </map>
    </property>
</bean>

Nota legacy: in molti articoli vecchi troverai classi come CronTriggerBean o JobDetailBean. Nei progetti moderni Spring è più comune usare CronTriggerFactoryBean e JobDetailFactoryBean, oppure configurazione Java.

Best practice per Spring Batch Scheduler

  • Usa @Scheduled per schedulazioni semplici.
  • Usa Quartz per scheduling persistente, cluster o molto complesso.
  • Disabilita il lancio automatico dei job se vuoi avviarli solo tramite scheduler.
  • Usa JobParameters significativi.
  • Non usare sempre timestamp se ti serve restart della stessa istanza logica.
  • Evita esecuzioni concorrenti dello stesso job.
  • Configura un pool di thread adeguato.
  • Monitora stato, durata ed errori dei job.
  • Gestisci retry e restart in modo esplicito.
  • Usa un JobRepository persistente in produzione.
  • Non lasciare database in-memory per job reali.
  • Configura logging e metriche.
  • Valuta scheduler esterni se il batch fa parte di una pipeline enterprise.

Errori comuni

Dimenticare JobParameters univoci

Se lanci sempre lo stesso job con gli stessi parametri, Spring Batch può impedire nuove esecuzioni perché considera il job già completato.

Usare @Scheduled senza @EnableScheduling

Senza @EnableScheduling, i metodi annotati con @Scheduled non vengono pianificati.

Usare fixedRate per job lunghi

Se un job dura più dell’intervallo configurato, fixedRate può portare a esecuzioni sovrapposte. In molti batch è meglio fixedDelay.

Schedulare lo stesso job su più nodi

Se hai più istanze dell’applicazione, ogni istanza può eseguire lo scheduler. Serve un lock distribuito o uno scheduler cluster-aware.

Usare database in-memory in produzione

Il JobRepository deve essere persistente se vuoi restart, tracciabilità e monitoraggio reale.

Confondere Quartz Job e Spring Batch Job

Quartz pianifica l’esecuzione. Spring Batch esegue il lavoro batch. Sono concetti diversi.

Non gestire errori e alert

Un batch schedulato che fallisce in silenzio è pericoloso. Devi loggare, monitorare e notificare gli errori.

Checklist per mettere in produzione uno scheduler batch

Controllo Domanda da farsi
JobRepository È persistente o ancora in-memory?
Parametri I JobParameters identificano correttamente l’esecuzione?
Concorrenza Il job può partire due volte contemporaneamente?
Cluster Lo scheduler gira su più nodi?
Retry Gli errori temporanei sono gestiti?
Restart Un job fallito può ripartire correttamente?
Logging I log permettono di capire cosa è successo?
Alert Qualcuno viene avvisato se il job fallisce?
Timezone La cron expression usa il timezone corretto?
Shutdown Cosa succede se l’app si ferma durante un job?

FAQ su Spring Batch Scheduler

Che cos’è Spring Batch Scheduler?

È il modo in cui un job Spring Batch viene eseguito automaticamente secondo una pianificazione, usando strumenti come @Scheduled, TaskScheduler, Quartz o scheduler esterni.

Spring Batch include uno scheduler?

No. Spring Batch gestisce i job batch, ma lo scheduling deve essere gestito con Spring Scheduling, Quartz o strumenti esterni.

Come posso schedulare un job Spring Batch con Spring Boot?

Puoi abilitare @EnableScheduling, creare un componente con @Scheduled e usare JobLauncher per avviare il job con JobParameters.

Perché ottengo “A job instance already exists and is complete”?

Perché stai rilanciando lo stesso job con gli stessi parametri già completati. Devi usare parametri diversi o una strategia corretta di restart.

Devo usare sempre System.currentTimeMillis nei JobParameters?

No. È utile per demo o job che devono creare sempre una nuova istanza, ma in produzione spesso è meglio usare parametri di business come data contabile, nome file o periodo elaborato.

Meglio @Scheduled o Quartz?

@Scheduled è più semplice e va bene per schedulazioni statiche. Quartz è più adatto a scheduling persistente, cluster, trigger complessi e misfire handling.

Come evitare che un job parta due volte?

Puoi usare fixedDelay, controlli applicativi, JobRepository, lock distribuiti, ShedLock, Quartz cluster o scheduler esterni.

Posso usare cron con Spring Batch?

Sì. Puoi usare @Scheduled(cron = "...") oppure Quartz con CronTrigger.

Spring Boot può avviare job automaticamente?

Sì. Spring Boot può lanciare job batch all’avvio. Se vuoi controllare l’avvio solo tramite scheduler, puoi configurare spring.batch.job.enabled=false.

Quartz Job e Spring Batch Job sono la stessa cosa?

No. Un Quartz Job serve a schedulare un’attività. Un Spring Batch Job contiene la logica batch composta da step.

Serve un database per Spring Batch?

In produzione è consigliabile usare un database persistente per il JobRepository, in modo da tracciare esecuzioni, stati, errori e restart.

Serve un database per Quartz?

Non sempre. Quartz può usare uno store in memoria, ma per persistenza, clustering e produzione è spesso preferibile uno store JDBC.

Conclusione

Lo Spring Batch Scheduler è una parte fondamentale quando vogliamo eseguire job batch in modo periodico, controllato e affidabile. Spring Batch definisce ed esegue il job, mentre lo scheduler decide quando avviarlo.

Per schedulazioni semplici, @Scheduled è spesso la scelta migliore: è leggibile, veloce da configurare e perfettamente integrato con Spring Boot. Per scenari più complessi, Quartz offre funzionalità avanzate come trigger persistenti, misfire handling e clustering.

Il punto più importante da ricordare è che un job Spring Batch deve essere lanciato con JobParameters coerenti. Parametri vuoti o sempre uguali possono causare l’errore “job instance already complete”. Parametri troppo casuali, invece, possono rendere difficile il restart di una specifica esecuzione logica.

Usare bene Spring Batch con uno scheduler significa progettare correttamente job, parametri, repository, concorrenza, error handling, logging e monitoraggio.

Riassunto finale: usa @Scheduled per job semplici, Quartz per scheduling avanzato e uno scheduler esterno quando il batch fa parte di un flusso enterprise più ampio. In ogni caso, cura sempre JobParameters, concorrenza e tracciabilità.

Risorse ufficiali utili

Categorie: Spring