diff --git a/src/site/docbook/reference/job.xml b/src/site/docbook/reference/job.xml
index ce93d5770..3a20276df 100644
--- a/src/site/docbook/reference/job.xml
+++ b/src/site/docbook/reference/job.xml
@@ -10,12 +10,12 @@
+ fileref="images/spring-batch-reference-model.png" scale="80" />
@@ -36,27 +36,21 @@
three required dependencies: a name, JobRepository,
and a list of Steps.
-
- <job id="footballJob">
+ <job id="footballJob">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s2" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
- </job>
-
-
+</job>The namespace defaults to referencing a repository with an id of
'jobRepository', which is a sensible default. However, this can be
overridden explicitly:
-
- <job id="footballJob" job-repository="specialRepository">
+ <job id="footballJob" job-repository="specialRepository">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s3" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
- </job>
-
-
+</job>Restartability
@@ -74,38 +68,30 @@
be run as part of a new JobInstance, then the
restartable property may be set to 'false':
-
- <job id="footballJob" restartable="false">
- <step id="playerload" parent="s1" next="gameLoad"/>
- <step id="gameLoad" parent="s2" next="playerSummarization"/>
- <step id="playerSummarization" parent="s3"/>
- </job>
-
-
+ <job id="footballJob" restartable="false">
+ ...
+</job>To phrase it another way, setting restartable to false means "this
Job does not support being started again". Restarting a Job that is not
restartable will cause a JobRestartException to
be thrown:
-
- Job job = new SimpleJob();
- job.setRestartable(false);
+ Job job = new SimpleJob();
+job.setRestartable(false);
- JobParameters jobParameters = new JobParameters();
+JobParameters jobParameters = new JobParameters();
- JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters);
- jobRepository.saveOrUpdate(firstExecution);
+JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters);
+jobRepository.saveOrUpdate(firstExecution);
- try {
+try {
jobRepository.createJobExecution(job, jobParameters);
fail();
- }
- catch (JobRestartException e) {
+}
+catch (JobRestartException e) {
// expected
- }
-
-
+}This snippet of JUnit code shows how attempting to create a
JobExecution the first time for a non restartable
@@ -122,49 +108,40 @@
SimpleJob allows for this by calling a
JobListener at the appropriate time:
-
- public interface JobExecutionListener {
+ public interface JobExecutionListener {
void beforeJob(JobExecution jobExecution);
void afterJob(JobExecution jobExecution);
- }
-
-
+}JobListeners can be added to a
SimpleJob via the listeners element on the
job:
-
- <job id="footballJob">
+ <job id="footballJob">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s2" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
- <listeners>
- <listener class="org.springframework.batch.sample.SampleListener"/>
+ <listeners>
+ <listener class="org.springframework.batch.sample.SampleListener"/>
</listeners>
- </job>
-
-
+</job>It should be noted that afterJob will be
called regardless of the success or failure of the
Job. If success or failure needs to be determined
it can be obtained from the JobExecution:
-
- void afterJob(JobExecution jobExecution){
+ public void afterJob(JobExecution jobExecution){
if( jobExecution.getStatus() == BatchStatus.COMPLETED ){
- //job success
+ //job success
}
else if(jobExecution.getStatus() == BatchStatus.FAILED){
- //job failure
+ //job failure
}
- }
-
-
+}The annotations corresponding to this interface are:
@@ -197,22 +174,19 @@
Job with two listeners and one
Step, "step1".
-
- <job id="baseJob" abstract="true">
+ <job id="baseJob" abstract="true">
<listeners>
- <listener class="com.ListenerOne"/>
+ <listener class="com.ListenerOne"/>
<listeners>
- </job>
+</job>
- <job id="job1" parent="baseJob3">
+<job id="job1" parent="baseJob3">
<step id="step1" parent="standaloneStep"/>
<listeners merge="true">
- <listener class="com.ListenerTwo"/>
+ <listener class="com.ListenerTwo"/>
<listeners>
- </job>
-
-
+</job>Please see the section on Inheriting from a Parent Step
@@ -262,15 +236,12 @@
collaborators. However, there are still a few configuration options
available:
-
- <job-repository id="jobRepository"
+ <job-repository id="jobRepository"
dataSource="dataSource"
transactionManager="transactionManager"
isolation-level-for-create="serializable"
table-prefix="BATCH_"
- />
-
-
+/>None of the configuration options listed above are required except
the id. If they are not set, the defaults shown above will be used. They
@@ -295,30 +266,24 @@
that the SERIALIZED will cause problems, as long as the database
platform supports it. However, this can be overridden:
-
- <job-repository id="jobRepository"
- isolation-level-for-create="ISOLATION_REPEATABLE_READ" />
-
-
+ <job-repository id="jobRepository"
+ isolation-level-for-create="ISOLATION_REPEATABLE_READ" />If the namespace or factory beans aren't used then it is also
essential to configure the transactional behavior of the repository
using AOP:
-
- <aop:config>
- <aop:advisor
- pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))"
- <advice-ref="txAdvice" />
- </aop:config>
+ <aop:config>
+ <aop:advisor
+ pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))"/>
+ <advice-ref="txAdvice" />
+</aop:config>
- <tx:advice id="txAdvice" transaction-manager="transactionManager">
- <tx:attributes>
- <tx:method name="*" />
- </tx:attributes>
- </tx:advice>
-
-
+<tx:advice id="txAdvice" transaction-manager="transactionManager">
+ <tx:attributes>
+ <tx:method name="*" />
+ </tx:attributes>
+</tx:advice>This fragment can be used as is, with almost no changes. Remember
also to include the appropriate namespace declarations and to make sure
@@ -338,12 +303,8 @@
meta data tables is needed within the same schema, then the table prefix
will need to be changed:
-
- <job-repository id="jobRepository"
- table-prefix="SYSTEM.TEST_"
- />
-
-
+ <job-repository id="jobRepository"
+ table-prefix="SYSTEM.TEST_" />Given the above changes, every query to the meta data tables will
be prefixed with "SYSTEM.TEST_". BATCH_JOB_EXECUTION will be referred to
@@ -363,13 +324,12 @@
objects at each commit point takes extra time. Another reason may be
that you just don't need to persist status for a particular job. For
this reason, Spring batch provides an in-memory Map version of the job
- respository:
+ repository:
-
-
-
-]]>
+ <bean id="jobRepository"
+ class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
+ <property name="transactionManager" ref="transactionManager"/>
+</bean>Note that the in-memory repository is volatile and so does not
allow restart between JVM instances. It also cannot guarantee that two
@@ -377,14 +337,11 @@
use the database version of the repository wherever you need quality of
service.
- However it does require a transaction manager to be
- defined because there are rollback semantics within the
- repository, and because the business logic might still be
- transactional (e.g. RDBMS access). For testing purposes many
- people find
- the ResourcelessTransactionManager
- useful.
-
+ However it does require a transaction manager to be defined
+ because there are rollback semantics within the repository, and because
+ the business logic might still be transactional (e.g. RDBMS access). For
+ testing purposes many people find the
+ ResourcelessTransactionManager useful.
@@ -397,12 +354,10 @@
shortcut and use it to set the database type to the closest
match:
-
-<bean id="jobRepository" class="org...JobRepositoryFactoryBean">
- <property name="databaseType" value="db2"/>
- <property name="dataSource" ref="dataSource"/>
-</bean>
-
+ <bean id="jobRepository" class="org...JobRepositoryFactoryBean">
+ <property name="databaseType" value="db2"/>
+ <property name="dataSource" ref="dataSource"/>
+</bean>(The JobRepositoryFactoryBean tries to
auto-detect the database type from the DataSource
@@ -428,10 +383,10 @@
a JobRepository, in order to obtain an
execution:
- <bean id="jobLauncher"
- class="org.springframework.batch.execution.launch.SimpleJobLauncher">
+ <bean id="jobLauncher"
+ class="org.springframework.batch.execution.launch.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
- </bean>
+</bean>Once a JobExecution is
@@ -479,13 +434,13 @@
configured to allow for this scenario by configuring a
TaskExecutor:
- <bean id="jobLauncher"
- class="org.springframework.batch.execution.launch.SimpleJobLauncher">
+ <bean id="jobLauncher"
+ class="org.springframework.batch.execution.launch.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
<property name="taskExecutor">
- <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
+ <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
</property>
- </bean>
+</bean>Any implementation of the spring TaskExecutor
interface can be used to control how jobs are asynchronously
@@ -594,16 +549,13 @@
will be converted into JobParameters. An
example of the XML configuration is below:
- <job id="endOfDay">
- <steps>
- <step id="step1" parent="simpleStep" />
- <!-- Step details removed for clarity -->
- </steps>
- </job>
+ <job id="endOfDay">
+ <step id="step1" parent="simpleStep" />
+</job>
- <!-- Launcher details removed for clarity -->
- <beans: bean id="jobLauncher"
- class="org.springframework.batch.core.launch.support.SimpleJobLauncher" />
+<!-- Launcher details removed for clarity -->
+<beans:bean id="jobLauncher"
+ class="org.springframework.batch.core.launch.support.SimpleJobLauncher" />This example is overly simplistic, since there are many more
requirements to a run a batch job in Spring Batch in general, but it
@@ -641,9 +593,10 @@
to a number using the ExitCodeMapper
interface:
- public interface ExitCodeMapper {
+ public interface ExitCodeMapper {
public int intValue(String exitCode);
+
}The essential contract of an
@@ -703,9 +656,8 @@
is required when handling an HttpRequest. An
example is below:
-
- @Controller
- public class JobLauncherController {
+ @Controller
+public class JobLauncherController {
@Autowired
JobLauncher jobLauncher;
@@ -715,11 +667,9 @@
@RequestMapping("/jobLauncher.html")
public void handle() throws Exception{
- jobLauncher.run(job, new JobParameters());
+ jobLauncher.run(job, new JobParameters());
}
- }
-
-
+}
@@ -776,8 +726,7 @@
query the repository for existing executions. This functionality is
provided by the JobExplorer interface:
-
- public interface JobExplorer {
+ public interface JobExplorer {
List<JobInstance> getJobInstances(String jobName, int start, int count);
@@ -790,9 +739,7 @@
List<JobExecution> getJobExecutions(JobInstance jobInstance);
Set<JobExecution> findRunningJobExecutions(String jobName);
- }
-
-
+}As is evident from the method signatures above,
JobExplorer is a read-only version of the
@@ -800,11 +747,8 @@
JobRepository, it can be easily configured via a
factory bean:
-
- <bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"
- p:dataSource-ref="dataSource" />
-
-
+ <bean id="jobExplorer" class="org.spr...JobExplorerFactoryBean"
+ p:dataSource-ref="dataSource" />Earlier in this
chapter, it was mentioned that the table prefix of the
@@ -813,11 +757,8 @@
JobExplorer is working with the same tables, it
too needs the ability to set a prefix:
-
- <bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"
- p:dataSource-ref="dataSource" p:tablePrefix="BATCH_" />
-
-
+ <bean id="jobExplorer" class="org.spr...JobExplorerFactoryBean"
+ p:dataSource-ref="dataSource" p:tablePrefix="BATCH_" />
@@ -832,39 +773,39 @@
provides for these types of operations via the
JobOperator interface:
-
- public interface JobOperator {
+ public interface JobOperator {
List<Long> getExecutions(long instanceId) throws NoSuchJobInstanceException;
- List<Long> getJobInstances(String jobName, int start, int count) throws NoSuchJobException;
+ List<Long> getJobInstances(String jobName, int start, int count)
+ throws NoSuchJobException;
Set<Long> getRunningExecutions(String jobName) throws NoSuchJobException;
String getParameters(long executionId) throws NoSuchJobExecutionException;
Long start(String jobName, String parameters)
- throws NoSuchJobException, JobInstanceAlreadyExistsException;
+ throws NoSuchJobException, JobInstanceAlreadyExistsException;
Long restart(long executionId)
- throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException,
+ throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException,
NoSuchJobException, JobRestartException;
Long startNextInstance(String jobName)
- throws NoSuchJobException, JobParametersNotFoundException, JobRestartException,
- JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException;
+ throws NoSuchJobException, JobParametersNotFoundException, JobRestartException,
+ JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException;
- boolean stop(long executionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException;
+ boolean stop(long executionId)
+ throws NoSuchJobExecutionException, JobExecutionNotRunningException;
String getSummary(long executionId) throws NoSuchJobExecutionException;
- Map<Long, String> getStepExecutionSummaries(long executionId) throws NoSuchJobExecutionException;
+ Map<Long, String> getStepExecutionSummaries(long executionId)
+ throws NoSuchJobExecutionException;
Set<String> getJobNames();
- }
-
-
+}The above operations represent methods from many different
interfaces, such as JobLauncher,
@@ -874,19 +815,16 @@
implementation of JobOperator,
SimpleJobOperator, has many dependencies:
-
- <bean id="jobOperator" class="org.springframework.batch.core.launch.support.SimpleJobOperator">
+ <bean id="jobOperator" class="org.spr...SimpleJobOperator">
<property name="jobExplorer">
- <bean class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean">
- <property name="dataSource" ref="dataSource" />
- </bean>
+ <bean class="org.spr...JobExplorerFactoryBean">
+ <property name="dataSource" ref="dataSource" />
+ </bean>
</property>
<property name="jobRepository" ref="jobRepository" />
<property name="jobRegistry" ref="jobRegistry" />
<property name="jobLauncher" ref="jobLauncher" />
- </bean>
-
-
+</bean>
@@ -911,14 +849,11 @@
Job to force the Job to a
new instance:
-
- public interface JobParametersIncrementer {
+ public interface JobParametersIncrementer {
JobParameters getNext(JobParameters parameters);
- }
-
-
+}The contract of JobParametersIncrementer is
that, given a Job,
as shown below:
-
- public class SampleIncrementer implements JobParametersIncrementer {
+ public class SampleIncrementer implements JobParametersIncrementer {
public JobParameters getNext(JobParameters parameters) {
- if (parameters==null || parameters.isEmpty()) {
- return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
- }
- long id = parameters.getLong("run.id",1L) + 1;
- return new JobParametersBuilder().addLong("run.id", id).toJobParameters();
+ if (parameters==null || parameters.isEmpty()) {
+ return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
+ }
+ long id = parameters.getLong("run.id",1L) + 1;
+ return new JobParametersBuilder().addLong("run.id", id).toJobParameters();
}
-}
-
-
+}In this example, the value with a key of 'run.id' is used to
discriminate between JobInstances. If the
@@ -957,14 +889,9 @@
be associated with Job via the 'incrementer'
attribute in the namespace:
-
- <job id="footballJob" incrementer="sampleIncrementer">
- <step id="playerload" parent="s1" next="gameLoad"/>
- <step id="gameLoad" parent="s2" next="playerSummarization"/>
- <step id="playerSummarization" parent="s3"/>
- </job>
-
-
+ <job id="footballJob" incrementer="sampleIncrementer">
+ ...
+</job>
@@ -974,11 +901,8 @@
JobOperator is gracefully stopping a
Job:
-
- Set<Long> executions = jobOperator.getRunningExecutions("sampleJob");
-
- jobOperator.stop(executions.iterator().next());
-
+ Set<Long> executions = jobOperator.getRunningExecutions("sampleJob");
+jobOperator.stop(executions.iterator().next()); The shutdown is not immediate, since there is no way to force
immediate shutdown, especially if the execution is currently in
diff --git a/src/site/docbook/reference/readersAndWriters.xml b/src/site/docbook/reference/readersAndWriters.xml
index 101026dd1..b87ee0fea 100644
--- a/src/site/docbook/reference/readersAndWriters.xml
+++ b/src/site/docbook/reference/readersAndWriters.xml
@@ -48,10 +48,9 @@
public interface ItemReader<T> {
- T read() throws Exception, UnexpectedInputException, ParseException;
+ T read() throws Exception, UnexpectedInputException, ParseException;
-}
-
+}
The read method defines the most essential
contract of the ItemReader; calling it returns one
@@ -87,10 +86,9 @@
public interface ItemWriter<T> {
- void write(List<? extends T> items) throws Exception;
+ void write(List<? extends T> items) throws Exception;
-}
-
+}
As with read on
ItemReader, write provides
@@ -117,23 +115,21 @@
that contains another ItemReader. For
example:
- public class CompositeItemWriter<T> implements ItemWriter<T> {
+ public class CompositeItemWriter<T> implements ItemWriter<T> {
ItemWriter<T> itemWriter;
public CompositeItemWriter(ItemWriter<T> itemWriter) {
- this.itemWriter = itemWriter;
+ this.itemWriter = itemWriter;
}
public void write(List<? extends T> items) throws Exception {
-
- //Add business logic here
-
- itemWriter.write(item);
+ //Add business logic here
+ itemWriter.write(item);
}
public void setDelegate(ItemWriter<T> itemWriter){
- this.itemWriter = itemWriter;
+ this.itemWriter = itemWriter;
}
}
@@ -149,9 +145,9 @@
For this scenario, Spring Batch provides the
ItemProcessor interface:
- public interface ItemProcessor<I, O> {
+ public interface ItemProcessor<I, O> {
- O process(I item) throws Exception;
+ O process(I item) throws Exception;
}An ItemProcessor is very simple; given one
@@ -164,28 +160,24 @@
written out. An ItemProcessor can be written that
performs the conversion:
- public class Foo {}
+ public class Foo {}
- public class Bar {
+public class Bar {
public Bar(Foo foo) {}
- }
+}
- public class FooProcessor implements ItemProcessor<Foo,Bar>{
-
- //Perform simple transformation, convert a Foo to a Bar
+public class FooProcessor implements ItemProcessor<Foo,Bar>{
public Bar process(Foo foo) throws Exception {
- return new Bar(foo);
+ //Perform simple transformation, convert a Foo to a Bar
+ return new Bar(foo);
}
- }
-
- public class BarWriter implements ItemWriter<Bar>{
+}
+public class BarWriter implements ItemWriter<Bar>{
public void write(List<? extends Bar> bars) throws Exception {
- //write bars
+ //write bars
}
-
- //rest of class ommitted for clarity
- }
+}In the very simple example above, there is a class
Foo, a class Bar, and a
@@ -199,16 +191,14 @@
provided. The FooProcessor can then be injected
into a Step:
-
- <job id="ioSampleJob">
+ <job id="ioSampleJob">
<step name="step1">
- <tasklet>
- <chunk reader="fooReader" processor="fooProcessor" writer="barWriter" commit-interval="2"/>
- </tasklet>
+ <tasklet>
+ <chunk reader="fooReader" processor="fooProcessor" writer="barWriter"
+ commit-interval="2"/>
+ </tasklet>
</step>
- </job>
-
-
+</job>Chaining ItemProcessors
@@ -221,73 +211,67 @@
transformed to Bar, which will be transformed to
Foobar and written out:
- public class Foo {}
+ public class Foo {}
- public class Bar {
+public class Bar {
public Bar(Foo foo) {}
- }
+}
- public class Foobar{
- public Foobar(Bar bar){}
- }
+public class Foobar{
+ public Foobar(Bar bar) {}
+}
- public class FooProcessor implements ItemProcessor<Foo,Bar>{
-
- //Perform simple transformation, convert a Foo to a Bar
+public class FooProcessor implements ItemProcessor<Foo,Bar>{
public Bar process(Foo foo) throws Exception {
- return new Bar(foo);
+ //Perform simple transformation, convert a Foo to a Bar
+ return new Bar(foo);
}
- }
-
- public class BarProcessor implements ItemProcessor<Bar,FooBar>{
+}
+public class BarProcessor implements ItemProcessor<Bar,FooBar>{
public FooBar process(Bar bar) throws Exception {
- return new Foobar(bar);
+ return new Foobar(bar);
}
- }
-
- public class FoobarWriter implements ItemWriter<FooBar>{
+}
+public class FoobarWriter implements ItemWriter<FooBar>{
public void write(List<? extends FooBar> items) throws Exception {
- //write items
+ //write items
}
-
- //rest of class ommitted for clarity
- }
+}
- A FooTransformer and
- BarTransformer can be 'chained' together to give
+ A FooProcessor and
+ BarProcessor can be 'chained' together to give
the resultant Foobar:
- CompositeItemProcessor<Foo,Foobar> compositeProcessor = new CompositeItemProcessor<Foo,Foobar>();
- List itemProcessors = new ArrayList();
- itemProcessors.add(new FooTransformer());
- itemProcessors.add(new BarTransformer());
- compositeProcessor.setItemProcessors(itemProcessors);
+ CompositeItemProcessor<Foo,Foobar> compositeProcessor =
+ new CompositeItemProcessor<Foo,Foobar>();
+List itemProcessors = new ArrayList();
+itemProcessors.add(new FooTransformer());
+itemProcessors.add(new BarTransformer());
+compositeProcessor.setItemProcessors(itemProcessors);Just as with the previous example, the composite processor can be
configured into the Step:
-
- <job id="ioSampleJob">
+ <job id="ioSampleJob">
<step name="step1">
- <tasklet>
- <chunk reader="fooReader" processor="compositeProcessor" writer="foobarWriter" commit-interval="2"/>
- </tasklet>
+ <tasklet>
+ <chunk reader="fooReader" processor="compositeProcessor" writer="foobarWriter"
+ commit-interval="2"/>
+ </tasklet>
</step>
- </job>
+</job>
- <bean id="compositeItemProcessor"
- class="org.springframework.batch.item.support.CompositeItemProcessor">
+<bean id="compositeItemProcessor"
+ class="org.springframework.batch.item.support.CompositeItemProcessor">
<property name="itemProcessors">
- <list>
- <bean class="..FooProcessor" />
- <bean class="..BarProcessor" />
- </list>
+ <list>
+ <bean class="..FooProcessor" />
+ <bean class="..BarProcessor" />
+ </list>
</property>
- </bean>
-
-
+</bean>
@@ -295,7 +279,7 @@
One typical use for an item processor is to filter out records
before they are passed to the ItemWriter. Filtering is an action
- distinct from skpping; skipping indicates that a record is invalid
+ distinct from skipping; skipping indicates that a record is invalid
whereas filtering simply indicates that a record should not be
written.
@@ -329,13 +313,12 @@
public interface ItemStream {
- void open(ExecutionContext executionContext) throws ItemStreamException;
+ void open(ExecutionContext executionContext) throws ItemStreamException;
- void update(ExecutionContext executionContext) throws ItemStreamException;
+ void update(ExecutionContext executionContext) throws ItemStreamException;
- void close() throws ItemStreamException;
-}
-
+ void close() throws ItemStreamException;
+}
Before describing each method, we should mention the
ExecutionContext. Clients of an
@@ -384,27 +367,24 @@
are not known to the Step, they need to be injected
as listeners or streams (or both if appropriate):
-
- <job id="ioSampleJob">
+ <job id="ioSampleJob">
<step name="step1">
- <tasklet>
- <chunk reader="fooReader" processor="fooProcessor" writer="compositeItemWriter"
- commit-interval="2">
- <streams>
- <stream ref="barWriter" />
- </streams>
- </chunk>
- </tasklet>
+ <tasklet>
+ <chunk reader="fooReader" processor="fooProcessor" writer="compositeItemWriter"
+ commit-interval="2">
+ <streams>
+ <stream ref="barWriter" />
+ </streams>
+ </chunk>
+ </tasklet>
</step>
- </job>
+</job>
- <bean id="compositeItemWriter" class="...CompositeItemWriter">
+<bean id="compositeItemWriter" class="...CompositeItemWriter">
<property name="delegate" ref="barWriter" />
- </bean>
+</bean>
- <bean id="barWriter" class="...BarWriter" />
-
-
+<bean id="barWriter" class="...BarWriter" />
@@ -436,14 +416,11 @@
fields so that the fields may be accessed either by index or name as
patterned after ResultSet:
-
- String[] tokens = new String[]{"foo", "1", "true"};
- FieldSet fs = new DefaultFieldSet(tokens);
- String name = fs.readString(0);
- int value = fs.readInt(1);
- boolean booleanValue = fs.readBoolean(2);
-
-
+ String[] tokens = new String[]{"foo", "1", "true"};
+FieldSet fs = new DefaultFieldSet(tokens);
+String name = fs.readString(0);
+int value = fs.readInt(1);
+boolean booleanValue = fs.readBoolean(2);There are many more options on the FieldSet
interface, such as Date, long,
@@ -473,9 +450,7 @@
Framework, Chapter 4.Resources. Therefore, this
guide will not go into the details of creating
Resource objects. However, a simple example of a
- file system resource can be found below:
- Resource resource = new FileSystemResource("resources/trades.csv");
-
+ file system resource can be found below: Resource resource = new FileSystemResource("resources/trades.csv");In complex batch environments the directory structures are often
managed by the EAI infrastructure where drop zones for external
@@ -594,13 +569,11 @@
level construct such as ResultSet and returns
an Object, flat file processing requires the
same construct to convert a String line into an
- Object:
- public interface LineMapper<T> {
+ Object:public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
- }
-
+}
The basic contract is that, given the current line and the line
number with which it is associated, the mapper should return a
@@ -626,14 +599,11 @@
FieldSet. In Spring Batch, this interface is
the LineTokenizer:
-
- public interface LineTokenizer {
+ public interface LineTokenizer {
FieldSet tokenize(String line);
- }
-
-
+}The contract of a LineTokenizer is such
that, given a line of input (in theory the
@@ -678,14 +648,11 @@
LineTokenizer to translate a line of data from
a resource into an object of the desired type:
-
- public interface FieldSetMapper<T> {
+ public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet);
- }
-
-
+}The pattern used is the same as the
RowMapper used by
@@ -728,27 +695,24 @@
DefaultLineMapper represents the behavior most
users will need:
-
- public class DefaultLineMapper<T> implements LineMapper<T>, InitializingBean {
+ public class DefaultLineMapper<T> implements LineMapper<T>, InitializingBean {
private LineTokenizer tokenizer;
private FieldSetMapper<T> fieldSetMapper;
public T mapLine(String line, int lineNumber) throws Exception {
- return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line));
+ return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line));
}
public void setLineTokenizer(LineTokenizer tokenizer) {
- this.tokenizer = tokenizer;
+ this.tokenizer = tokenizer;
}
public void setFieldSetMapper(FieldSetMapper<T> fieldSetMapper) {
- this.fieldSetMapper = fieldSetMapper;
+ this.fieldSetMapper = fieldSetMapper;
}
- }
-
-
+}The above functionality is provided in a default implementation,
rather than being built into the reader itself (as was done in
@@ -762,35 +726,33 @@
The following example will be used to illustrate this using an
actual domain scenario. This particular batch job reads in football
- players from the following file: ID,lastName,firstName,position,birthYear,debutYear
- "AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996",
- "AbduRa00,Abdullah,Rabih,rb,1975,1999",
- "AberWa00,Abercrombie,Walter,rb,1959,1982",
- "AbraDa00,Abramowicz,Danny,wr,1945,1967",
- "AdamBo00,Adams,Bob,te,1946,1969",
- "AdamCh00,Adams,Charlie,wr,1979,2003"
+ players from the following file:ID,lastName,firstName,position,birthYear,debutYear
+"AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996",
+"AbduRa00,Abdullah,Rabih,rb,1975,1999",
+"AberWa00,Abercrombie,Walter,rb,1959,1982",
+"AbraDa00,Abramowicz,Danny,wr,1945,1967",
+"AdamBo00,Adams,Bob,te,1946,1969",
+"AdamCh00,Adams,Charlie,wr,1979,2003" The contents of this file will be mapped to the following
- Player domain object:
- public class Player implements Serializable {
+ Player domain object: public class Player implements Serializable {
- private String ID;
- private String lastName;
- private String firstName;
- private String position;
- private int birthYear;
- private int debutYear;
+ private String ID;
+ private String lastName;
+ private String firstName;
+ private String position;
+ private int birthYear;
+ private int debutYear;
public String toString() {
-
- return "PLAYER:ID=" + ID + ",Last Name=" + lastName +
- ",First Name=" + firstName + ",Position=" + position +
- ",Birth Year=" + birthYear + ",DebutYear=" +
- debutYear;
+ return "PLAYER:ID=" + ID + ",Last Name=" + lastName +
+ ",First Name=" + firstName + ",Position=" + position +
+ ",Birth Year=" + birthYear + ",DebutYear=" +
+ debutYear;
}
// setters and getters...
- }
+}
In order to map a FieldSet into a
@@ -798,39 +760,34 @@
FieldSetMapper that returns players needs to be
defined:
-
- protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> {
+ protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> {
public Player mapFieldSet(FieldSet fieldSet) {
- Player player = new Player();
+ Player player = new Player();
- player.setID(fieldSet.readString(0));
- player.setLastName(fieldSet.readString(1));
- player.setFirstName(fieldSet.readString(2));
- player.setPosition(fieldSet.readString(3));
- player.setBirthYear(fieldSet.readInt(4));
- player.setDebutYear(fieldSet.readInt(5));
+ player.setID(fieldSet.readString(0));
+ player.setLastName(fieldSet.readString(1));
+ player.setFirstName(fieldSet.readString(2));
+ player.setPosition(fieldSet.readString(3));
+ player.setBirthYear(fieldSet.readInt(4));
+ player.setDebutYear(fieldSet.readInt(5));
- return player;
+ return player;
}
- }
-
+} The file can then be read by correctly constructing a
FlatFileItemReader and calling
read:
-
- FlatFileItemReader<Player> itemReader = new FlatFileItemReader<Player>();
- itemReader.setResource(new FileSystemResource("resources/players.csv"));
- //DelimitedLineTokenizer defaults to comma as its delimiter
- LineMapper<Player> lineMapper = new DefaultLineMapper<Player>();
- lineMapper.setLineTokenizer(new DelimitedLineTokenizer());
- lineMapper.setFieldSetMapper(new PlayerFieldSetMapper());
- itemReader.setLineMapper(lineMapper);
- itemReader.open(new ExecutionContext());
- Player player = itemReader.read();
-
-
+ FlatFileItemReader<Player> itemReader = new FlatFileItemReader<Player>();
+itemReader.setResource(new FileSystemResource("resources/players.csv"));
+//DelimitedLineTokenizer defaults to comma as its delimiter
+LineMapper<Player> lineMapper = new DefaultLineMapper<Player>();
+lineMapper.setLineTokenizer(new DelimitedLineTokenizer());
+lineMapper.setFieldSetMapper(new PlayerFieldSetMapper());
+itemReader.setLineMapper(lineMapper);
+itemReader.open(new ExecutionContext());
+Player player = itemReader.read();Each call to read will return a new
Player object from each line in the file. When the end of the file is
@@ -849,33 +806,29 @@
readability of the mapping function. First, the column names of all
fields in the flat file are injected into the tokenizer:
-
- tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"});
-
+ tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"}); a FieldSetMapper can this use this
information as follows:
-
- public class PlayerMapper implements FieldSetMapper<Player> {
- public Player mapFieldSet(FieldSet fs) {
+ public class PlayerMapper implements FieldSetMapper<Player> {
+ public Player mapFieldSet(FieldSet fs) {
- if(fs == null){
- return null;
- }
+ if(fs == null){
+ return null;
+ }
- Player player = new Player();
- player.setID(fs.readString("ID"));
- player.setLastName(fs.readString("lastName"));
- player.setFirstName(fs.readString("firstName"));
- player.setPosition(fs.readString("position"));
- player.setDebutYear(fs.readInt("debutYear"));
- player.setBirthYear(fs.readInt("birthYear"));
-
- return player;
- }
- }
-
+ Player player = new Player();
+ player.setID(fs.readString("ID"));
+ player.setLastName(fs.readString("lastName"));
+ player.setFirstName(fs.readString("firstName"));
+ player.setPosition(fs.readString("position"));
+ player.setDebutYear(fs.readInt("debutYear"));
+ player.setBirthYear(fs.readInt("birthYear"));
+
+ return player;
+ }
+}
@@ -891,17 +844,14 @@
BeanWrapperFieldSetMapper configuration looks
like the following:
-
- <bean id="fieldSetMapper"
- class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
+ <bean id="fieldSetMapper"
+ class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="player" />
- </bean>
+</bean>
- <bean id="player"
- class="org.springframework.batch.sample.domain.Player"
- scope="prototype" />
-
-
+<bean id="player"
+ class="org.springframework.batch.sample.domain.Player"
+ scope="prototype" />For each entry in the FieldSet, the
mapper will look for a corresponding setter on a new instance of the
@@ -921,11 +871,11 @@
organizations that use flat files use fixed length formats. An example
fixed length file is below:
- UK21341EAH4121131.11customer1
- UK21341EAH4221232.11customer2
- UK21341EAH4321333.11customer3
- UK21341EAH4421434.11customer4
- UK21341EAH4521535.11customer5
+ UK21341EAH4121131.11customer1
+UK21341EAH4221232.11customer2
+UK21341EAH4321333.11customer3
+UK21341EAH4421434.11customer4
+UK21341EAH4521535.11customer5While this looks like one large field, it actually represent 4
distinct fields:
@@ -955,14 +905,11 @@
FixedLengthLineTokenizer, each of these lengths
must be provided in the form of ranges:
-
- <bean id="fixedLengthLineTokenizer"
- class="org.springframework.batch.io.file.transform.FixedLengthTokenizer">
+ <bean id="fixedLengthLineTokenizer"
+ class="org.springframework.batch.io.file.transform.FixedLengthTokenizer">
<property name="names" value="ISIN, Quantity, Price, Customer" />
<property name="columns" value="1-12, 13-15, 16-20, 21-29" />
- </bean>
-
-
+</bean>Because the FixedLengthLineTokenizer uses
the same LineTokenizer interface as discussed
@@ -971,23 +918,15 @@
handling its output, such as using the
BeanWrapperFieldSetMapper.
- It should be noted that supporting the above ranges requires a
- specialized property editor be configured anywhere in the
- ApplicationContext:
-
-
- <bean id="customEditorConfigurer"
- class="org.springframework.beans.factory.config.CustomEditorConfigurer">
- <property name="customEditors">
- <map>
- <entry key="org.springframework.batch.item.file.transform.Range[]">
- <bean class="org.springframework.batch.item.file.transform.RangeArrayPropertyEditor" />
- </entry>
- </map>
- </property>
- </bean>
-
-
+
+ Supporting the above syntax for ranges requires that a
+ specialized property editor,
+ RangeArrayPropertyEditor, be configured in
+ the ApplicationContext. However, this bean
+ is automatically declared in an
+ ApplicationContext where the batch
+ namespace is used.
+
@@ -1000,12 +939,9 @@
need to be tokenized differently and mapped to different objects. The
following excerpt from a file illustrates this:
-
- USER;Smith;Peter;;T;20014539;F
- LINEA;1044391041ABC037.49G201XX1383.12H
- LINEB;2134776319DEF422.99M005LI
-
-
+ USER;Smith;Peter;;T;20014539;F
+LINEA;1044391041ABC037.49G201XX1383.12H
+LINEB;2134776319DEF422.99M005LIIn this file we have three types of records, "USER", "LINEA",
and "LINEB". A "USER" line corresponds to a User object. "LINEA" and
@@ -1016,31 +952,28 @@
individually, but we must specify different
LineTokenizer and
FieldSetMapper objects so that the
- ItemWriter will recieve the correct items. The
+ ItemWriter will receive the correct items. The
PatternMatchingCompositeLineMapper makes this
easy by allowing maps of patterns to
LineTokenizers and patterns to
FieldSetMappers to be configured:
-
- <bean id="orderFileLineMapper"
- class="org.springframework.batch.item.file.mapping.PatternMatchingCompositeLineMapper">
+ <bean id="orderFileLineMapper"
+ class="org.spr...PatternMatchingCompositeLineMapper">
<property name="tokenizers">
- <map>
- <entry key="USER*" value-ref="userTokenizer" />
- <entry key="LINEA*" value-ref="lineATokenizer" />
- <entry key="LINEB*" value-ref="lineBTokenizer" />
- </map>
+ <map>
+ <entry key="USER*" value-ref="userTokenizer" />
+ <entry key="LINEA*" value-ref="lineATokenizer" />
+ <entry key="LINEB*" value-ref="lineBTokenizer" />
+ </map>
</property>
<property name="fieldSetMappers">
- <map>
- <entry key="USER*" value-ref="userFieldSetMapper" />
- <entry key="LINE*" value-ref="lineFieldSetMapper" />
- </map>
+ <map>
+ <entry key="USER*" value-ref="userFieldSetMapper" />
+ <entry key="LINE*" value-ref="lineFieldSetMapper" />
+ </map>
</property>
- </bean>
-
-
+</bean>In this example, "LINEA" and "LINEB" have separate
LineTokenizers but they both use the same
@@ -1053,7 +986,7 @@
allows for two wildcard characters with special meaning: the question
mark ("?") will match exactly one character, while the asterisk ("*")
will match zero or more characters. Note that in the configuration
- above, all paterns end with an asterisk, making them effectively
+ above, all patterns end with an asterisk, making them effectively
prefixes to lines. The PatternMatcher will
always match the most specific pattern possible, regardless of the
order in the configuration. So if "LINE*" and "LINEA*" were both
@@ -1062,10 +995,7 @@
("*") can serve as a default by matching any line not matched by any
other pattern.
-
- <entry key="*" value-ref="defaultLineTokenizer" />
-
-
+ <entry key="*" value-ref="defaultLineTokenizer" />There is also a
PatternMatchingCompositeLineTokenizer that can
@@ -1084,9 +1014,9 @@
exceptions to be thrown. Many flat files are imperfect and contain
records that aren't formatted correctly. Many users choose to skip
these erroneous lines, logging out the issue, original line, and line
- number. These logs can later be inspected manually or or by another
- batch job. For this reason, Spring Batch provides a hierarchy of
- exceptions for handling parse exceptions:
+ number. These logs can later be inspected manually or by another batch
+ job. For this reason, Spring Batch provides a hierarchy of exceptions
+ for handling parse exceptions:
FlatFileParseException and
FlatFileFormatException.
FlatFileParseException is thrown by the
@@ -1110,18 +1040,15 @@
contains the number of tokens encountered, and the number
expected:
-
- tokenizer.setNames(new String[] {"A", "B", "C", "D"});
+ tokenizer.setNames(new String[] {"A", "B", "C", "D"});
- try{
+try{
tokenizer.tokenize("a,b,c");
- }
- catch(IncorrectTokenCountException e){
+}
+catch(IncorrectTokenCountException e){
assertEquals(4, e.getExpectedCount());
assertEquals(3, e.getActualCount());
- }
-
-
+}Because the tokenizer was configured with 4 column names, but
only 3 tokens were found in the file, an
@@ -1138,18 +1065,17 @@
line length doesn't add up to the widest value of this column, an
exception is thrown:
-
- tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10), new Range(11, 15) });
- try {
+ tokenizer.setColumns(new Range[] { new Range(1, 5),
+ new Range(6, 10),
+ new Range(11, 15) });
+try {
tokenizer.tokenize("12345");
fail("Expected IncorrectLineLengthException");
- }
- catch (IncorrectLineLengthException ex) {
+}
+catch (IncorrectLineLengthException ex) {
assertEquals(15, ex.getExpectedLength());
assertEquals(5, ex.getActualLength());
- }
-
-
+}The configured ranges for the tokenizer above are: 1-5, 6-10,
and 11-15, thus the total length of the line expected is 15.
@@ -1163,14 +1089,11 @@
For this reason, validation of line length can be turned off via the
'strict' property:
-
- tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) });
- tokenizer.setStrict(false);
- FieldSet tokens = tokenizer.tokenize("12345");
- assertEquals("12345", tokens.readString(0));
- assertEquals("", tokens.readString(1));
-
-
+ tokenizer.setColumns(new Range[] { new Range(1, 5), new Range(6, 10) });
+tokenizer.setStrict(false);
+FieldSet tokens = tokenizer.tokenize("12345");
+assertEquals("12345", tokens.readString(0));
+assertEquals("", tokens.readString(1));The above example is almost identical to the one before it,
except that tokenizer.setStrict(false) was called. This setting
@@ -1200,14 +1123,11 @@
In Spring Batch this is the
LineAggregator:
-
- public interface LineAggregator<T> {
+ public interface LineAggregator<T> {
public String aggregate(T item);
- }
-
-
+}The LineAggregator is the opposite of a
LineTokenizer.
@@ -1226,15 +1146,12 @@
simply assumes that the object is already a string, or that its
string representation is acceptable for writing:
-
- public class PassThroughLineAggregator<T> implements LineAggregator<T> {
+ public class PassThroughLineAggregator<T> implements LineAggregator<T> {
public String aggregate(T item) {
- return item.toString();
+ return item.toString();
}
- }
-
-
+}The above implementation is useful if direct control of
creating the string is required, but the advantages of a
@@ -1268,26 +1185,20 @@
FlatFileItemWriter expresses this in
code:
-
- public void write(T item) throws Exception {
+ public void write(T item) throws Exception {
write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
- }
-
-
+}A simple configuration would look like the following:
-
- <bean id="itemWriter"
- class="org.springframework.batch.io.file.FlatFileItemWriter">
+ <bean id="itemWriter"
+ class="org.springframework.batch.io.file.FlatFileItemWriter">
<property name="resource"
value="file:target/test-outputs/20070122.testStream.multilineStep.txt" />
<property name="lineAggregator">
- <bean class="org.springframework.batch.item.file.transform.PassThroughLineAggregator"/>
+ <bean class="org.spr...PassThroughLineAggregator"/>
</property>
- </bean>
-
-
+</bean>
@@ -1295,9 +1206,9 @@
The above example may be useful for the most basic uses of a
writing to a file. However, most users of the
- FlatFileItemWriter will will have a domain
- object that needs to be written out, and thus must be converted into a
- line. In file reading, the following was required:
+ FlatFileItemWriter will have a domain object
+ that needs to be written out, and thus must be converted into a line.
+ In file reading, the following was required:Read one line from the file.
@@ -1337,14 +1248,11 @@
FieldExtractor must be written to accomplish
the task of turning the item into an array:
-
- public interface FieldExtractor<T> {
+ public interface FieldExtractor<T> {
Object[] extract(T item);
- }
-
-
+}Implementations of the FieldExtractor
interface should create an array from the fields of the provided
@@ -1371,28 +1279,25 @@
BeanWrapperFieldExtractorAs with the BeanWrapperFieldSetMapper
- described in the file reading section, it is often preferrable to
+ described in the file reading section, it is often preferable to
configure how to convert a domain object to an object array, rather
than writing the conversion yourself. The
BeanWrapperFieldExtractor provides just this
type of functionality:
-
- BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<Name>();
- extractor.setNames(new String[] { "first", "last", "born" });
+ BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<Name>();
+extractor.setNames(new String[] { "first", "last", "born" });
- String first = "Alan";
- String last = "Turing";
- int born = 1912;
+String first = "Alan";
+String last = "Turing";
+int born = 1912;
- Name n = new Name(first, last, born);
- Object[] values = extractor.extract(n);
+Name n = new Name(first, last, born);
+Object[] values = extractor.extract(n);
- assertEquals(first, values[0]);
- assertEquals(last, values[1]);
- assertEquals(born, values[2]);
-
-
+assertEquals(first, values[0]);
+assertEquals(last, values[1]);
+assertEquals(born, values[2]);This extractor implementation has only one required property,
the names of the fields to map. Just as the
@@ -1415,38 +1320,32 @@
writes out a simple domain object that represents a credit to a
customer account:
-
- public class CustomerCredit {
+ public class CustomerCredit {
private int id;
private String name;
private BigDecimal credit;
//getters and setters removed for clarity
- }
-
-
+}Because a domain object is being used, an implementation of the
FieldExtractor interface must be provided, along with the delimiter to
use:
-
- <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
+ <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
- <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
- <property name="delimiter" value=","/>
- <property name="fieldExtractor">
- <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
- <property name="names" value="name,credit"/>
- </bean>
- </property>
- </bean>
+ <bean class="org.spr...DelimitedLineAggregator">
+ <property name="delimiter" value=","/>
+ <property name="fieldExtractor">
+ <bean class="org.spr...BeanWrapperFieldExtractor">
+ <property name="names" value="name,credit"/>
+ </bean>
+ </property>
+ </bean>
</property>
- </bean>
-
-
+</bean>In this case, the
BeanWrapperFieldExtractor described earlier in
@@ -1465,30 +1364,24 @@
Using the same CustomerCredit domain object
described above, it can be configured as follows:
-
- <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
+ <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
- <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator">
- <property name="fieldExtractor">
- <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
- <property name="names" value="name,credit" />
- </bean>
- </property>
- <property name="format" value="%-9s%-2.0f" />
- </bean>
+ <bean class="org.spr...FormatterLineAggregator">
+ <property name="fieldExtractor">
+ <bean class="org.spr...BeanWrapperFieldExtractor">
+ <property name="names" value="name,credit" />
+ </bean>
+ </property>
+ <property name="format" value="%-9s%-2.0f" />
+ </bean>
</property>
- </bean>
-
-
+</bean>Most of the above example should look familiar. However, the
value of the format property is new:
-
- <property name="format" value="%-9s%-2.0f" />
-
-
+ <property name="format" value="%-9s%-2.0f" />The underlying implementation is built using the same
Formatter added as part of Java 5. The Java
@@ -1554,7 +1447,7 @@
+ scale="70" />