Add spring-batch: jobs, steps, chunk processing and restartability on Boot 4.1

Companion code for "Spring Batch on Boot 4.1: Jobs, Steps, Chunk Processing and
Restartability". A productImportJob configured three ways by profile against a
poisoned CSV row, run as real java -jar processes (not just JUnit) so the
restart story is genuine: a chunk fails and rolls back, the process exits, a
brand-new JVM against the same file-based H2 database resumes at the exact
next unread row (READ_COUNT 20, not 60) and completes.

Findings the build pins:
- StepBuilder.chunk(int, PlatformTransactionManager) still compiles in Batch
  6.0.5 but returns the legacy SimpleStepBuilder; chunk(int) returns the new
  ChunkOrientedStepBuilder, and only the latter is used here.
- Two different ExecutionContext classes now exist in two different packages
  (infrastructure.item vs core.repository.persistence) with different shapes.
- spring-boot-starter-batch alone gives a resourceless JobRepository that
  forgets every JobInstance the moment the JVM exits; spring-boot-starter-
  batch-jdbc is what makes the restart demo possible at all, demonstrated by
  excluding BatchJdbcAutoConfiguration and watching a "restart" collide with
  the previous run's own data instead of resuming it.
- A migration-guide summary claiming CommandLineJobRunner was removed in 6.0
  is wrong -- javap against the real jar shows @Deprecated(forRemoval=true),
  not removed.
- RepeatStatus moved from core.repeat to infrastructure.repeat, caught by the
  compiler rather than by reading docs.

11 documentation chapters, 10 captured transcripts (unit tests, javap output,
and real two-JVM scenario runs), all regenerated by scripts/run-all.sh.

Fixed after push: three dead docs.spring.io links in the doc chapters
(readersAndWriters/* and chunk-oriented-processing/*.html paths moved when
Spring Batch 6 reorganized its reference docs; corrected to the current
readers-and-writers/*, processor.html and chunk-oriented-processing.html
paths, verified 200 via curl before committing).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019DXsJ1zpikbA1MQJN6RqFA
This commit is contained in:
Claude
2026-09-13 06:37:46 +00:00
parent a9867c0423
commit b81af72bc3
40 changed files with 2001 additions and 0 deletions
@@ -0,0 +1,71 @@
# 10. Resourceless vs. JDBC-backed: the dependency that makes chapter 7 possible
[&larr; Previous](09-corrections.md) | [README](../README.md) | [Next: Production checklist &rarr;](11-production-checklist.md)
Everything in [chapter 7](07-restartability.md) depends on one dependency choice that is easy to
get wrong on Spring Boot 4.1, because getting it wrong does not produce an error &mdash; the
application starts, the job runs, nothing complains.
Spring Boot 4.1 split Batch autoconfiguration into separate modules rather than one monolith:
| Starter | Autoconfiguration class | What it provides |
|---|---|---|
| `spring-boot-starter-batch` | `BatchAutoConfiguration` | Batch infrastructure basics; a **resourceless** (in-memory, non-persistent) `JobRepository` if nothing more specific is configured |
| `spring-boot-starter-batch-jdbc` | `BatchJdbcAutoConfiguration` | A real JDBC-backed `JobRepository`, plus `spring.batch.jdbc.initialize-schema` to create the `BATCH_*` tables |
`spring-boot-starter-batch-jdbc` (this module's actual dependency, in
[`pom.xml`](../pom.xml)) pulls in `spring-boot-starter-batch` transitively, so **both**
autoconfiguration classes are normally on the classpath together, and `BatchJdbcAutoConfiguration`
wins by providing the more specific `JobRepository` bean. Nothing about compiling or starting the
application tells you which one actually won.
## Proving it, by turning the JDBC half off
`--spring.autoconfigure.exclude=...BatchJdbcAutoConfiguration` removes the JDBC-backed
`JobRepository` from the picture, leaving `BatchAutoConfiguration`'s resourceless one as the only
candidate. Same job, same identifying job parameters, same `PRODUCT` table, run twice, in two
separate JVMs:
```console
-- run 1 --
REPORT: 58 products now in the PRODUCT table
JOB FINISHED: status=COMPLETED exitCode=COMPLETED
-- run 2: a second, completely fresh JVM, same jar, same job parameters, same PRODUCT table --
org.springframework.dao.DuplicateKeyException: ... Unique index or primary key violation ...
VALUES ( /* 1 */ 'ABC-0001' )
JOB FINISHED: status=FAILED exitCode=FAILED
```
Full transcript: [`docs/output/11-resourceless-forgets-everything.txt`](output/11-resourceless-forgets-everything.txt).
Run 2 does not throw `JobInstanceAlreadyCompleteException`, the exception you would expect for
re-running an already-completed job (see [chapter 5](05-launching-and-jobparameters.md)). It does
not know there was a run 1 at all. The resourceless `JobRepository` lives only in that JVM's
heap; when the process exits, every `JobInstance`, `JobExecution`, and `StepExecution` it ever
recorded goes with it. Run 2 starts `productImportJob` as if for the first time, tries to insert
`ABC-0001` again, and collides with what run 1 already committed to the `PRODUCT` table &mdash;
a table that, unlike the job repository, *is* backed by the same file-based H2 database both
times.
This is the practical failure mode: **the job repository's persistence and your own application
data's persistence are two separate decisions**, and it is entirely possible to get the second
one right (a real database, a real table) while getting the first one wrong (resourceless,
because `-jdbc` was left off a dependency list or excluded by a profile without realizing what it
carried). The symptom is not a startup error. It is "my restart didn't restart" or "why did this
job double-insert everything," discovered in whatever environment happens to restart the JVM
between runs &mdash; which, for anything running in a container orchestrator, is any redeploy,
crash-restart, or scale-down/scale-up cycle.
<blockquote style="background:#f4f5f7;border:1px solid #e2e5ea;border-left:4px solid #b7bec9;border-radius:6px;padding:16px 20px;"><strong>How to check which one you actually have.</strong> Query <code>information_schema.tables</code> (or your database's equivalent) for a table named <code>BATCH_JOB_INSTANCE</code> after your application starts. If it is not there, you have the resourceless job repository, whatever your dependency list looks like on paper &mdash; check for an exclusion in <code>spring.autoconfigure.exclude</code>, a conflicting <code>JobRepository</code> bean of your own, or simply a missing <code>-jdbc</code>/<code>-mongodb</code> starter.</blockquote>
## Going deeper
- `BatchProperties` and `BatchJdbcProperties`, including `spring.batch.job.enabled`,
`spring.batch.jdbc.initialize-schema`, and `spring.batch.jdbc.table-prefix`: the
`spring-configuration-metadata.json` inside `spring-boot-batch-4.1.1.jar` and
`spring-boot-batch-jdbc-4.1.1.jar` is the authoritative list, more current than any blog post
including this one.
- MongoDB-backed job repositories follow the identical pattern via
`spring-boot-starter-batch-data-mongodb`, not covered by this module.
[Next: Production checklist &rarr;](11-production-checklist.md)