BATCH-1056:

common-patterns.xml:
   *Replaced reference to RepeatOperationsStepFactoryBean with a valid configuration.
   *Rewrote "Adding a Footer Record" to discuss FlatFileFooterCallback

  job.xml: 
   *Fixed error in the way JobParameters are defined for the CommandLineJobLauncher.
This commit is contained in:
dhgarrette
2009-02-04 18:47:59 +00:00
parent a9d1ac3018
commit 9a112e2203
2 changed files with 114 additions and 54 deletions

View File

@@ -124,17 +124,13 @@
strategy which signals a complete batch when the item to be processed is
null. A more sophisticated completion policy could be implemented and
injected into the <classname>Step</classname> through the
<classname>RepeatOperationsStepFactoryBean</classname>:</para>
<classname>SimpleStepFactoryBean</classname>:</para>
<programlisting>&lt;bean id="simpleStep"
class="org.springframework.batch.core.step.item.RepeatOperationsStepFactoryBean" &gt;
class="org.springframework.batch.core.step.item.SimpleStepFactoryBean" &gt;
...
&lt;property name="chunkOperations"&gt;
&lt;bean class="org.springframework.batch.repeat.support.RepeatTemplate"&gt;
&lt;property name="completionPolicy"&gt;
&lt;bean class="org.example...SpecialCompletionPolicy"/&gt;
&lt;/property&gt;
&lt;/bean&gt;
&lt;property name="chunkCompletionPolicy"&gt;
&lt;bean class="org.example...SpecialCompletionPolicy"/&gt;
&lt;/property&gt;
&lt;/bean&gt;</programlisting>
@@ -156,11 +152,9 @@
}
public void afterRead(Object item) {
if (isPoisonPill(item)) {
stepExecution.setTerminateOnly(true);
}
}
}</programlisting>
@@ -175,59 +169,125 @@
<section>
<title>Adding a Footer Record</title>
<para>A very common requirement is to aggregate information during the
output process and to append a record at the end of a file summarizing the
data, or providing a checksum. This can also be achieved with a callback
in the step, normally as part of a custom
<classname>ItemWriter</classname>. In this case, since a job is
accumulating state that should not be lost if the job aborts, the
<classname>ItemStream</classname> interface should be implemented:</para>
<para>Often when writing to flat files, a "footer" record must be appended
to the end of the file, after all processing has be completed. This can
also be achieved using the <classname>FlatFileFooterCallback</classname>
interface provided by Spring Batch. The
<classname>FlatFileFooterCallback</classname> (and its counterpart, the
<classname>FlatFileHeaderCallback</classname>) are optional properties of
the <classname>FlatFileItemWriter</classname>:</para>
<programlisting>public class CustomItemWriter implements ItemWriter&lt;Trade&gt;,
ItemStream, StepListener
{
<programlisting> &lt;bean id="itemWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter"&gt;
&lt;property name="resource" ref="outputResource" /&gt;
&lt;property name="lineAggregator" ref="lineAggregator"/&gt;
<emphasis role="bold">&lt;property name="headerCallback" ref="headerCallback" /&gt;</emphasis>
<emphasis role="bold">&lt;property name="footerCallback" ref="footerCallback" /&gt;</emphasis>
&lt;/bean&gt;
</programlisting>
private static final String TOTAL_AMOUNT_KEY = "total.amount";
<para>The footer callback interface is very simple. It has just one method
that is called when the footer must be written:</para>
private ItemWriter delegate;
<programlisting> public interface FlatFileFooterCallback {
void writeFooter(Writer writer) throws IOException;
}</programlisting>
<section>
<title>Writing a Summary Footer</title>
<para>A very common requirement involving footer records is to aggregate
information during the output process and to append this information to
the end of the file. This footer serves as a summarization of the file
or provides a checksum.</para>
<para>For example, if a batch job is writing Trade records to a flat
file, and there is a requirement that the total amount from all the
Trades is placed in a footer, then the following ItemWriter
implementation can be used:</para>
<programlisting> public class TradeItemWriter implements ItemWriter&lt;Trade&gt;,
FlatFileFooterCallback {
private ItemWriter&lt;Trade&gt; delegate;
private double totalAmount = 0.0;
public void setDelegate(ItemWriter delegate) { ... }
public ExitStatus afterStep(StepExecution stepExecution) {
// Add the footer record here...
delegate.write("Total Amount Processed: " + totalAmount);
}
public void open(ExecutionContext executionContext) {
if (executionContext.containsKey(TOTAL_AMOUNT_KEY) {
totalAmount = executionContext.getDouble(TOTAL_AMOUNT_KEY);
public void write(List&lt;? extends Trade&gt; items) {
delegate.write(items);
for(Trade trade : items) {
totalAmount += trade.getAmount();
}
}
public void update(ExecutionContext executionContext) {
executionContext.setDouble(TOTAL_AMOUNT_KEY, totalAmount);
}
public void write(Trade item) {
delegate.write(item);
totalAmount += item.getAmount();
public void writeFooter(Writer writer) throws IOException {
writer.write("Total Amount Processed: " + totalAmount);
}
}</programlisting>
public void setDelegate(ItemWriter delegate) {...}
}</programlisting>
<para>The custom writer in the example is stateful (it maintains its total
in an instance variable <varname>totalAmount</varname>), but the state is
stored through the <classname>ItemStream</classname> interface in the
<classname>ExecutionContext</classname>. In this way we can be sure that
when the <code>open()</code> callback is received on a restart. The
framework garuntees we always get the last value that was committed. It
should be noted that it is not always necessary to implement
<classname>ItemStream</classname>. For example, if the
<classname>ItemWriter</classname> is re-runnable, in the sense that it
maintains its own state in a transactional resource like a database, there
is no need to maintain state within the writer itself.</para>
<para>This <classname>TradeItemWriter</classname> stores a
<code>totalAmount</code> value that is increased with the
<code>amount</code> from each <classname>Trade</classname> item written.
After the last <classname>Trade</classname> is processed, the framework
will call <methodname>writeFooter</methodname>, which will put that
<code>totalAmount</code> into the file. In order for the method to be
called, the <classname>TradeItemWriter</classname> must be wired into
the <classname>FlatFileItemWriter</classname> as the
<code>footerCallback</code>:</para>
<programlisting> &lt;bean id="tradeItemWriter" class="..TradeItemWriter"&gt;
&lt;property name="delegate" ref="flatFileItemWriter" /&gt;
&lt;/bean&gt;
&lt;bean id="flatFileItemWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter"&gt;
&lt;property name="resource" ref="outputResource" /&gt;
&lt;property name="lineAggregator" ref="lineAggregator"/&gt;
<emphasis role="bold">&lt;property name="footerCallback" ref="tradeItemWriter" /&gt;</emphasis>
&lt;/bean&gt;
</programlisting>
<para>The way that the <classname>TradeItemWriter</classname> has been
so far will only function correctly if the <classname>Step</classname>
is not restartable. This is because the class is stateful (since it
stores the <code>totalAmount</code>), but the <code>totalAmount</code>
is not persisted to the database, and therefore, it cannot be retrieved
in the event of a restart. In order to make this class restartable, the
<classname>ItemStream</classname> interface should be implemented along
with the methods <methodname>open</methodname> and
<methodname>update</methodname>: </para>
<programlisting> private static final String TOTAL_AMOUNT_KEY = "total.amount";
public void open(ExecutionContext executionContext) {
if (executionContext.containsKey(TOTAL_AMOUNT_KEY) {
totalAmount = executionContext.getDouble(TOTAL_AMOUNT_KEY);
}
}
public void update(ExecutionContext executionContext) {
executionContext.setDouble(TOTAL_AMOUNT_KEY, totalAmount);
}</programlisting>
<para>The <methodname>update</methodname> method will store the most
current version of <code>totalAmount</code> to the
<classname>ExecutionContext</classname> just before that object is
persisted to the database. The <methodname>open</methodname> method will
retrieve any existing <code>totalAmount</code> from the
<classname>ExecutionContext</classname> and use it as the starting point
for processing, allowing the <classname>TradeItemWriter</classname> to
pick up on restart where it left off the previous time the
<classname>Step</classname> was executed.</para>
<para>It should be noted that it is not always necessary to implement
<classname>ItemStream</classname>. For example, if the
<classname>ItemWriter</classname> is re-runnable, meaning that it
maintains its own state in a transactional resource like a database,
there is no need to maintain state within the writer itself.</para>
</section>
</section>
<section>

View File

@@ -494,9 +494,9 @@
linkend="domain" />. The first argument is 'endOfDayJob.xml', which is
the Spring <classname>ApplicationContext</classname> containing the
<classname>Job</classname>. The second argument, 'endOfDay' represents
the job name. The final argument, 'schedule.date=01-01-2008' will be
converted into <classname>JobParameters</classname>. An example of the
XML configuration is below:</para>
the job name. The final argument, 'schedule.date(date)=2008/01/01'
will be converted into <classname>JobParameters</classname>. An
example of the XML configuration is below:</para>
<programlisting> &lt;job id="endOfDay"&gt;
&lt;steps&gt;