skip to content

Explain the item-level listeners (ItemReadListener, ItemProcessListener, ItemWriteListener) and their error callbacks.

level: seniorimportance: should knowfreq 42%

answer

  1. read/process per item, write per chunk
  2. before / after / onError x3
  3. onError observes, does NOT swallow
  4. processor null -> afterProcess(item,null) = filter, not error
  5. @BeforeRead/@AfterProcess/@OnWriteError forms

basics

~20 s

These three listeners hook the read, process, and write phases of each chunk. Each has a before, an after, and an onError callback — for example ItemReadListener has beforeRead, afterRead(item), and onReadError(exception) — so you can log, count, or inspect failures at each stage.

solid answer

~40 s

In a chunk-oriented step, items flow read -> process -> write. Each phase has a listener: ItemReadListener (beforeRead, afterRead(item), onReadError(Exception)), ItemProcessListener (beforeProcess(item), afterProcess(item,result), onProcessError(item,Exception)), and ItemWriteListener (beforeWrite(chunk), afterWrite(chunk), onWriteError(Exception,chunk)). Note the granularity difference: read and process fire per item, but write fires per chunk — beforeWrite/afterWrite/onWriteError receive the whole list. The onXxxError callbacks are observation hooks: they let you log or record the failure but they do not swallow it — the exception still propagates to Batch's skip/retry/rollback machinery. A processor returning null (filtering) triggers afterProcess with a null result, not an error. Annotation equivalents (@BeforeRead, @OnReadError, @AfterProcess, @OnWriteError, etc.) work the same and are registered via StepBuilder.listener.

code

java · 27 lines
java
public class AuditWriteListener implements ItemWriteListener<Order> {
    @Override
    public void beforeWrite(Chunk<? extends Order> items) {
        // whole chunk, not a single item
    }
    @Override
    public void afterWrite(Chunk<? extends Order> items) { }
    @Override
    public void onWriteError(Exception ex, Chunk<? extends Order> items) {
        // observe only: ex still propagates to skip/retry/rollback handling
        log.error("Write failed for {} items", items.size(), ex);
    }
}

// Annotation form for the read phase
public class ReadMetrics {
    @OnReadError
    public void onReadError(Exception e) { readErrors.increment(); }
}

// Registration
new StepBuilder("s", jobRepository)
    .<In, Order>chunk(50, tx)
    .reader(r).processor(p).writer(w)
    .listener(new AuditWriteListener())
    .listener(new ReadMetrics())
    .build();

go deeper

for a junior

Name the three phases and that each has before/after/onError callbacks.

for a middle

Know read/process are per-item while write is per-chunk, and that onError observes rather than handles.

for a senior

Explain null-filter vs error, chunk-level write callbacks, and that error callbacks don't imply skip.

for a principal

Discuss re-invocation on retry (item-by-item replay), choosing item listeners vs SkipListener/RetryListener for audit, and cost of per-item hooks at scale.

## The three phases A chunk-oriented step loops: read one item at a time until the chunk size is reached, process each item, then write the whole chunk in one call, inside a transaction. Each phase has a dedicated listener interface. ### ItemReadListener<T> - `beforeRead()` — before each `read()` call. - `afterRead(T item)` — after a successful read, with the item. - `onReadError(Exception ex)` — when `read()` throws. ### ItemProcessListener<T, S> - `beforeProcess(T item)` — before `process(item)`. - `afterProcess(T item, S result)` — after processing; `result` is `null` when the processor **filtered** the item (returned null). - `onProcessError(T item, Exception e)` — when processing throws. ### ItemWriteListener<S> - `beforeWrite(Chunk<? extends S> items)` — before the write, with the **whole chunk**. - `afterWrite(Chunk<? extends S> items)` — after a successful write. - `onWriteError(Exception exception, Chunk<? extends S> items)` — when the write throws, with the whole chunk. ## Granularity: per-item vs per-chunk This trips people up: **read and process are per-item, but write is per-chunk.** `beforeWrite`/`afterWrite`/`onWriteError` hand you a `Chunk` (a list). That's because `ItemWriter.write(Chunk)` gets the batch of items for one transaction. (Since Spring Batch 5 the write callbacks use `Chunk<S>`; pre-5 they used `List<? extends S>`.) ## onXxxError does NOT swallow the exception The error callbacks are **notification hooks**. They run when an exception is thrown, but returning normally from them does **not** suppress the exception — it still propagates into the framework's skip/retry/rollback handling. If you want to *tolerate* the error you configure `faultTolerant().skip(...)`/`retry(...)` (sibling topics) and use `SkipListener`/`RetryListener` for those specific outcomes; the item listeners are purely for observing. ## Filtering vs error A processor that returns `null` **filters** the item — it's removed from the chunk and never written. This is a normal path: `afterProcess(item, null)` fires; `onProcessError` does **not**. The step's `filterCount` increments. ## Ordering within a chunk For a chunk the framework interleaves: beforeRead/read/afterRead per item until the chunk is full, then beforeProcess/process/afterProcess per item, then a single beforeWrite/write/afterWrite. On an exception the matching onError fires and the chunk transaction typically rolls back. ## Annotation forms `@BeforeRead`, `@AfterRead`, `@OnReadError`, `@BeforeProcess`, `@AfterProcess`, `@OnProcessError`, `@BeforeWrite`, `@AfterWrite`, `@OnWriteError`. Put them on any bean, register the bean via `StepBuilder.listener(...)`; Spring's `StepListenerFactoryBean` wires them. ## When to use - Log or meter per-stage throughput and failures. - Capture the offending item/exception for a dead-letter or audit record (though for *skips* prefer `SkipListener`, which fires only on actually-skipped items). - Enrich diagnostics (e.g., record the item id that failed to write). ## Gotchas - Don't expect `onWriteError` to identify *which* item in the chunk failed — you get the whole chunk. On retry, Batch may re-run the chunk item-by-item, so write callbacks can fire multiple times for the same logical chunk. - Error callbacks firing is not the same as the item being skipped; that depends on skip policy. - Heavy work in per-item callbacks multiplies across every record — keep them cheap.

  • If onWriteError runs but returns normally, is the exception suppressed?
    No. Item error listeners only observe. The exception continues into the framework's rollback/skip/retry logic. To actually tolerate it you configure skip/retry policies; the listener can't cancel the failure by itself.
  • Why does beforeWrite receive a list/Chunk while beforeRead takes no item?
    Reading is one item at a time, so afterRead carries a single item and beforeRead has nothing yet. Writing is done once per chunk via ItemWriter.write(Chunk), so the write callbacks operate on the whole batch.
  • A processor returns null. Which callback fires?
    afterProcess(item, null) fires — the item is filtered (filterCount++), not written. onProcessError does not fire because returning null is a normal filter, not an exception.

saying these in an interview costs you the question

  • Thinking onWriteError swallows/handles the exception
  • Assuming write listeners get a single item rather than the whole chunk
  • Treating a null processor result as an error instead of a filter
  • Believing error callbacks mean the item was skipped

context