As discussed in Batch Domain Language, a
Step is a domain object that encapsulates an
independent, sequential phase of a batch job and contains all of the
information necessary to define and control the actual batch processing.
This is a necessarily vague description because the contents of any given
Step are at the discretion of the developer writing a
Job. A Step can be as simple or complex as the
developer desires. A simple Step might load data from
a file into the database, requiring little or no code. (depending upon the
implementations used) A more complex Step may have
complicated business rules that are applied as part of the
processing.

Spring Batch uses a 'Chunk Oriented' processing style within its
most common implementation. Chunk oriented processing refers to reading
the data one at a time, and creating 'chunks' that will be written out,
within a transaction boundary. One item is read in from an
ItemReader, handed to an
ItemProcessor, and aggregated. Once the number of
items read equals the commit interval, the entire chunk is written out via
the ItemWriter, and then the transaction is committed.

Below is a code representation of the same concepts shown above:
List items = new Arraylist();
for(int i = 0; i < commitInterval; i++){
Object item = itemReader.read()
Object processedItem = itemProcessor.process(item);
items.add(processedItem);
}
itemWriter.write(items);Despite the relatively short list of required dependencies for a
Step, it is an extremely complex class that can
potentially contain many collaborators. In order to ease configuration,
the Spring Batch namespace can be used:
<job id="sampleJob" job-repository="jobRepository">
<step id="step1">
<tasklet transaction-manager="transactionManager">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
</job>The configuration above represents the only required dependencies to create a item-oriented step:
reader - The ItemReader that provides
items for processing.
writer - The ItemWriter that
processes the items provided by the
ItemReader.
transaction-manager - Spring's
PlatformTransactionManager that will be
used to begin and commit transactions during processing.
job-repository - The JobRepository
that will be used to periodically store the
StepExecution and
ExecutionContext during processing (just
before committing). For an in-line <step/> (one defined
within a <job/>) it is an attribute on the <job/>
element; for a standalone step, it is defined as an attribute of
the <tasklet/>.
commit-interval - The number of items that will be processed before the transaction is committed.
It should be noted that, job-repository defaults to
"jobRepository" and transaction-manager defaults to "transactionManger".
Furthermore, the ItemProcessor is optional, not
required, since the item could be directly passed from the reader to the
writer.
If a group of Steps share similar
configurations, then it may be helpful to define a "parent"
Step from which the concrete
Steps may inherit properties. Similar to class
inheritance in Java, the "child" Step will
combine its elements and attributes with the parent's. The child will
also override any of the parent's Steps.
In the following example, the Step
"concreteStep1" will inherit from "parentStep". It will be instantiated
with 'itemReader', 'itemProcessor', 'itemWriter', startLimit=5, and
allowStartIfComplete=true. Additionally, the commitInterval will be '5'
since it is overridden by the "concreteStep1":
<step id="parentStep">
<tasklet allow-start-if-complete="true">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
<step id="concreteStep1" parent="parentStep">
<tasklet start-limit="5">
<chunk processor="itemProcessor" commit-interval="5"/>
</tasklet>
</step>The id attribute is still required on the step within the job element. This is for two reasons:
The id will be used as the step name when persisting the StepExecution. If the same standalone step is referenced in more than one step in the job, an error will occur.
When creating job flows, as described later in this chapter, the next attribute should be referring to the step in the flow, not the standalone step.
Sometimes it may be necessary to define a parent
Step that is not a complete
Step configuration. If, for instance, the
reader, writer, and tasklet attributes are left off of a
Step configuration, then initialization will
fail. If a parent must be defined without these properties, then the
"abstract" attribute should be used. An "abstract"
Step will not be instantiated; it is used only
for extending.
In the following example, the Step
"abstractParentStep" would not instantiate if it were not declared to
be abstract. The Step "concreteStep2" will have
'itemReader', 'itemWriter', and commitInterval=10.
<step id="abstractParentStep" abstract="true">
<tasklet>
<chunk commit-interval="10"/>
</tasklet>
</step>
<step id="concreteStep2" parent="abstractParentStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter"/>
</tasklet>
</step>Some of the configurable elements on
Steps are lists; the <listeners/>
element, for instance. If both the parent and child
Steps declare a <listeners/> element,
then the child's list will override the parent's. In order to allow a
child to add additional listeners to the list defined by the parent,
every list element has a "merge" attribute. If the element specifies
that merge="true", then the child's list will be combined with the
parent's instead of overriding it.
In the following example, the Step
"concreteStep3" will be created will two listeners:
listenerOne and
listenerTwo:
<step id="listenersParentStep" abstract="true">
<listeners>
<listener ref="listenerOne"/>
<listeners>
</step>
<step id="concreteStep3" parent="listenersParentStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
</tasklet>
<listeners merge="true">
<listener ref="listenerTwo"/>
<listeners>
</step>As mentioned above, a step reads in and writes out items,
periodically committing using the supplied
PlatformTransactionManager. With a
commit-interval of 1, it will commit after writing each individual item.
This is less than ideal in many situations, since beginning and
committing a transaction is expensive. Ideally, it is preferable to
process as many items as possible in each transaction, which is
completely dependent upon the type of data being processed and the
resources with which the step is interacting. For this reason, the
number of items that are processed within a commit can be
configured.
<job id="sampleJob">
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
</job>In the example above, 10 items will be processed within each
transaction. At the beginning of processing a transaction is begun, and
each time read is called on the
ItemReader, a counter is incremented. When it
reaches 10, the list of aggregated items is passed to the
ItemWriter, and the transaction will be
committed.
In Chapter 4, Configuring and Running a Job, restarting a
Job was discussed. Restart has numerous impacts
on steps, and as such may require some specific configuration.
There are many scenarios where you may want to control the
number of times a Step may be started. For
example, a particular Step might need to be
configured so that it only runs once because it invalidates some
resource that must be fixed manually before it can be run again. This
is configurable on the step level, since different steps may have
different requirements. A Step that may only be
executed once can exist as part of the same Job
as a Step that can be run infinitely. Below is
an example start limit configuration:
<step id="step1">
<tasklet start-limit="1">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>The simple step above can be run only once. Attempting to run it
again will cause an exception to be thrown. It should be noted that
the default value for the start-limit is
Integer.MAX_VALUE.
In the case of a restartable job, there may be one or more steps
that should always be run, regardless of whether or not they were
successful the first time. An example might be a validation step, or a
Step that cleans up resources before
processing. During normal processing of a restarted job, any step with
a status of 'COMPLETED', meaning it has already been completed
successfully, will be skipped. Setting allow-start-if-complete to
"true" overrides this so that the step will always run:
<step id="step1">
<tasklet allow-start-if-complete="true">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step><job id="footballJob" restartable="true">
<step id="playerload" next="gameLoad">
<tasklet>
<chunk reader="playerFileItemReader" writer="playerWriter"
commit-interval="10" />
</tasklet>
</step>
<step id="gameLoad" next="playerSummarization">
<tasklet allow-start-if-complete="true">
<chunk reader="gameFileItemReader" writer="gameWriter"
commit-interval="10"/>
</tasklet>
</step>
<step id="playerSummarization">
<tasklet start-limit="3">
<chunk reader="playerSummarizationSource" writer="summaryWriter"
commit-interval="10"/>
</tasklet>
</step>
</job>The above example configuration is for a job that loads in
information about football games and summarizes them. It contains
three steps: playerLoad, gameLoad, and playerSummarization. The
playerLoad Step loads player information from a
flat file, while the gameLoad Step does the
same for games. The final Step,
playerSummarization, then summarizes the statistics for each player
based upon the provided games. It is assumed that the file loaded by
'playerLoad' must be loaded only once, but that 'gameLoad' will load
any games found within a particular directory, deleting them after
they have been successfully loaded into the database. As a result, the
playerLoad Step contains no additional
configuration. It can be started almost limitlessly, and if complete
will be skipped. The 'gameLoad' Step, however,
needs to be run every time in case extra files have been dropped since
it last executed. It has 'allow-start-if-complete' set to 'true' in
order to always be started. (It is assumed that the database tables
games are loaded into has a process indicator on it, to ensure new
games can be properly found by the summarization step). The
summarization Step, which is the most important
in the Job, is configured to have a start limit
of 3. This is useful because if the step continually fails, a new exit
code will be returned to the operators that control job execution, and
it won't be allowed to start again until manual intervention has taken
place.
![]() | Note |
|---|---|
This job is purely for example purposes and is not the same as the footballJob found in the samples project. |
Run 1:
playerLoad is executed and completes successfully, adding 400 players to the 'PLAYERS' table.
gameLoad is executed and processes 11 files worth of game data, loading their contents into the 'GAMES' table.
playerSummarization begins processing and fails after 5 minutes.
Run 2:
playerLoad is not run, since it has already completed successfully, and allow-start-if-complete is 'false' (the default).
gameLoad is executed again and processes another 2 files, loading their contents into the 'GAMES' table as well (with a process indicator indicating they have yet to be processed)
playerSummarization begins processing of all remaining game data (filtering using the process indicator) and fails again after 30 minutes.
Run 3:
playerLoad is not run, since it has already completed successfully, and allow-start-if-complete is 'false' (the default).
gameLoad is executed again and processes another 2 files, loading their contents into the 'GAMES' table as well (with a process indicator indicating they have yet to be processed)
playerSummarization is not start, and the job is immediately
killed, since this is the third execution of playerSummarization,
and its limit is only 2. The limit must either be raised, or the
Job must be executed as a new
JobInstance.
There are many scenarios where errors encountered while processing
should not result in Step failure, but should be
skipped instead. This is usually a decision that must be made by someone
who understands the data itself and what meaning it has. Financial data,
for example, may not be skippable because it results in money being
transferred, which needs to be completely accurate. Loading a list of
vendors, on the other hand, might allow for skips. If a vendor is not
loaded because it was formatted incorrectly or was missing necessary
information, then there probably won't be issues. Usually these bad
records are logged as well, which will be covered later when discussing
listeners.
<step id="step1">
<tasklet>
<chunk reader="flatFileItemReader" writer="itemWriter"
commit-interval="10" skip-limit="10">
<skippable-exception-classes>
<include class="org.springframework.batch.item.file.FlatFileParseException"/>
</skippable-exception-classes>
</chunk>
</tasklet>
</step>In this example, a FlatFileItemReader is
used, and if at any point a
FlatFileParseException is thrown, it will be
skipped and counted against the total skip limit of 10. Separate counts
are made of skips on read, process and write inside the step execution,
and the limit applies across all. Once the skip limit is reached, the
next exception found will cause the step to fail.
One problem with the example above is that any other exception
besides a FlatFileParseException will cause the
Job to fail. In certain scenarios this may be the
correct behavior. However, in other scenarios it may be easier to
identify which exceptions should cause failure and skip everything
else:
<step id="step1">
<tasklet>
<chunk reader="flatFileItemReader" writer="itemWriter"
commit-interval="10" skip-limit="10">
<skippable-exception-classes>
<include class="java.lang.Exception"/>
<exclude class="java.io.FileNotFoundException"/>
</skippable-exception-classes>
</chunk>
</tasklet>
</step>By 'including' java.lang.Exception as a
skippable exception class, the configuration indicates that all
Exceptions are skippable. However, by 'excluding'
java.io.FileNotFoundException, the configuration
refines the list of skippable exception classes to be all
Exceptions except
FileNotFoundException. Any excluded exception
classes will be fatal if encountered (i.e. not skipped).
For any exception encountered, the skippability will be determined
by the nearest superclass in the class hierarchy. Any unclassifed
exception will be treated as 'fatal'. The order of the
<include/> and <exclude/> elements
does not matter.
In most cases you want an exception to cause either a skip or
Step failure. However, not all exceptions are
deterministic. If a FlatFileParseException is
encountered while reading, it will always be thrown for that record;
resetting the ItemReader will not help. However,
for other exceptions, such as a
DeadlockLoserDataAccessException, which indicates
that the current process has attempted to update a record that another
process holds a lock on, waiting and trying again might result in
success. In this case, retry should be configured:
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter"
commit-interval="2" retry-limit="3">
<retryable-exception-classes>
<include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
</retryable-exception-classes>
</chunk>
</tasklet>
</step>The Step allows a limit for the number of
times an individual item can be retried, and a list of exceptions that
are 'retryable'. More details on how retry works can be found in Chapter 9, Retry.
By default, regardless of retry or skip, any exceptions thrown
from the ItemWriter will cause the transaction
controlled by the Step to rollback. If skip is
configured as described above, exceptions thrown from the
ItemReader will not cause a rollback. However,
there are many scenarios in which exceptions thrown from the
ItemWriter should not cause a rollback because no
action has taken place to invalidate the transaction. For this reason,
the Step can be configured with a list of
exceptions that should not cause rollback.
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
<no-rollback-exception-classes>
<include class="org.springframework.batch.item.validator.ValidationException"/>
</no-rollback-exception-classes>
</tasklet>
</step>The basic contract of the ItemReader is
that it is forward only. The step buffers reader input, so that in the
case of a rollback the items don't need to be re-read from the reader.
However, there are certain scenarios in which the reader is built on
top of a transactional resource, such as a JMS queue. In this case,
since the queue is tied to the transaction that is rolled back, the
messages that have been pulled from the queue will be put back on. For
this reason, the step can be configured to not buffer the
items:
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2"
is-reader-transactional-queue="true"/>
</tasklet>
</step>Transaction attributes can be used to control the isolation, propagation, and timeout settings. More information on setting transaction attributes can be found in the spring core documentation.
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
<transaction-attributes isolation="DEFAULT"
propagation="REQUIRED"
timeout="30"/>
</tasklet>
</step>The step has to take care of ItemStream
callbacks at the necessary points in its lifecycle. (for more
information on the ItemStream interface, please
refer to the section called “ItemStream”) This is vital if a step fails,
and might need to be restarted, because the
ItemStream interface is where the step gets the
information it needs about persistent state between executions.
If the ItemReader,
ItemProcessor, or
ItemWriter itself implements the
ItemStream interface, then these will be
registered automatically. Any other streams need to be registered
separately. This is often the case where there are indirect dependencies
such as delegates being injected into the reader and writer. A stream
can be registered on the Step through the
'streams' element, as illustrated below:
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="compositeWriter" commit-interval="2">
<streams>
<stream ref="fileItemWriter1"/>
<stream ref="fileItemWriter2"/>
</streams>
</chunk>
</tasklet>
</step>
<beans:bean id="compositeWriter"
class="org.springframework.batch.item.support.CompositeItemWriter">
<beans:property name="delegates">
<beans:list>
<beans:ref bean="fileItemWriter1" />
<beans:ref bean="fileItemWriter2" />
</beans:list>
</beans:property>
</beans:bean>In the example above, the
CompositeItemWriter is not an
ItemStream, but both of its delegates are.
Therefore, both delegate writers must be explicitly registered as
streams in order for the framework to handle them correctly. The
ItemReader does not need to be explicitly
registered as a stream because it is a direct property of the
Step. The step will now be restartable and the
state of the reader and writer will be correctly persisted in the event
of a failure.
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, in order 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
written. This can be accomplished with one of many
Step scoped listeners.
Any class that implements one of the extensions
of StepListener (but not that interface
itself since it is empty) can be applied to a step via the
listeners element. The listeners element is valid inside a
step, tasklet or chunk declaration. It is recommended that you
declare the listeners at the level which its function applies,
or if it is multi-featured
(e.g. StepExecutionListener
and ItemReadListener) then declare it at
the most granular level that it applies (chunk in the example
given).
<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 will be registered
automatically with the Step if using the
namespace <step> element, or one of the the
*StepFactoryBean factories. This only applies to
components directly injected into the Step: if
the listener is nested inside another component, it needs to be
explicitly registered (as described above).
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 like
ItemReader or ItemWriter
or Tasklet. The annotations are analysed by the
XML parser for the <listener/> elements, so all you
need to do is use the XML namespace to register the listeners with a
step.
StepExecutionListener represents the most
generic listener for Step execution. It allows
for notification before a Step is started and
after it has ends, whether it ended normally or failed:
public interface StepExecutionListener extends StepListener {
void beforeStep(StepExecution stepExecution);
ExitStatus afterStep(StepExecution stepExecution);
}ExitStatus is the return type of
afterStep in order to allow 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
A chunk is defined as the items processed within the scope of a
transaction. Committing a transaction, at each commit interval,
commits a 'chunk'. A ChunkListener can be
useful to perform logic before a chunk begins processing or after a
chunk has completed successfully:
public interface ChunkListener extends StepListener {
void beforeChunk();
void afterChunk();
}The beforeChunk method is called after
the transaction is started, but before read
is called on the ItemReader. Conversely,
afterChunk is called after the chunk has been
committed (and not at all if there is a rollback).
The annotations corresponding to this interface are:
@BeforeChunk
@AfterChunk
A ChunkListener can be applied
when there is no chunk declaration: it is
the TaskletStep that is responsible for
calling the ChunkListener so it applies
to a non-item-oriented tasklet as well (called before and
after the tasklet).
When discussing skip logic above, it was mentioned that it may
be beneficial to log the skipped records, so that they can be deal
with later. In the case of read errors, this can be done with an
ItemReaderListener:
public interface ItemReadListener<T> extends StepListener {
void beforeRead();
void afterRead(T item);
void onReadError(Exception ex);
}The beforeRead method will be called
before each call to read on the
ItemReader. The
afterRead method will be called after each
successful call to read, and will be passed
the item that was read. If there was an error while reading, the
onReadError method will be called. The
exception encountered will be provided so that it can be
logged.
The annotations corresponding to this interface are:
@BeforeRead
@AfterRead
@OnReadError
Just as with the ItemReadListener, the
processing of an item can be 'listened' to:
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 will be called
before process on the
ItemProcessor, and is handed the item that will
be processed. The afterProcess method will be
called after the item has been successfully processed. If there was an
error while processing, the onProcessError
method will be called. The exception encountered and the item that was
attempted to be processed will be provided, so that they can be
logged.
The annotations corresponding to this interface are:
@BeforeProcess
@AfterProcess
@OnProcessError
The writing of an item can be 'listened' to with the
ItemWriteListener:
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 will be called
before write on the
ItemWriter, and is handed the item that will be
written. The afterWrite method will be called
after the item has been successfully written. If there was an error
while writing, the onWriteError method will
be called. The exception encountered and the item that was attempted
to be written will be provided, so that they can be logged.
The annotations corresponding to this interface are:
@BeforeWrite
@AfterWrite
@OnWriteError
ItemReadListener,
ItemProcessListener, and
ItemWriteListner all provide mechanisms for
being notified of errors, but none will inform you that a record has
actually been skipped. onWriteError, for
example, will be called even if an item is retried and successful. For
this reason, there is a separate interface for tracking skipped
items:
public interface SkipListener<T,S> extends StepListener {
void onSkipInRead(Throwable t);
void onSkipInProcess(T item, Throwable t);
void onSkipInWrite(S item, Throwable t);
}onSkipInRead will be 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 will be 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
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 leading 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) will only be called once per item.
The SkipListener will always be
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.