Configuring a job
|
This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
A Job bean is assembled with a JobBuilder: the steps in order, plus the cross-cutting configuration — restartability, parameter validation, an incrementer and listeners.
JobBuilder and step ordering
Since the removal of JobBuilderFactory, the builder is constructed directly with the job name and the
JobRepository:
@Bean
public Job endOfDayJob(JobRepository jobRepository,
Step stageFileStep, Step loadStep, Step reportStep) {
return new JobBuilder("endOfDayJob", jobRepository)
.start(stageFileStep)
.next(loadStep)
.next(reportStep)
.build();
}
.start(step) fixes the first step and returns a SimpleJobBuilder; .next(step) appends the next one.
Steps run sequentially, and the job stops at the first step that fails.
.flow(…) switches to the flow builder, which is what conditional transitions, splits and deciders need:
@Bean
public Job conditionalJob(JobRepository jobRepository, Flow mainFlow) {
return new JobBuilder("conditionalJob", jobRepository)
.start(mainFlow)
.end()
.build();
}
Everything that can be expressed inside a flow — .on(…), .to(…), .from(…), splits, deciders — is covered in Step flow & listeners.
Restartability and start limits
By default a failed or stopped JobInstance can be restarted, and completed steps are skipped on the retry.
Two switches change that.
.preventRestart() marks the job non-restartable: if an execution fails, that JobInstance is finished for
good, and attempting to launch it again throws JobRestartException. Use it when a partial run leaves the
system in a state that only a fresh instance can safely process:
@Bean
public Job oneShotJob(JobRepository jobRepository, Step transferStep) {
return new JobBuilder("oneShotJob", jobRepository)
.preventRestart()
.start(transferStep)
.build();
}
At the step level, startLimit caps how many times a given step may be started across all executions of one
instance — a guard against a step being retried indefinitely by repeated restarts — and
allowStartIfComplete(true) forces a step to run again on restart even though it already completed (useful for
validation or clean-up steps that must always run):
@Bean
public Step validateStep(JobRepository jobRepository, PlatformTransactionManager tx) {
return new StepBuilder("validateStep", jobRepository)
.tasklet(new ValidateInputTasklet(), tx)
.startLimit(3) // at most 3 attempts across restarts
.allowStartIfComplete(true) // re-run even if it completed previously
.build();
}
The restart semantics themselves — what is skipped, what is replayed — are in Stopping, restart & recovery.
Validating parameters
A job that requires a business date should refuse to start without one, rather than failing three steps in.
DefaultJobParametersValidator takes the required and optional key names:
@Bean
public JobParametersValidator endOfDayValidator() {
DefaultJobParametersValidator validator = new DefaultJobParametersValidator(
new String[] { "run.date" }, // required keys
new String[] { "output.dir", "dry.run" }); // optional keys
validator.afterPropertiesSet();
return validator;
}
@Bean
public Job endOfDayJob(JobRepository jobRepository, Step loadStep,
JobParametersValidator endOfDayValidator) {
return new JobBuilder("endOfDayJob", jobRepository)
.validator(endOfDayValidator)
.start(loadStep)
.build();
}
Any key that is neither required nor optional makes the launch fail — which catches typos in scheduler scripts before they cost a night.
For rules the default validator cannot express — a value range, a relationship between two parameters, a
readable file — implement JobParametersValidator:
public class RunDateValidator implements JobParametersValidator {
@Override
public void validate(JobParameters parameters) throws JobParametersInvalidException {
LocalDate runDate = parameters.getLocalDate("run.date");
if (runDate == null) {
throw new JobParametersInvalidException("run.date is required");
}
if (runDate.isAfter(LocalDate.now())) {
throw new JobParametersInvalidException(
"run.date must not be in the future, was " + runDate);
}
if (runDate.getDayOfWeek() == DayOfWeek.SUNDAY) {
throw new JobParametersInvalidException("no settlement run on Sundays");
}
}
}
Several validators can be combined with CompositeJobParametersValidator.
Incrementers
A JobParametersIncrementer computes the next parameter set from the previous one, so an operator can say
"run the next instance" without composing parameters by hand. RunIdIncrementer bumps a run.id counter:
@Bean
public Job nightlyJob(JobRepository jobRepository, Step loadStep) {
return new JobBuilder("nightlyJob", jobRepository)
.incrementer(new RunIdIncrementer())
.start(loadStep)
.build();
}
A date-based incrementer is often more meaningful, because the resulting instance identity carries business sense:
public class NextBusinessDateIncrementer implements JobParametersIncrementer {
@Override
public JobParameters getNext(JobParameters parameters) {
LocalDate previous = (parameters == null) ? null : parameters.getLocalDate("run.date");
LocalDate next = (previous == null) ? LocalDate.now() : previous.plusDays(1);
while (next.getDayOfWeek() == DayOfWeek.SATURDAY || next.getDayOfWeek() == DayOfWeek.SUNDAY) {
next = next.plusDays(1);
}
return new JobParametersBuilder(parameters == null ? new JobParameters() : parameters)
.addLocalDate("run.date", next)
.toJobParameters();
}
}
The incrementer is applied by JobOperator.startNextInstance(job) — see
Running a job. It is not applied by a plain start(job,
parameters), which uses exactly the parameters given.
JobExecutionListener
Job-level listeners run before the first step and after the last one, regardless of outcome. Implement the interface, or use the annotations:
@Component
public class JobTimingListener {
private static final Logger log = LoggerFactory.getLogger(JobTimingListener.class);
@BeforeJob
public void before(JobExecution jobExecution) {
log.info("starting {} execution {} with {}",
jobExecution.getJobInstance().getJobName(),
jobExecution.getId(),
jobExecution.getJobParameters());
}
@AfterJob
public void after(JobExecution jobExecution) {
Duration duration = Duration.between(jobExecution.getStartTime(), jobExecution.getEndTime());
log.info("{} finished as {} in {}s",
jobExecution.getJobInstance().getJobName(),
jobExecution.getStatus(),
duration.toSeconds());
if (jobExecution.getStatus() == BatchStatus.FAILED) {
jobExecution.getAllFailureExceptions()
.forEach(throwable -> log.error("failure", throwable));
}
}
}
@Bean
public Job endOfDayJob(JobRepository jobRepository, Step loadStep, JobTimingListener timingListener) {
return new JobBuilder("endOfDayJob", jobRepository)
.listener(timingListener)
.start(loadStep)
.build();
}
@AfterJob runs even when the job failed, which makes it the right place for notifications and clean-up.
Step-level and item-level listeners are covered in
Step flow & listeners.
Under Spring Boot, the concrete auto-configuration around all of this — the repository, the datasource, the startup runner — is described on Spring Batch (SpringBoot Reference).