How does late binding of job parameters into a Spring Batch bean work, and what SpEL expressions can you use?
answer
- @Value("#{jobParameters['x']}")
- Roots: jobParameters, stepExecutionContext, jobExecutionContext
- stepExecutionContext only under @StepScope
- Elvis ?: for defaults, null on missing key
- Typed parameters -> may need conversion
basics
~10 sYou inject runtime values with SpEL like @Value("#{jobParameters['x']}") on a bean annotated @StepScope or @JobScope. Spring resolves the expression when the step/job runs instead of at startup. Available roots include jobParameters, stepExecutionContext, and jobExecutionContext.
solid answer
~40 sLate binding means a bean's inputs are resolved at step/job execution time rather than at context-refresh time. You annotate the bean with @StepScope (or @JobScope) and use a SpEL expression in @Value, most commonly @Value("#{jobParameters['inputFile']}"). Because the bean is created per step/job run, Spring Batch exposes late-binding roots to the expression: jobParameters (the launch parameters), stepExecutionContext and jobExecutionContext (the mutable ExecutionContext maps), and also stepExecution/jobExecution. jobParameters values are typed (String/Long/Double/Date), so you may need conversion. Missing keys resolve to null unless you guard with SpEL like #{jobParameters['x'] ?: 'default'}. The whole mechanism depends on the scope annotation: the SpEL evaluates only when the scoped bean is instantiated, which happens once the JobExecution/StepExecution — and therefore the parameters and contexts — exist.
code
java · 14 lines@Bean
@StepScope
public StaxEventItemReader<Trade> reader(
@Value("#{jobParameters['inputFile']}") String file,
@Value("#{stepExecutionContext['partition']}") String partition) {
// 'file' comes from launch parameters (late binding),
// 'partition' from this step's ExecutionContext (e.g. set by a Partitioner)
return new StaxEventItemReaderBuilder<Trade>()
.name("tradeReader-" + partition)
.resource(new FileSystemResource(file))
.addFragmentRootElements("trade")
.unmarshaller(tradeMarshaller())
.build();
}go deeper
Recognize the @Value("#{jobParameters['x']}") syntax and that it needs @StepScope.
List the SpEL roots and handle defaults/nulls and typed parameters correctly.
Explain cross-step data passing via jobExecutionContext and ExecutionContextPromotionListener, plus partitioner-written stepExecutionContext keys.
Reason about type conversion pitfalls, evaluation timing, and how the synchronization managers expose the roots.
## Late binding defined **Late binding** is Spring Batch's term for injecting values that are only known at **runtime** (when a job/step actually executes) into bean definitions declared at **configuration time**. Normal singleton beans are wired at context startup; late binding pushes that wiring to step/job start so it can see launch-time data. ## The mechanism 1. Annotate the bean with `@StepScope` or `@JobScope` so it is created per run. 2. Use a **SpEL** (`#{...}`) expression inside `@Value` (or the XML/builder equivalent). 3. Spring Batch registers the current execution context via `StepSynchronizationManager` / `JobSynchronizationManager`, exposing special **root objects** to the expression evaluator. ## Available SpEL roots - `#{jobParameters['key']}` — the immutable `JobParameters` supplied at launch. Values are typed (`STRING`, `LONG`, `DOUBLE`, `DATE`). - `#{stepExecutionContext['key']}` — the step's `ExecutionContext` (per-step mutable map). **Only** available under `@StepScope`. - `#{jobExecutionContext['key']}` — the job's `ExecutionContext`, shared across steps. - `#{stepExecution}` and `#{jobExecution}` — the execution objects themselves (e.g. `#{stepExecution.jobExecution.startTime}`). ## Examples ```java @Bean @StepScope public JdbcCursorItemReader<Sale> reader( @Value("#{jobParameters['runDate']}") LocalDate runDate) { return new JdbcCursorItemReaderBuilder<Sale>() .name("salesReader") .dataSource(dataSource) .sql("SELECT * FROM sales WHERE sale_date = ?") .preparedStatementSetter(ps -> ps.setObject(1, runDate)) .rowMapper(new SaleRowMapper()) .build(); } ``` Defaulting and Elvis operator: ```java @Value("#{jobParameters['chunk'] ?: 100}") int chunk ``` ## Gotchas & edge cases - **Type mismatch:** `jobParameters` are stored with an explicit type. If you pass a date as a String parameter but inject into a `LocalDate`, you need a converter or should inject a `String` and parse it. - **Missing key → null:** an absent parameter yields `null`; primitives will NPE on unboxing. Use the Elvis operator `?:` or `Optional`-style handling. - **`stepExecutionContext` under `@JobScope`:** not available — the job scope has no single step context; use `@StepScope` when you need step-level data (common in partitioning, where the partitioner writes keys into each partition's `stepExecutionContext`). - **Passing data between steps:** a common pattern is to write a value into `jobExecutionContext` (or promote a `stepExecutionContext` key via `ExecutionContextPromotionListener`) in one step, then late-bind `#{jobExecutionContext['key']}` in a later step. - **No scope annotation:** the expression evaluates at startup → failure. The scope annotation is mandatory for late binding. ## When to use Any time a reader/writer/processor's configuration depends on launch parameters or on values computed by an earlier step/partitioner.
- How would you provide a default value if a job parameter is missing?Use the SpEL Elvis operator: @Value("#{jobParameters['chunk'] ?: 100}"). If the key is absent (null), it falls back to 100. Otherwise a missing key resolves to null and can NPE on primitive unboxing.
- How do you pass a value computed in step 1 to a reader in step 2?Write it into the jobExecutionContext (or promote a stepExecutionContext key via ExecutionContextPromotionListener), then late-bind it in step 2 with @Value("#{jobExecutionContext['key']}") on a @StepScope bean.
saying these in an interview costs you the question
- Claiming stepExecutionContext is readable in @JobScope beans
- Assuming a missing job parameter throws instead of resolving to null
- Thinking any @Value SpEL is 'late bound' even without a scope annotation