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
- Spring Batch e scheduling: due responsabilità diverse
- Componenti principali: Job, JobLauncher e JobParameters
- Schedulare un job con @Scheduled
- TaskScheduler in Spring
- JobParameters e job già completato
- Spring Batch con cron expression
- Spring Boot Batch Scheduler
- Quartz Scheduler con Spring Batch
- Quartz con Spring Boot
- TaskScheduler vs Quartz
- Come evitare esecuzioni concorrenti
- Restart, retry e job falliti
- Configurazione XML legacy
- Best practice
- Errori comuni
- FAQ su Spring Batch Scheduler
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
timeserve 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
fixedRateinvece difixedDelay; - 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,
@Scheduledverrà 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
CronTriggerBeanoJobDetailBean. Nei progetti moderni Spring è più comune usareCronTriggerFactoryBeaneJobDetailFactoryBean, oppure configurazione Java.
Best practice per Spring Batch Scheduler
- Usa
@Scheduledper schedulazioni semplici. - Usa Quartz per scheduling persistente, cluster o molto complesso.
- Disabilita il lancio automatico dei job se vuoi avviarli solo tramite scheduler.
- Usa
JobParameterssignificativi. - 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
@Scheduledper 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à.