Running & Scheduling Jobs
Writing a job is half the work; the other half is launching it correctly. Spring Batch jobs can run automatically on startup, on demand from a REST endpoint, or on a timer. The JobLauncher and JobParameters control how an instance is identified — which in turn determines whether a run is a fresh job or a restart. This page covers launching, parameter uniqueness, restart/skip/retry, scheduling, and listeners. The jobs themselves are built in Jobs & Steps.
Running on startup vs on demand
By default Spring Boot runs every Job bean once at application startup. That is convenient for a one-shot importer but wrong for a long-running web app, where you want to trigger jobs yourself. Disable auto-run with a property:
spring:
batch:
job:
enabled: false # don't run jobs at startup; launch them explicitly
Note: In older Spring Boot you listed job names under
spring.batch.job.names. On Spring Boot 3 that property was removed — startup execution is the all-or-nothingspring.batch.job.enabledflag. To run one specific job at startup, keep auto-run off and launch it from anApplicationRunner.
JobLauncher and JobParameters
To launch a job explicitly, inject the auto-configured JobLauncher and the Job bean and call run(job, params).
import org.springframework.batch.core.*;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.stereotype.Service;
@Service
public class CustomerImportRunner {
private final JobLauncher jobLauncher;
private final Job customerImportJob;
public CustomerImportRunner(JobLauncher jobLauncher, Job customerImportJob) {
this.jobLauncher = jobLauncher;
this.customerImportJob = customerImportJob;
}
public JobExecution launch(String inputFile) throws Exception {
JobParameters params = new JobParametersBuilder()
.addString("inputFile", inputFile)
.addLocalDateTime("runTime", java.time.LocalDateTime.now())
.toJobParameters();
return jobLauncher.run(customerImportJob, params);
}
}
Why unique parameters matter
A JobInstance is identified by the job name plus its identifying parameters. Spring Batch enforces a rule: a completed instance cannot be run again. Launch a job twice with identical parameters and the second call throws JobInstanceAlreadyCompleteException.
| Situation | What you want | How |
|---|---|---|
| Reprocess the same file nightly | a new instance each run | add a unique parameter (timestamp, run id) |
| Resume a failed run | the same instance, restarted | reuse the identical identifying parameters |
| Pass a value that shouldn’t affect identity | a non-identifying parameter | addString("k", v, false) |
Adding a timestamp (as above) makes each launch a new instance. To make a parameter informational only — present in the run but not part of the instance key — pass false as the last argument: .addString("inputFile", file, false).
Warning: If your job is restartable, do not blindly add a timestamp parameter — that makes every launch a brand-new instance and you lose the ability to resume the failed one. Use a stable identifying parameter (like the business date) for restartable jobs, and a unique one only for jobs meant to run fresh each time.
Restartability, skip, and retry
When a chunk step fails, the JobRepository records where it stopped. Relaunching with the same identifying parameters resumes at the failed step (and, for paging readers, near the failed position). Two policies make steps resilient to bad data and transient errors.
Skip tolerates a bounded number of bad records instead of failing the whole job:
return new StepBuilder("importStep", jobRepository)
.<CustomerCsv, CustomerEntity>chunk(500, txManager)
.reader(reader).processor(processor).writer(writer)
.faultTolerant()
.skip(org.springframework.batch.item.file.FlatFileParseException.class)
.skipLimit(50) // give up after 50 bad rows
.build();
Retry re-attempts a chunk when a transient exception occurs (a deadlock, a brief network blip) before giving up:
.faultTolerant()
.retry(org.springframework.dao.DeadlockLoserDataAccessException.class)
.retryLimit(3)
Tip: Skip is for permanent data problems (a malformed row you want to log and move past); retry is for transient infrastructure problems (re-running the operation may succeed). Combine them, and add a
SkipListenerto record skipped items for later review.
Launching from a @Scheduled method
To run a job on a timer, enable scheduling and call the launcher from a @Scheduled method — feeding a unique parameter so each fire is a new instance. See Scheduling Tasks for cron syntax and the thread pool.
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@Configuration
@EnableScheduling
class BatchSchedule {
// @EnableScheduling activates @Scheduled methods app-wide
}
@Component
class NightlyTrigger {
private final JobLauncher jobLauncher;
private final Job customerImportJob;
NightlyTrigger(JobLauncher jobLauncher, Job customerImportJob) {
this.jobLauncher = jobLauncher;
this.customerImportJob = customerImportJob;
}
@Scheduled(cron = "0 0 2 * * *", zone = "UTC") // 02:00 daily
void runImport() throws Exception {
JobParameters params = new JobParametersBuilder()
.addLocalDate("runDate", java.time.LocalDate.now())
.toJobParameters();
jobLauncher.run(customerImportJob, params);
}
}
Using runDate (a date, not a full timestamp) as the identifying parameter means a job that fails at 02:00 can be retried the same day and resume — but it can only run once per calendar day, which is exactly right for a nightly job.
Launching from a REST endpoint
Operators often want to trigger a job by hitting a URL. Launch it asynchronously so the HTTP call returns immediately rather than blocking for the whole run.
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/jobs")
class JobController {
private final JobLauncher jobLauncher;
private final Job customerImportJob;
JobController(JobLauncher jobLauncher, Job customerImportJob) {
this.jobLauncher = jobLauncher;
this.customerImportJob = customerImportJob;
}
@PostMapping("/customer-import")
ResponseEntity<String> run(@RequestParam String file) throws Exception {
JobParameters params = new JobParametersBuilder()
.addString("inputFile", file)
.addLong("ts", System.currentTimeMillis()) // make each call a new instance
.toJobParameters();
JobExecution exec = jobLauncher.run(customerImportJob, params);
return ResponseEntity.accepted()
.body("Launched, executionId=" + exec.getId());
}
}
Output:
HTTP/1.1 202 Accepted
Launched, executionId=42
Note: The auto-configured
JobLauncherruns jobs synchronously by default, so an HTTP thread would block for the whole job. For a long job, define aTaskExecutorJobLauncherbacked by an@AsyncTaskExecutor, or launch from an async method, so the endpoint returns202 Acceptedright away and the job runs in the background.
Listeners
Listeners hook into the lifecycle for logging, metrics, alerting, or cleanup. The most common are JobExecutionListener and StepExecutionListener.
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
@Component
class ImportJobListener implements JobExecutionListener {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(ImportJobListener.class);
@Override
public void beforeJob(JobExecution jobExecution) {
log.info("Starting {}", jobExecution.getJobInstance().getJobName());
}
@Override
public void afterJob(JobExecution jobExecution) {
log.info("Finished with status {} ({} step executions)",
jobExecution.getStatus(), jobExecution.getStepExecutions().size());
}
}
Register it on the job:
return new JobBuilder("customerImportJob", jobRepository)
.listener(importJobListener)
.start(importStep)
.build();
Other lifecycle hooks include ItemReadListener, ItemProcessListener, ItemWriteListener, ChunkListener, and SkipListener (to record skipped items). You can also expose batch metrics through Actuator and Micrometer.