Spring Batch main is at 6.1.0-SNAPSHOT. This window landed 40 commits across 99 files, with 2916 insertions and 1071 deletions. Operators will notice fewer JDBC metadata queries, a failed launch that leaves no empty job instance, and two restart bugs that could drop work already committed.
Job execution history is loaded in two queries ¶
JdbcJobExecutionDao.findJobExecutions selected execution ids, then loaded each execution, and each of those loads also read the job instance and the parameters. That is 1 + 3N queries. Ten executions meant 31 round trips. The new path selects the executions in one query, loads parameters in a second query keyed by execution id, and reuses the instance the caller already holds. Those ten executions are now 2 queries. getLastJobExecution was discarding the given instance the same way, and it goes from 4 queries to 2. Issue 5495.
Loading an execution skips the extra join back to the instance table. JOB_INSTANCE_ID already lives on JOB_EXECUTION, and the row mapper loads the instance and the parameters only after the result set closes. getLastStepExecution reads both halves from one join, with job execution columns prefixed JE_ so they do not collide with names StepExecutionRowMapper looks up.
updateJobExecution dropped the SELECT COUNT(*) it ran before every update. A missing row now surfaces as OptimisticLockingFailureException when the update count is 0, matching JdbcStepExecutionDao. That check was redundant with synchronizeStatus. Restart also reads only the last execution of the instance, since only one can be active. Issue 4169.
A failed launch no longer leaves an empty job instance ¶
Before 6.0, parameter validation ran first. The job instance, the job execution, and the parameters were inserted in one transaction. A failed launch left nothing behind.
After immutable domain entities, TaskExecutorJobLauncher created the job instance in its own REQUIRES_NEW transaction, then validated, then created the execution. A failure after that first commit left an instance with no execution. A rejecting validator does it. So does a parameter the repository cannot store, such as java.time.Year with no JDBC String converter and no Mongo codec. The next launch with the same parameters throws IllegalStateException, and so does startNextInstance on a job with an incrementer. Issue 5562.
The fix validates before any write. Fresh starts call JobRepository.createJobExecution(String, JobParameters), added in 6.1, which creates the instance and the first execution in one transaction. A failure rolls both back. Restart still creates an execution on the existing instance. Instances already orphaned by 6.0 and by earlier 6.1 builds stay in the tables. Delete those rows by hand.
default JobExecution createJobExecution(String jobName, JobParameters jobParameters) {
JobInstance jobInstance = createJobInstance(jobName, jobParameters);
return createJobExecution(jobInstance, jobParameters, new ExecutionContext());
}
Restart stops dropping work that already committed ¶
On a partitioned restart the splitter returns only the partitions that still need to run. The manager aggregated that partial set, and the aggregate was empty when every partition had already completed. Completed partitions are included now. RemoteStepExecutionAggregator keeps the completed ones, since they are not part of the current job execution. Issue 5158.
Chunk scanning could lose items. When a chunk write throws an exception the skip policy accepts, ChunkOrientedStep reprocesses each item in its own transaction. Those commits were also saving the reader. It had already consumed the chunk, so the stored position sat past the last item. If the scan then failed, restart resumed after the chunk, the unwritten items were gone, and the step could still complete. Issue 5558.
The reader checkpoint now stays where it was before the scan. readsSinceCheckpoint counts committed reads since that point. Restart replays them, discards the items, and continues. Replayed reads honor retry and skip policy, do not call read or skip listeners, and leave the step counts alone.
That path also logged Retry exhausted at info and the rollback at error, even when the retry policy allows zero retries. Both are debug now, with the writer exception on the debug line. A rejection from the skip policy still logs Chunk write failed with an exception that cannot be skipped at error. Pagers on Rolling back chunk transaction at error will miss ordinary skips. Issue 5527.
Flat file parsing fixes and a breaking JDBC DAO constructor ¶
A token that is only the quote character threw StringIndexOutOfBoundsException from DelimitedLineTokenizer. The quote guard used the line length, matched both ends on that one character, and built a negative substring. It checks the token length now and returns the quote as the field. Issue 5519. FixedLengthTokenizer set its open flag only when an unbounded min was strictly above the running max, so a single open ended column never qualified and longer lines failed with IncorrectLineLengthException in strict mode. Any range with no max marks it open. Issue 5518. FlatFileItemReaderBuilder.comments(String...) stored Arrays.asList, so addComment threw UnsupportedOperationException. The list is mutable now. Issue 5520. JpaPagingItemReader.close threw NullPointerException when entityManager was still null. Close checks first. Issue 5539.
The JDBC DAOs moved to JdbcClient. Queries took named parameters first. Inserts and updates followed, including a step execution insert that binds nineteen values. A misspelled name fails with InvalidDataAccessApiUsageException at runtime.
INSERT INTO %PREFIX%JOB_EXECUTION(JOB_EXECUTION_ID, JOB_INSTANCE_ID, START_TIME, END_TIME, STATUS, EXIT_CODE, EXIT_MESSAGE, VERSION, CREATE_TIME, LAST_UPDATED)
VALUES (:jobExecutionId, :jobInstanceId, :startTime, :endTime, :status, :exitCode, :exitMessage, :version, :createTime, :lastUpdated)
The four DAOs now require a JdbcClient in the constructor. The no argument constructors are gone, so 6.0 code that calls new JdbcJobInstanceDao() and then setJdbcTemplate will not compile. setJdbcTemplate and getJdbcTemplate on AbstractJdbcBatchMetadataDao are deprecated, removal set for 7.0. setJdbcClient is already gone. @EnableBatchProcessing, @EnableJdbcJobRepository, JdbcDefaultBatchConfiguration, JdbcJobRepositoryFactoryBean, and JobExplorerFactoryBean build the DAOs, so those setups are unaffected.
AsyncItemProcessor and AsyncItemWriter now use Future<@Nullable T> after a nullability pass. ChunkOrientedStep got the same annotations. Error Prone 2.44.0 crashes on JDK 26 in NestedInstanceOfConditions, so the build uses Error Prone 2.50.0 and NullAway 0.14.2. java.version stays 17.
spring-batch-infrastructure drops unused Jackson 2 jackson-databind and moves hibernate-core, spring-data-jpa, and mongodb-driver-sync to test scope. spring-batch-integration does the same for spring-integration-jms and spring-integration-jdbc.
What to watch ¶
Orphan job instances from 6.0 and from earlier 6.1 builds are still stored. Nothing in this change deletes them. Relaunch with the same parameters still throws IllegalStateException until the row is removed, and startNextInstance does the same when the job has an incrementer.
Hand built JDBC DAOs must take a JdbcClient. setJdbcTemplate is on the removal list for 7.0. The factory beans and enable annotations above already supply the client.
Chunk scan restart replays reads up to readsSinceCheckpoint. A read that is unsafe to repeat, or a skip policy that rejects the replay, fails a restart that used to complete. Readers with saveState(false) keep their own restart behavior.