What is ExecutionContextPromotionListener and how do you use it to share data between steps?
answer
- StepExecutionListener.afterStep
- setKeys(...) copies step ctx -> job ctx
- default statuses = COMPLETED, setStatuses to widen
- consumer reads #{jobExecutionContext['key']}
- setStrict(true) throws on missing key
basics
~20 sIt's a step listener that copies chosen keys from a step's ExecutionContext up to the job's ExecutionContext after the step finishes. That's how a value produced in one step becomes readable by a later step.
solid answer
~40 sExecutionContextPromotionListener is a StepExecutionListener whose afterStep method promotes named keys from the step ExecutionContext into the job ExecutionContext. Because step contexts are private, this is the standard bridge for passing a value from step 1 to step 2: in step 1 you put the value in the step context, register the listener with setKeys({"myKey"}), and after the step the value is copied to the job context. Later steps read it via jobExecution.getExecutionContext() or late-binding like @Value("#{jobExecutionContext['myKey']}"). By default it only promotes on a COMPLETED exit status; you can widen that with setStatuses(...). If a listed key is missing in the step context it's simply skipped (unless strict mode is set). It's for small control values, not bulk data.
code
java · 24 lines@Bean
public ExecutionContextPromotionListener promotionListener() {
ExecutionContextPromotionListener l = new ExecutionContextPromotionListener();
l.setKeys(new String[] { "rowCount" }); // keys to lift into job context
// l.setStatuses(new String[] { "COMPLETED", "FAILED" }); // default is COMPLETED only
return l;
}
@Bean
public Step step1(JobRepository repo, PlatformTransactionManager tx,
ExecutionContextPromotionListener promotionListener) {
return new StepBuilder("step1", repo)
.<In, Out>chunk(100, tx)
.reader(reader()).processor(processor()).writer(writer())
.listener(promotionListener) // register the promotion listener
.build();
}
// Consumer bean in a later step reads from the JOB context:
@Bean
@StepScope
public MyWriter writer(@Value("#{jobExecutionContext['rowCount']}") Long rowCount) {
return new MyWriter(rowCount);
}go deeper
May not know the listener exists.
Knows it shares data across steps but may forget the COMPLETED-only default.
Should configure keys/statuses and wire producer/consumer correctly.
Should weigh it vs direct job-context writes, restart interplay, and serialization/size constraints.
## The problem it solves Step ExecutionContexts are **private to each step**, so a value computed in step 1 is invisible to step 2. The **job** ExecutionContext, however, is copied into every step that starts. `ExecutionContextPromotionListener` (`org.springframework.batch.core.listener.ExecutionContextPromotionListener`) bridges the two: after a step completes, it **copies selected keys from the step context up into the job context**. ## How it works It implements `StepExecutionListener`. In `afterStep(StepExecution)` it: 1. Reads the step ExecutionContext. 2. For each key you configured via `setKeys(String[])`, copies the value into the job ExecutionContext. 3. Only runs when the step's exit status matches its allowed `statuses` — **default `{ "COMPLETED" }`** — configurable via `setStatuses(String[])` (supports patterns like `"*"`). Keys not present in the step context are skipped silently, unless you call `setStrict(true)`, which makes a missing key throw at `afterStep`. It's an `InitializingBean` — `afterPropertiesSet` asserts that `keys` was set. ## Producer and consumer **Producer (step 1)** puts the value in the *step* context (e.g. from a `StepExecutionListener.afterStep` or inside a tasklet). **Consumer (step 2)** reads from the *job* context — directly, or with step-scoped late binding `@Value("#{jobExecutionContext['total']}")` on a `@StepScope` bean. ## Why not just write to the job context directly? You can call `stepExecution.getJobExecution().getExecutionContext().put(...)` yourself, but the job context is persisted on job-execution updates, and doing it via the promotion listener keeps the write tied to a **successful** step and to an explicit, reviewable key list. It also plays nicely with restart: on restart, promotion happens only when the (re-run) step reaches COMPLETED. ## Gotchas - **Default statuses = COMPLETED**: if your step ends `FAILED` or a custom status, nothing is promoted unless you widen `setStatuses`. - **Small values only**: the job context is size-limited and serialized; promote scalars/small maps, not large collections. - **Serializable**: promoted values must be serializable by the configured `ExecutionContextSerializer`. - **Restart interplay**: a value is only in the job context after the producing step actually completes in that execution; don't assume it survives if that step was skipped as already-COMPLETED without re-promoting (in practice it was promoted in the original run and persisted in the job context, so it's still there on restart).
- By default, on what step exit status does promotion occur?Only COMPLETED. To promote on other statuses (e.g. FAILED) you must call setStatuses(...) with those values.
- How does the consuming step actually read the promoted value?From the job ExecutionContext — directly via jobExecution.getExecutionContext() or with @StepScope late binding: @Value("#{jobExecutionContext['key']}").
saying these in an interview costs you the question
- Thinking the listener promotes on any status by default (it's COMPLETED only)
- Reading the promoted value from the step context instead of the job context
- Using promotion to move large datasets between steps