This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Batch Documentation 6.0.5!

Intercepting Step Execution

Just as with the Job, there are many events during the execution of a Step where a user may need to perform some functionality. For example, to write out to a flat file that requires a footer, the ItemWriter needs to be notified when the Step has been completed so that the footer can be written. This can be accomplished with one of many Step scoped listeners.

You can apply any class that implements one of the extensions of StepListener (but not that interface itself, since it is empty) to a step through the listeners element. The listeners element is valid inside a step, tasklet, or chunk declaration. We recommend that you declare the listeners at the level at which its function applies or, if it is multi-featured (such as StepExecutionListener and ItemReadListener), declare it at the most granular level where it applies.

  • Java

  • XML

The following example shows a listener applied at the chunk level in Java:

Java Configuration
@Bean
public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
	return new StepBuilder("step1", jobRepository)
				.<String, String>chunk(10).transactionManager(transactionManager)
				.reader(reader())
				.writer(writer())
				.listener(chunkListener())
				.build();
}

The following example shows a listener applied at the chunk level in XML:

XML Configuration
<step id="step1">
    <tasklet>
        <chunk reader="reader" writer="writer" commit-interval="10"/>
        <listeners>
            <listener ref="chunkListener"/>
        </listeners>
    </tasklet>
</step>

An ItemReader, ItemWriter, or ItemProcessor that itself implements one of the StepListener interfaces is registered automatically with the Step if using the namespace <step> element or one of the *StepFactoryBean factories. This only applies to components directly injected into the Step. If the listener is nested inside another component, you need to explicitly register it (as described previously under Registering ItemStream with a Step).

In addition to the StepListener interfaces, annotations are provided to address the same concerns. Plain old Java objects can have methods with these annotations that are then converted into the corresponding StepListener type. It is also common to annotate custom implementations of chunk components, such as ItemReader or ItemWriter or Tasklet. The annotations are analyzed by the XML parser for the <listener/> elements as well as registered with the listener methods in the builders, so all you need to do is use the XML namespace or builders to register the listeners with a step.

StepExecutionListener

StepExecutionListener represents the most generic listener for Step execution. It allows for notification before a Step is started and after it ends, whether it ended normally or failed, as the following example shows:

public interface StepExecutionListener extends StepListener {

    void beforeStep(StepExecution stepExecution);

    ExitStatus afterStep(StepExecution stepExecution);

}

afterStep has a return type of ExitStatus, to give listeners the chance to modify the exit code that is returned upon completion of a Step.

The annotations corresponding to this interface are:

  • @BeforeStep

  • @AfterStep

ChunkListener

A “chunk” is defined as the items processed within the scope of a transaction. Committing a transaction, at each commit interval, commits a chunk. You can use a ChunkListener to perform logic before a chunk begins processing or after a chunk has completed successfully or in failure, as the following interface definition shows:

public interface ChunkListener<I, O> extends StepListener {

    void beforeChunk(Chunk<I> chunk);
    void afterChunk(Chunk<O> chunk);
    void onChunkError(Exception exception, Chunk<O> chunk);

}

The beforeChunk method is called after the transaction is started after reading a chunk of items but before processing start. Conversely, afterChunk is called after the chunk is written but before the transaction is committed or rolled back.

onChunkError is never called with an empty chunk: a failure that happens before any item was processed, such as a read failure, has no processed chunk to report and is signalled by ItemReadListener#onReadError or ItemProcessListener#onProcessError instead. This means an implementation does not need to guard against an empty chunk.

The ChunkListener listener interface is not called in concurrent steps

When a fault-tolerant step scans a chunk after a write failure (see Skipping Write Failures and Chunk Scanning), each item is re-attempted in its own transaction, and the callbacks are invoked for the whole chunk as well as for each scanned item:

  • beforeChunk is called once for the chunk that was read, then once per scanned item, with a chunk holding that single input item.

  • afterChunk is called only for the scanned items that were successfully written.

  • onChunkError is called once for the whole chunk that failed to be written, then once per scanned item that turns out to be faulty, whether that item is skipped or fails the step.

For example, for a chunk of three items whose write fails because the second and third items are invalid, a listener is called as follows:

beforeChunk([1, 2, 3])   onChunkError(ex, [1, 2, 3])   (1)
beforeChunk([1])         afterChunk([1])               (2)
beforeChunk([2])         onChunkError(ex, [2])         (3)
beforeChunk([3])         onChunkError(ex, [3])
1 The initial chunk transaction, which is rolled back.
2 The first scan transaction, which commits.
3 A scan transaction for a faulty item, which is rolled back after the item is skipped.

As stated above, a ChunkListener must handle its own errors. An exception thrown from a listener callback terminates the step, including during a chunk scan: at that point the item being scanned can no longer be written or skipped, so the step is failed rather than completed with the item unaccounted for. The job can then be restarted.

The annotations corresponding to this interface are:

  • @BeforeChunk

  • @AfterChunk

  • @OnChunkError

A ChunkListener is not designed to throw checked exceptions. Errors must be handled in the implementation or the step will terminate.

ItemReadListener

When discussing skip logic previously, it was mentioned that it may be beneficial to log the skipped records so that they can be dealt with later. In the case of read errors, this can be done with an ItemReaderListener, as the following interface definition shows:

public interface ItemReadListener<T> extends StepListener {

    void beforeRead();
    void afterRead(T item);
    void onReadError(Exception ex);

}

The beforeRead method is called before each call to read on the ItemReader. The afterRead method is called after each successful call to read and is passed the item that was read. If there was an error while reading, the onReadError method is called. The exception encountered is provided so that it can be logged.

The annotations corresponding to this interface are:

  • @BeforeRead

  • @AfterRead

  • @OnReadError

ItemProcessListener

As with the ItemReadListener, the processing of an item can be “listened” to, as the following interface definition shows:

public interface ItemProcessListener<T, S> extends StepListener {

    void beforeProcess(T item);
    void afterProcess(T item, S result);
    void onProcessError(T item, Exception e);

}

The beforeProcess method is called before process on the ItemProcessor and is handed the item that is to be processed. The afterProcess method is called after the item has been successfully processed. If there was an error while processing, the onProcessError method is called. The exception encountered and the item that was attempted to be processed are provided, so that they can be logged.

The annotations corresponding to this interface are:

  • @BeforeProcess

  • @AfterProcess

  • @OnProcessError

ItemWriteListener

You can “listen” to the writing of an item with the ItemWriteListener, as the following interface definition shows:

public interface ItemWriteListener<S> extends StepListener {

    void beforeWrite(List<? extends S> items);
    void afterWrite(List<? extends S> items);
    void onWriteError(Exception exception, List<? extends S> items);

}

The beforeWrite method is called before write on the ItemWriter and is handed the list of items that is written. The afterWrite method is called after the items have been successfully written, but before committing the transaction associated with the chunk’s processing. If there was an error while writing, the onWriteError method is called. The exception encountered and the item that was attempted to be written are provided, so that they can be logged.

The annotations corresponding to this interface are:

  • @BeforeWrite

  • @AfterWrite

  • @OnWriteError

SkipListener

ItemReadListener, ItemProcessListener, and ItemWriteListener all provide mechanisms for being notified of errors, but none informs you that a record has actually been skipped. onWriteError, for example, is called even if an item is retried and successful. For this reason, there is a separate interface for tracking skipped items, as the following interface definition shows:

public interface SkipListener<T,S> extends StepListener {

    void onSkipInRead(Throwable t);
    void onSkipInProcess(T item, Throwable t);
    void onSkipInWrite(S item, Throwable t);

}

onSkipInRead is called whenever an item is skipped while reading. It should be noted that rollbacks may cause the same item to be registered as skipped more than once. onSkipInWrite is called when an item is skipped while writing. Because the item has been read successfully (and not skipped), it is also provided the item itself as an argument.

The annotations corresponding to this interface are:

  • @OnSkipInRead

  • @OnSkipInWrite

  • @OnSkipInProcess

SkipListeners and Transactions

One of the most common use cases for a SkipListener is to log out a skipped item, so that another batch process or even human process can be used to evaluate and fix the issue that leads to the skip. Because there are many cases in which the original transaction may be rolled back, Spring Batch makes two guarantees:

  • The appropriate skip method (depending on when the error happened) is called only once per item.

  • The SkipListener is always called just before the transaction is committed. This is to ensure that any transactional resources call by the listener are not rolled back by a failure within the ItemWriter.