diff --git a/docs/src/site/docbook/reference/core.xml b/docs/src/site/docbook/reference/core.xml index 4f84173ce..297f0f780 100644 --- a/docs/src/site/docbook/reference/core.xml +++ b/docs/src/site/docbook/reference/core.xml @@ -963,11 +963,13 @@ public JobExecution createJobExecution(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException, JobRestartException; - void saveOrUpdate(JobExecution jobExecution); + void add(StepExecution stepExecution); - void saveOrUpdate(StepExecution stepExecution); + void update(JobExecution jobExecution); - void saveOrUpdateExecutionContext(StepExecution stepExecution); + void update(StepExecution stepExecution); + + void updateExecutionContext(StepExecution stepExecution); StepExecution getLastStepExecution(JobInstance jobInstance, Step step); diff --git a/docs/src/site/docbook/reference/readersAndWriters.xml b/docs/src/site/docbook/reference/readersAndWriters.xml index 44c61c650..d8befc122 100644 --- a/docs/src/site/docbook/reference/readersAndWriters.xml +++ b/docs/src/site/docbook/reference/readersAndWriters.xml @@ -50,15 +50,15 @@ ItemReader is a basic interface for generic input operations: - public interface ItemReader<T> { - Object read() throws Exception; + T read() throws Exception, UnexpectedInputException, NoWorkFoundException, ParseException; void mark() throws MarkFailedException; void reset() throws ResetFailedException; } -]]> + The read method defines the most essential contract of the ItemReader, calling it returns one @@ -97,15 +97,15 @@ As with ItemReader, ItemWriter is a fairly generic interface: - public interface ItemWriter<T> { - void write(Object item) throws Exception; + void write(T item) throws Exception; void flush() throws FlushFailedException; void clear() throws ClearFailedException; } -]]> + As with read on ItemReader, write provides @@ -135,7 +135,7 @@ readers and writers need to be opened, closed, and require a mechanism for persisting state: - public interface ItemStream { void open(ExecutionContext executionContext) throws StreamException; @@ -143,7 +143,7 @@ void close(ExecutionContext executionContext) throws StreamException; } -]]> + Before describing each method, its worth briefly mentioning the ExecutionContext. Clients of an @@ -203,11 +203,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);]]> + boolean booleanValue = fs.readBoolean(2); There are many more options on the FieldSet interface, such as Date, long, @@ -240,9 +240,9 @@ 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"); - ]]> + In complex batch environments the directory structures are often managed by the EAI infrastructure where drop zones for external @@ -336,11 +336,11 @@ LineTokenizer to translate a line of data from a resource into an object of the desired type: - public interface FieldSetMapper { public Object mapLine(FieldSet fs); - }]]> + } The pattern used is the same as RowMapper used by JdbcTemplate. @@ -356,11 +356,11 @@ FieldSet is necessary. In Spring Batch, this is called a LineTokenizer: - public interface LineTokenizer { FieldSet tokenize(String line); - }]]> + } The contract of a LineTokenizer is such that, given a line of input (in theory the @@ -420,14 +420,14 @@ In code, the above flow looks like the following: - String line = readLine(); if (line != null) { FieldSet tokenizedLine = tokenizer.tokenize(line); return fieldSetMapper.mapLine(tokenizedLine); } - return null;]]> + return null; Exception handling has been removed for clarity. @@ -435,16 +435,16 @@ 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" ]]> + "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 { private String ID; @@ -464,14 +464,14 @@ // setters and getters... } - ]]> + In order to map a FieldSet into a Player object, a FieldSetMapper that returns players needs to be defined: - + protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> { public Object mapLine(FieldSet fieldSet) { Player player = new Player(); @@ -484,22 +484,22 @@ 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 it's delimiter itemReader.setLineTokenizer(new DelimitedLineTokenizer()); itemReader.setFieldSetMapper(new PlayerFieldSetMapper()); itemReader.open(new ExecutionContext()); - Player player = (Player)itemReader.read(); + 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 @@ -517,15 +517,15 @@ fields in the flat file are injected into the LineTokenizer: - tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"}); - ]]> + a FieldSetMapper can this use this information as follows: - public class PlayerMapper implements FieldSetMapper { + public class PlayerMapper implements FieldSetMapper<Player> { public Object mapLine(FieldSet fs) { if(fs == null){ @@ -559,14 +559,14 @@ FieldSetMapper configuration looks like the following: - - - + <bean id="fieldSetMapper" + class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> + <property name="prototypeBeanName" value="player" /> + </bean> - ]]> + scope="prototype" /> For each entry in the FieldSet, the mapper will look for a corresponding setter on a new instance of the @@ -586,11 +586,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]]> + UK21341EAH4521535.11customer5 While this looks like one large field, it actually represent 4 distinct fields: @@ -620,14 +620,14 @@ FixedLengthLineTokenizer, each of these lengths must be provided in the form of ranges: - - - - + + <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> -]]> + This LineTokenizer will return the same FieldSet as if a dlimiter had been used, @@ -639,18 +639,18 @@ 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> -]]> +
@@ -662,7 +662,7 @@ might have records spanning multiple lines with multiple formats. The following excerpt from a file illustrates this: - HEA;0013100345;2007-02-15 NCU;Smith;Peter;;T;20014539;F BAD;;Oak Street 31/A;;Small Town;00235;IL;US SAD;Smith, Elizabeth;Elm Street 17;;Some City;30011;FL;United States @@ -670,30 +670,30 @@ LIT;1044391041;37.49;0;0;4.99;2.99;1;45.47 LIT;2134776319;221.99;5;0;7.99;2.99;1;221.87 SIN;UPS;EXP;DELIVER ONLY ON WEEKDAYS - FOT;2;2;267.34]]> + FOT;2;2;267.34 Everything between the line starting with 'HEA' and the line starting with 'FOT' is considered one record. The PrefixMatchingCompositeLineTokenizer makes this easier by matching the prefix in a line with a particular tokenizer: - - - - - - - - - - - - - - - - ]]> + <bean id="orderFileDescriptor" + class="org.springframework.batch.io.file.transform.PrefixMatchingCompositeLineTokenizer"> + <property name="tokenizers"> + <map> + <entry key="HEA" value-ref="headerRecordDescriptor" /> + <entry key="FOT" value-ref="footerRecordDescriptor" /> + <entry key="BCU" value-ref="businessCustomerLineDescriptor" /> + <entry key="NCU" value-ref="customerLineDescriptor" /> + <entry key="BAD" value-ref="billingAddressLineDescriptor" /> + <entry key="SAD" value-ref="shippingAddressLineDescriptor" /> + <entry key="BIN" value-ref="billingLineDescriptor" /> + <entry key="SIN" value-ref="shippingLineDescriptor" /> + <entry key="LIT" value-ref="itemLineDescriptor" /> + <entry key="" value-ref="defaultLineDescriptor" /> + </map> + </property> + </bean> This ensures that the line will be parsed correctly, which is especially important for fixed length input. Any users of the @@ -716,70 +716,40 @@ LineAggregator Just as the LineTokenizer interface is - necessary to take a string and split it into tokens, file writing must + necessary to take an item and turn it into a string, file writing must have a way to aggregate multiple fields into a single string for writing to a file. In Spring Batch this is the LineAggregator: - public interface LineAggregator<T> { - public String aggregate(FieldSet fieldSet); + public String aggregate(T item); - }]]> + } The LineAggregator is the opposite of a LineTokenizer. LineTokenizer takes a String and returns a FieldSet, whereas - LineAggregator takes a - FieldSet and returns a + LineAggregator takes an + item and returns a String. As with reading there are two types: DelimitedLineAggregator and FixedLengthLineAggregator.
-
- FieldSetCreator - - Because the LineAggregator interface uses a - FieldSet as it's mechanism for converting to a - string, there needs to be an interface that describes how to convert - from an object into a FieldSet: - - - - As with LineTokenizer and - LineAggregator, - FieldSetCreator is the polar opposite of - FieldSetMapper. - FieldSetMapper takes a - FieldSet and returns a mapped object, whereas a - FieldSetCreator takes an Object and returns a - FieldSet. -
-
Simple Delimited File Writing Example - Now that both the LineAggregator and - FieldSetCreator interfaces have been defined, - the basic flow of writing can be explained: + Now that the LineAggregator interface has + been defined, the basic flow of writing can be explained: The object to be written is passed to the - FieldSetCreator in order to obtain a - FieldSet. - - - - The returned FieldSet is passed to - the LineAggregator + LineAggregator in order to obtain a + String. @@ -792,22 +762,21 @@ FlatFileItemWriter expresses this in code: - + public void write(T item) throws Exception { + getOutputState().write(lineAggregator.aggregate(item) + LINE_SEPARATOR); + } A simple configuration with the smallest ammount of setters would look like the following: - - - - - - ]]> + <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"/> + </property> + </bean>
@@ -911,28 +880,28 @@ stream. First, lets examine a set of XML records that the StaxEventItemReader can process. - - - - XYZ0001 - 5 - 11.39 - Customer1 - - - XYZ0002 - 2 - 72.99 - Customer2c - - - XYZ0003 - 9 - 99.99 - Customer3 - -]]> + +<?xml version="1.0" encoding="UTF-8"?> +<records> + <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> + <isin>XYZ0001</isin> + <quantity>5</quantity> + <price>11.39</price> + <customer>Customer1</customer> + </trade> + <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> + <isin>XYZ0002</isin> + <quantity>2</quantity> + <price>72.99</price> + <customer>Customer2c</customer> + </trade> + <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain"> + <isin>XYZ0003</isin> + <quantity>9</quantity> + <price>99.99</price> + <customer>Customer3</customer> + </trade> +</records> To be able to process the XML records the following is needed: @@ -954,22 +923,22 @@ - - - - - - - - - - - - - - - - ]]> + <property name="itemReader"> + <bean class="org.springframework.batch.io.xml.StaxEventItemReader"> + <property name="fragmentRootElementName" value="trade" /> + <property name="resource" value="data/staxJob/input/20070918.testStream.xmlFileStep.xml" /> + <property name="fragmentDeserializer"> + <bean class="org.springframework.batch.io.xml.oxm.UnmarshallingEventReaderDeserializer"> + <constructor-arg> + <bean class="org.springframework.oxm.xstream.XStreamMarshaller"> + <property name="aliases" ref="aliases" /> + </bean> + </constructor-arg> + </bean> + </property> + </bean> +</property> + Notice that in this example we have chosen to use an XStreamMarshaller that requires an alias passed @@ -980,16 +949,16 @@ the map. In the configuration file we can use a spring configuration utility to describe the required alias as follows: - - - - - - - - ]]> + + <util:map id="aliases"> + <entry key="trade" + value="org.springframework.batch.sample.domain.Trade" /> + <entry key="isin" value="java.lang.String" /> + <entry key="quantity" value="long" /> + <entry key="price" value="java.math.BigDecimal" /> + <entry key="customer" value="java.lang.String" /> + </util:map> + On input the reader reads the XML resource until it recognizes a new fragment is about to start (by matching the tag name by default). @@ -1003,7 +972,7 @@ injection provided by the spring configuration would look something like the following: - StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader() Resource resource = new ByteArrayResource(xmlResource.getBytes()) @@ -1031,7 +1000,7 @@ } } -]]> +
@@ -1049,13 +1018,13 @@ MarshallingEventWriterSerializer. The Spring configuration for this setup looks as follows: - - - - - - -]]> + <bean class="org.springframework.batch.item.xml.StaxEventItemWriter" id="tradeStaxWriter"> + <property name="resource"value="file:target/test-outputs/20070918.testStream.xmlFileStep.output.xml" /> + <property name="serializer" ref="tradeMarshallingSerializer" /> + <property name="rootTagName" value="trades" /> + <property name="overwriteOutput" value="true" /> +</bean> + The configuration sets up the three required properties and optionally sets the overwriteOutput=true, mentioned earlier in the @@ -1075,7 +1044,7 @@ all of the points discussed, demonstrating the programmatic setup of the required properties. - StaxEventItemWriter staxItemWriter = new StaxEventItemWriter() FileSystemResource resource = new FileSystemResource(File.createTempFile("StaxEventWriterOutputSourceTests", "xml")) Map aliases = new HashMap(); @@ -1103,7 +1072,7 @@ trade.customer = "Customer1" println trade staxItemWriter.write(trade) - staxItemWriter.flush()]]> + staxItemWriter.flush() For a complete example configuration of XML input and output and a corresponding Job see the sample xmlStaxJob. @@ -1120,11 +1089,11 @@ java.io.File. Both XML and Flat File resources can be configured using standard Spring constructs: - - - ]]> + <bean id="flatFileItemReader" + class="org.springframework.batch.item.file.FlatFileItemReader"> + <property name="resource" + value="file://outputs/20070122.testStream.CustomerReportStep.TEMP.txt" /> + </bean> The above Resource will load the file from the file system, at the location specificied. Note that absolute locations @@ -1134,10 +1103,10 @@ determined at runtime as a parameter to the job. This could be solved using '-D' parameters, i.e. a system property: - - -]]> + <bean id="flatFileItemReader" + class="org.springframework.batch.item.file.FlatFileItemReader"> + <property name="resource" value="${input.file.name}" /> +</bean> All that would be required for this solution to work would be a system argument (-Dinput.file.name="file://file.txt"). (Note that although @@ -1154,10 +1123,10 @@ either job name, step name, or any values from the JobParameters, by surrounding them with %: - - - ]]> + <bean id="inputFile" + class="org.springframework.batch.core.resource.StepExecutionResourceProxy" /> + <property name="filePattern" value="//%JOB_NAME%/%STEP_NAME%/%file.name%" /> + </bean> Assuming a job name of 'fooJob', and a step name of 'fooStep', and the key-value pair of 'file.name="fileName.txt"' is in the @@ -1168,11 +1137,11 @@ StepExecution, it must be registered as a StepListener: - <bean id="fooStep" parent="abstractStep" p:itemReader-ref="itemReader" - p:itemWriter-ref="itemWriter"> - - ]]> + p:itemWriter-ref="itemWriter"> + <property name="listeners" ref="inputFile" /> + </bean> The StepListener interface will be discussed in more detail in Chapter 4. For now, it is sufficient to know that the @@ -1188,20 +1157,20 @@ for both XML and FlatFile processing. Consider the following files in a directory: - + file-1.txt file-2.txt ignored.txt file-1.txt and file-2.txt are formatted the same and for business reasons should be processed together. The MuliResourceItemReader can be used to read in both files by using wildcards: - - - - + + <bean id="multiResourceReader" class="org.springframework.batch.item.SortedMultiResourceItemReader"> + <property name="resources" value="classpath:data/multiResourceJob/input/file-*.txt" /> + <property name="delegate" ref="flatFileItemReader" /> + </bean> -]]> + The referenced delegate is a simple FlatFileItemReader. The above configuration will @@ -1288,18 +1257,18 @@ DataSource. The following database schema will be used as an example: - CREATE TABLE CUSTOMER ( ID BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(45), CREDIT FLOAT -);]]> +); Many people prefer to use a domain object for each row, so we'll use an implementation of the RowMapper interface to map a CustomerCredit object: - public class CustomerCreditRowMapper implements RowMapper { public static final String ID_COLUMN = "id"; public static final String NAME_COLUMN = "name"; @@ -1315,7 +1284,7 @@ return customerCredit; } -}]]> +} Because JdbcTemplate is so familiar to users of Spring, and the JdbcCursorItemReader @@ -1326,12 +1295,12 @@ CUSTOMER database. The first example will be using JdbcTemplate: - //For simplicity sake, assume a dataSource has already been obtained JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); List customerCredits = jdbcTemplate.query("SELECT ID, NAME, CREDIT from CUSTOMER", new CustomerCreditRowMapper()); -]]> + After running this code snippet the customerCredits list will contain 1,000 CustomerCredit objects. In the @@ -1342,7 +1311,7 @@ constrast this with the approach of the JdbcCursorItemReader: - JdbcCursorItemReader itemReader = new JdbcCursorItemReader(); itemReader.setDataSource(dataSource); itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER"); @@ -1357,7 +1326,7 @@ } itemReader.close(executionContext); -]]> + After running this code snippet the counter will equal 1,000. If the code above had put the returned customerCredit into a list, the @@ -1473,7 +1442,7 @@ configuration using the same 'customer credit' example as the JDBC reader: - HibernateCursorItemReader itemReader = new HibernateCursorItemReader(); itemReader.setQueryString("from CustomerCredit"); //For simplicity sake, assume sessionFactory already obtained. @@ -1489,7 +1458,7 @@ } itemReader.close(executionContext); -]]> + This configured ItemReader will return CustomerCredit objects in the exact same manner @@ -1534,13 +1503,13 @@ is an example configuration using the same 'customer credit' example as the JDBC reader above: - - - - - -]]> + <bean id="itemReader" + class="org.springframework.batch.item.database.JpaPagingItemReader"> + <property name="entityManagerFactory" ref="entityManagerFactory"/> + <property name="queryString" value="select c from CustomerCredit c"/> + <property name="pageSize" value="1000"/> + </bean> + This configured ItemReader will return CustomerCredit objects in the exact same manner @@ -1612,12 +1581,12 @@ real complication is how those keys are obtained. The KeyCollector interface abstracts this: - public interface KeyCollector { List retrieveKeys(ExecutionContext executionContext); void updateContext(Object key, ExecutionContext executionContext); - }]]> + } The primary method in this interface is the retrieveKeys method. It is expected that this @@ -1632,12 +1601,12 @@ retrieveKeys method can then use this value to retrieve a subset of the original keys: - ExecutionContext executionContext = new ExecutionContext(); List keys = keyStrategy.retrieveKeys(executionContext); //Assume keys contains 1 through 1,000 keyStrategy.updateContext(new Long(500), executionContext); keys = keyStrategy.retrieveKeys(executionContext); - //keys should now contains 500 through 1,000]]> + //keys should now contains 500 through 1,000 This generalization illustrates the KeyCollector contract. If we assume that @@ -1700,36 +1669,36 @@ The following code helps illustrate how to setup and use a SingleColumnJdbcKeyCollector: - SingleColumnJdbcKeyCollector keyCollector = new SingleColumnJdbcKeyCollector(getJdbcTemplate(), "SELECT ID from T_FOOS order by ID"); - keyCollector.setRestartSql("SELECT ID from T_FOOS where ID > ? order by ID"); + keyCollector.setRestartSql("SELECT ID from T_FOOS where ID > ? order by ID"); ExecutionContext executionContext = new ExecutionContext(); List keys = keyStrategy.retrieveKeys(new ExecutionContext()); - for (int i = 0; i < keys.size(); i++) { + for (int i = 0; i < keys.size(); i++) { System.out.println(keys.get(i)); - }]]> + } If this code were run in the proper environment with the correct database tables setup, then it would output the following: - 1 2 3 4 -5]]> +5 Now, let's modify the code slightly to show what would happen if the code were started again after a restart, having failed after processing key 3 successfully: - SingleColumnJdbcKeyCollector keyCollector = new SingleColumnJdbcKeyCollector(getJdbcTemplate(), "SELECT ID from T_FOOS order by ID"); - keyCollector.setRestartSql("SELECT ID from T_FOOS where ID > ? order by ID"); + keyCollector.setRestartSql("SELECT ID from T_FOOS where ID > ? order by ID"); ExecutionContext executionContext = new ExecutionContext(); @@ -1737,19 +1706,19 @@ List keys = keyStrategy.retrieveKeys(executionContext); - for (int i = 0; i < keys.size(); i++) { + for (int i = 0; i < keys.size(); i++) { System.out.println(keys.get(i)); - }]]> + } Running this code snippet would result in the following: - + 4 +5 The key difference between the two examples is the following line: - + keyStrategy.updateContext(new Long(3), executionContext); This tells the key collector to update the provided ExecutionContext with the key of three. This @@ -1760,7 +1729,7 @@ ExecutionContext that was updated to contain 3, the argument of 3 will be passed to the restartSql: - ? order by ID");]]> + keyCollector.setRestartSql("SELECT ID from T_FOOS where ID > ? order by ID"); This will cause only keys 4 and 5 to be returned, since they are the only ones with an ID greater than 3. @@ -1787,13 +1756,13 @@ An ExecutionContextRowMapper provides this: - public interface ExecutionContextRowMapper extends RowMapper { public void mapKeys(Object key, ExecutionContext executionContext); public PreparedStatementSetter createSetter(ExecutionContext executionContext); } -]]> + The ExecutionContextRowMapper interface extends the standard RowMapper interface to @@ -1891,36 +1860,36 @@ items to be skipped reliably. The following example illustrates how to configure the HibernateAwareItemWriter: - - - - + <bean id="hibernateItemWriter" + class="org.springframework.batch.item.database.HibernateAwareItemWriter"> + <property name="sessionFactory" ref="sessionFactory" /> + <property name="delegate" ref="customerCreditWriter" /> + </bean> - - - + <bean id="customerCreditWriter" + class="org.springframework.batch.sample.dao.HibernateCreditDao"> + <property name="sessionFactory" ref="sessionFactory" /> + </bean> -]]> + If you are using JPA then the JpaAwareItemWriter provides comparable functionality. The following example illustrates how to configure the JpaAwareItemWriter: - - - - + <bean id="hibernateItemWriter" + class="org.springframework.batch.item.database.JpaAwareItemWriter"> + <property name="entityManagerFactory" ref="entityManagerFactory" /> + <property name="delegate" ref="customerCreditWriter" /> + </bean> - - - + <bean id="customerCreditWriter" + class="org.springframework.batch.sample.dao.JpaCreditDao"> + <property name="entityManagerFactory" ref="entityManagerFactory" /> + </bean> -]]> +
@@ -1945,12 +1914,12 @@ standard Spring method invoking delegator pattern and are fairly simple to set up. Below is an example of the reader: - - - - + <bean id="itemReader" class="org.springframework.batch.item.adapter.ItemReaderAdapter"> + <property name="targetObject" ref="fooService" /> + <property name="targetMethod" value="generateFoo" /> + </bean> - ]]> + <bean id="fooService" class="org.springframework.batch.item.sample.FooService" /> One important point to note is that the contract of the targetMethod must be the same as the contract for read: when @@ -1961,13 +1930,13 @@ ItemWriter. The ItemWriter implementation is equally as simple: - - - - + <bean id="itemWriter" class="org.springframework.batch.item.adapter.ItemWriterAdapter"> + <property name="targetObject" ref="fooService" /> + <property name="targetMethod" value="processFoo" /> + </bean> - -]]> + <bean id="fooService" class="org.springframework.batch.item.sample.FooService" /> +
@@ -1982,7 +1951,7 @@ that contains another ItemReader. For example: - public class CompositeItemWriter implements ItemWriter { ItemWriter itemWriter; @@ -2004,7 +1973,7 @@ public void flush() throws FlushFailedException { itemWriter.flush(); } -}]]> +} The class above contains another ItemWriter that it delgates to after having provided some business logic. It should @@ -2021,10 +1990,10 @@ just want to modify the item. For this scenario, Spring Batch provides the ItemTransformer interface: - public interface ItemTransformer { Object transform(Object item) throws Exception; - }]]> + } An ItemTransformer is very simple, given one object, transorm it and return another. The object provided may or may not @@ -2040,7 +2009,7 @@ ItemTransformer can be written that performs the conversion: - public class Foo {} public class Bar { public Bar(Foo foo) {} @@ -2063,7 +2032,7 @@ } //rest of class ommitted for clarity - }]]> + } In the very simple example above, there is a class Foo, a class Bar, and a @@ -2080,10 +2049,10 @@ Bar returned. The resulting Bar will then be written: - ItemTransformerItemWriter itemTransformerItemWriter = new ItemTransformerItemWriter(); itemTransformerItemWriter.setItemTransformer(new FooTransformer()); itemTransformerItemWriter.setDelegate(new BarWriter()); - itemTransformerItemWriter.write(new Foo());]]> + itemTransformerItemWriter.write(new Foo());
The Delegate Pattern and Registering with the Step @@ -2116,7 +2085,7 @@ Transformed to Bar, which will be transformed to Foobar and written out: - public class Foo {} public class Bar { public Bar(Foo foo) {} @@ -2151,17 +2120,17 @@ } //rest of class ommitted for clarity - }]]> + } A FooTransformer and BarTransformer can be 'chained' together to give the resultant Foobar: - CompositeItemTransformer compositeTransformer = new CompositeItemTransformer(); List itemTransformers = new ArrayList(); itemTransformers.add(new FooTransformer()); itemTransformers.add(new BarTransformer()); - compositeTransformer.setItemTransformers(itemTransformers);]]> + compositeTransformer.setItemTransformers(itemTransformers); The compositeTransformer could be said to accept a Foo and return a Foobar. @@ -2197,11 +2166,11 @@ rather provides a very simple interface that can be implemented by any number of frameworks: - public interface Validator { void validate(Object value) throws ValidationException; - }]]> + } The contract is that the validate method will throw an exception if the object is invalid, and return normally if @@ -2330,7 +2299,7 @@ basic contract of ItemReader, read: - public class CustomItemReader implements ItemReader{ List items; @@ -2350,14 +2319,14 @@ public void mark() throws MarkFailedException { }; public void reset() throws ResetFailedException { }; - }]]> + } This very simple class takes a list of items, and returns one at a time, removing it from the list. When the list empty, it returns null, thus satisfying the most basic requirements of an ItemReader, as illustrated below: - List items = new ArrayList(); items.add("1"); items.add("2"); items.add("3"); @@ -2366,7 +2335,7 @@ assertEquals("1", itemReader.read()); assertEquals("2", itemReader.read()); assertEquals("3", itemReader.read()); - assertNull(itemReader.read());]]> + assertNull(itemReader.read());
Making the <classname>ItemReader</classname> @@ -2382,7 +2351,7 @@ empty, but we'll need to add code to them in order to support the rollback scenario:</para> - <programlisting><![CDATA[ public class CustomItemReader implements ItemReader{ + <programlisting> public class CustomItemReader implements ItemReader{ List items; int currentIndex = 0; @@ -2395,7 +2364,7 @@ public Object read() throws Exception, UnexpectedInputException, NoWorkFoundException, ParseException { - if (currentIndex < items.size()) { + if (currentIndex < items.size()) { return items.get(currentIndex++); } return null; @@ -2408,7 +2377,7 @@ public void reset() throws ResetFailedException { currentIndex = lastMarkedIndex; }; - }]]></programlisting> + }</programlisting> <para>The <classname>CustomItemReader</classname> has now been modified to keep track of where it is currently, and where it was when @@ -2418,12 +2387,12 @@ <classname>ItemReader</classname> to the state it was in when <methodname>mark</methodname> was last called:</para> - <programlisting><![CDATA[ //Assume same setup as last example, a list with "1", "2", and "3" + <programlisting> //Assume same setup as last example, a list with "1", "2", and "3" itemReader.mark(); assertEquals("1", itemReader.read()); assertEquals("2", itemReader.read()); itemReader.reset(); - assertEquals("1", itemReader.read());]]></programlisting> + assertEquals("1", itemReader.read());</programlisting> <para>In most real world scenarios, there will likely be some kind of underlying resource that will require tracking. In the case of a file, @@ -2465,7 +2434,7 @@ implemented with the <classname>ItemStream</classname> interface:</para> - <programlisting><![CDATA[ public class CustomItemReader implements ItemReader, ItemStream{ + <programlisting> public class CustomItemReader implements ItemReader, ItemStream{ List items; int currentIndex = 0; @@ -2479,7 +2448,7 @@ public Object read() throws Exception, UnexpectedInputException, NoWorkFoundException, ParseException { - if (currentIndex < items.size()) { + if (currentIndex < items.size()) { return items.get(currentIndex++); } return null; @@ -2508,7 +2477,7 @@ }; public void close(ExecutionContext executionContext) throws ItemStreamException {} - }]]></programlisting> + }</programlisting> <para>On each call to <classname>ItemStream</classname> <methodname>update</methodname> method, the current index of the @@ -2520,7 +2489,7 @@ current index is moved to that location. This is a fairly trivial example, but it still meets the general contract:</para> - <programlisting><![CDATA[ ExecutionContext executionContext = new ExecutionContext(); + <programlisting> ExecutionContext executionContext = new ExecutionContext(); ((ItemStream)itemReader).open(executionContext); assertEquals("1", itemReader.read()); ((ItemStream)itemReader).update(executionContext); @@ -2532,7 +2501,7 @@ itemReader = new CustomItemReader(items); ((ItemStream)itemReader).open(executionContext); - assertEquals("2", itemReader.read());]]></programlisting> + assertEquals("2", itemReader.read());</programlisting> <para>Most ItemReaders have much more sophisticated restart logic. The <classname>DrivingQueryItemReader</classname>, for example, only loads @@ -2563,7 +2532,7 @@ example. As with the <classname>ItemReader</classname> example, a List will be used in order to keep the example as simple as possible:</para> - <programlisting><![CDATA[ public class CustomItemWriter implements ItemWriter{ + <programlisting> public class CustomItemWriter implements ItemWriter{ List output = new ArrayList(); @@ -2574,7 +2543,7 @@ public void clear() throws ClearFailedException { } public void flush() throws FlushFailedException { } - }]]></programlisting> + }</programlisting> <section> <title>Making the <classname>ItemReader</classname> @@ -2594,7 +2563,7 @@ <methodname>flush</methodname> and <methodname>clear</methodname> should be implemented to allow for a buffering solution:</para> - <programlisting><![CDATA[ public class CustomItemWriter implements ItemWriter{ + <programlisting> public class CustomItemWriter implements ItemWriter{ List output = new ArrayList(); List buffer = new ArrayList(); @@ -2613,7 +2582,7 @@ it.remove(); } } - }]]></programlisting> + }</programlisting> <para>The <classname>ItemWriter</classname> buffers all output, only writing to the actual output (in this case by added to a list) when