正如在Batch Domain Language中敘述的,Step是一個獨立封裝域對象,包含了所有定義和控制實際處理信息批任務的序列。這是一個比較抽象的描述,因爲任意一個Step的內容都是開發者自己編寫的Job。一個Step的簡單或複雜取決於開發者的意願。一個簡單的Step也許是從本地文件讀取數據存入數據庫,寫很少或基本無需寫代碼。一個複雜的Step也許有複雜的業務規則(取決於所實現的方式),並作爲整個個流程的一部分。
所有的批處理都可以描述爲最簡單的形式: 讀取大量的數據, 執行某種類型的計算/轉換, 以及寫出執行結果.Spring Batch 提供了三個主要接口來輔助執行大量的讀取與寫出: ItemReader, ItemProcessor 和 ItemWriter.
1.1 ItemReader
最簡單的概念, ItemReader 就是一種從各個輸入源讀取數據,然後提供給後續步驟的方式. 最常見的例子包括:
- Flat FileFlat File Item Readers 從純文本文件中讀取一行行的數據, 存儲數據的純文本文件通常具有固定的格式, 並且使用某種特殊字符來分隔每條記錄中的各個字段(例如逗號,Comma).
- XML XML ItemReaders 獨立地處理XML,包括用於解析、映射和驗證對象的技術。還可以對輸入數據的XML文件執行XSD schema驗證。
- Database 數據庫就是對請求返回結果集的資源,結果集可以被映射轉換爲需要處理的對象。默認的SQL ItemReaders調用一個 RowMapper 來返回對象, 並跟蹤記錄當前行,以備有重啓的情況, 存儲基本統計信息,並提供一些事務增強特性,關於事物將在稍後解釋。
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException;
}
一般約定 ItemReader 接口的實現都是向前型的(forward only). 但如果底層資源是事務性質的(如JMS隊列),並且發生回滾(rollback), 那麼下一次調用 read 方法有可能會返回和前次邏輯上相等的結果(對象)。值得一提的是, 處理過程中如果沒有items, ItemReader 不應該拋出異常。例如,數據庫 ItemReader 配置了一條查詢語句, 返回結果數爲0, 則第一次調用read方法將返回null。
1.2 ItemWriter
public interface ItemWriter<T> {
void write(List<? extends T> items) throws Exception;
}
1.3 ItemProcessor
public class CompositeItemWriter<T> implements ItemWriter<T> {
ItemWriter<T> itemWriter;
public CompositeItemWriter(ItemWriter<T> itemWriter) {
this.itemWriter = itemWriter;
}
public void write(List<? extends T> items) throws Exception {
// ... 此處可以執行某些業務邏輯
itemWriter.write(item);
}
public void setDelegate(ItemWriter<T> itemWriter){
this.itemWriter = itemWriter;
}
}
public interface ItemProcessor<I, O> {
O process(I item) throws Exception;
}
ItemProcessor非常簡單; 傳入一個對象,對其進行某些處理/轉換,然後返回另一個對象(也可以是同一個)。傳入的對象和返回的對象類型可以一樣,也可以不一致。關鍵點在於處理過程中可以執行一些業務邏輯操作,當然這完全取決於開發者怎麼實現它。一個ItemProcessor可以被直接關聯到某個Step(步驟),例如,假設ItemReader的返回類型是 Foo ,而在寫出之前需要將其轉換成類型Bar的對象。就可以編寫一個ItemProcessor來執行這種轉換:
public class Foo {}
public class Bar {
public Bar(Foo foo) {}
}
public class FooProcessor implements ItemProcessor<Foo,Bar>{
public Bar process(Foo foo) throws Exception {
//執行某些操作,將 Foo 轉換爲 Bar對象
return new Bar(foo);
}
}
public class BarWriter implements ItemWriter<Bar>{
public void write(List<? extends Bar> bars) throws Exception {
//write bars
}
}
<job id="ioSampleJob">
<step name="step1">
<tasklet>
<chunk reader="fooReader" processor="fooProcessor" writer="barWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
1.3.1 Chaining ItemProcessors
public class Foo {}
public class Bar {
public Bar(Foo foo) {}
}
public class Foobar{
public Foobar(Bar bar) {}
}
public class FooProcessor implements ItemProcessor<Foo,Bar>{
public Bar process(Foo foo) throws Exception {
//Perform simple transformation, convert a Foo to a Bar
return new Bar(foo);
}
}
public class BarProcessor implements ItemProcessor<Bar,FooBar>{
public FooBar process(Bar bar) throws Exception {
return new Foobar(bar);
}
}
public class FoobarWriter implements ItemWriter<FooBar>{
public void write(List<? extends FooBar> items) throws Exception {
//write items
}
}
CompositeItemProcessor<Foo,Foobar> compositeProcessor = new CompositeItemProcessor<Foo,Foobar>();
List itemProcessors = new ArrayList();
itemProcessors.add(new FooTransformer());
itemProcessors.add(new BarTransformer());
compositeProcessor.setDelegates(itemProcessors);
<job id="ioSampleJob">
<step name="step1">
<tasklet>
<chunk reader="fooReader" processor="compositeProcessor" writer="foobarWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
<bean id="compositeItemProcessor" class="org.springframework.batch.item.support.CompositeItemProcessor">
<property name="delegates">
<list>
<bean class="..FooProcessor" />
<bean class="..BarProcessor" />
</list>
</property>
</bean>
1.3.2 Filtering Records
例如, 某個批處理作業,從一個文件中讀取三種不同類型的記錄: 準備 insert 的記錄、準備 update 的記錄,需要 delete 的記錄。如果系統中不允許刪除記錄, 那麼我們肯定不希望將 “delete” 類型的記錄傳遞給 ItemWriter。 但因爲這些記錄又不是損壞的信息(bad records), 我們只想將其過濾掉,而不是跳過。 因此,ItemWriter只會收到 "insert" 和 "update"的記錄。
要過濾某條記錄, 只需要 ItemProcessor 返回“ null ” 即可. 框架將自動檢測結果爲“ null ”的情況, 不會將該item 添加到傳給ItemWriter的list中。 像往常一樣, 在 ItemProcessor 中拋出異常將會導致跳過(skip)。
1.3.3 容錯(Fault Tolerance)
1.4 ItemStream
public interface ItemStream {
void open(ExecutionContext executionContext) throws ItemStreamException;
void update(ExecutionContext executionContext) throws ItemStreamException;
void close() throws ItemStreamException;
}
1.5 委託模式(Delegate Pattern)與註冊Step
<job id="ioSampleJob">
<step name="step1">
<tasklet>
<chunk reader="fooReader" processor="fooProcessor" writer="compositeItemWriter" commit-interval="2">
<streams>
<stream ref="barWriter" />
</streams>
</chunk>
</tasklet>
</step>
</job>
<bean id="compositeItemWriter" class="...CustomCompositeItemWriter">
<property name="delegate" ref="barWriter" />
</bean>
<bean id="barWriter" class="...BarWriter" />
1.6 純文本平面文件(Flat Files)
1.6.1 The FieldSet(字段集)
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);
1.6.2 FlatFileItemReader
Resource resource = new FileSystemResource("resources/trades.csv");
LineMapper
public interface LineMapper<T> {
T mapLine(String line, int lineNumber) throws Exception;
}
FlatFileItemReader
但與 RowMapper 不同的是, LineMapper 只能取得原始行的String值, 正如上面所說, 給你的是一個半成品。 這行文本值必須先被解析爲 FieldSet, 然後纔可以映射爲一個對象,如下所述。
LineTokenizer
public interface LineTokenizer {
FieldSet tokenize(String line);
}
- DelmitedLineTokenizer 適用於處理使用分隔符(delimiter)來分隔一條數據中各個字段的文件。最常見的分隔符是逗號(comma),但管道或分號也經常使用。
- FixedLengthTokenizer 適用於記錄中的字段都是“固定寬度(fixed width)”的文件。每種記錄類型中,每個字段的寬度必須先定義。
- PatternMatchingCompositeLineTokenizer 通過使用正則模式匹配,來決定對特定的某一行應該使用 LineTokenizers 列表中的哪一個來執行字段拆分。
FieldSetMapper
public interface FieldSetMapper<T> {
T mapFieldSet(FieldSet fieldSet);
}
這和JdbcTemplate中的RowMapper是一樣的道理。
DefaultLineMapper
- 從文件中讀取一行。
- 將讀取的字符串傳給 LineTokenizer#tokenize() 方法,以獲取一個 FieldSet。
- 將解析後的 FieldSet 傳給 FieldSetMapper ,然後將 ItemReader#read() 方法執行的結果返回給調用者。
FlatFileItemReader
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));
}
public void setLineTokenizer(LineTokenizer tokenizer) {
this.tokenizer = tokenizer;
}
public void setFieldSetMapper(FieldSetMapper<T> fieldSetMapper) {
this.fieldSetMapper = fieldSetMapper;
}
}
文件分隔符讀取簡單示例
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"
public class Player implements Serializable {
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;
}
// setters and getters...
}
protected static class PlayerFieldSetMapper implements FieldSetMapper<Player> {
public Player mapFieldSet(FieldSet fieldSet) {
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));
return player;
}
}
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();
根據Name映射 Fields
tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"});
public class PlayerMapper implements FieldSetMapper<Player> {
public Player mapFieldSet(FieldSet fs) {
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;
}
}
將FieldSet字段映射爲Domain Object
BeanWrapperFieldSetMapper 的配置如下所示:
<bean id="fieldSetMapper" class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="prototypeBeanName" value="player" />
</bean>
<bean id="player" class="org.springframework.batch.sample.domain.Player" scope="prototype" />
Fixed Length File Formats
UK21341EAH4121131.11customer1
UK21341EAH4221232.11customer2
UK21341EAH4321333.11customer3
UK21341EAH4421434.11customer4
UK21341EAH4521535.11customer5
- ISIN : 唯一標識符,訂購的商品編碼 - 佔12字符。
- Quantity : 訂購的商品數量 - 佔3字符。
- Price : 商品的價格 - 佔5字符。
- Customer : 訂購商品的顧客Id - 佔9字符。
<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>
單文件中含有多種類型數據的處理
USER;Smith;Peter;;T;20014539;F
LINEA;1044391041ABC037.49G201XX1383.12H
LINEB;2134776319DEF422.99M005LI
<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>
</property>
<property name="fieldSetMappers">
<map>
<entry key="USER*" value-ref="userFieldSetMapper" />
<entry key="LINE*" value-ref="lineFieldSetMapper" />
</map>
</property>
</bean>
<entry key="*" value-ref="defaultLineTokenizer" />
Flat File 的異常處理
在解析一行時, 可能有很多情況會導致異常被拋出。很多平面文件不是很完整, 或者裏面的某些記錄格式不正確。許多用戶會選擇忽略這些錯誤的行, 只將這個問題記錄到日誌, 比如原始行,行號。稍後可以人工審查這些日誌,也可以由另一個批處理作業來檢查。出於這個原因,Spring Batch提供了一系列的異常類: FlatFileParseException ,和 FlatFileFormatException 。
FlatFileParseException 是由 FlatFileItemReader 在讀取文件時解析錯誤而拋出的。 FlatFileFormatException 是由實現了LineTokenizer 接口的類拋出的, 表明在拆分字段時發生了一個更具體的錯誤。
IncorrectTokenCountException
tokenizer.setNames(new String[] {"A", "B", "C", "D"});
try {
tokenizer.tokenize("a,b,c");
}catch(IncorrectTokenCountException e){
assertEquals(4, e.getExpectedCount());
assertEquals(3, e.getActualCount());
}
IncorrectLineLengthException
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) {
assertEquals(15, ex.getExpectedLength());
assertEquals(5, ex.getActualLength());
}
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));
1.6.3 FlatFileItemWriter
LineAggregator
public interface LineAggregator<T> {
public String aggregate(T item);
}
PassThroughLineAggregator
public class PassThroughLineAggregator<T> implements LineAggregator<T> {
public String aggregate(T item) {
return item.toString();
}
}
上面的實現對於需要直接轉換爲string的時候是很管用的,但是 FlatFileItemWriter 的一些優勢也是很有必要的,比如 事務,以及支持重啓特性等.
簡單的文件寫入示例
- 將要寫出的對象傳遞給 LineAggregator 以獲取一個字符串(String).
- 將返回的 String 寫入配置指定的文件中.
public void write(T item) throws Exception {
write(lineAggregator.aggregate(item) + LINE_SEPARATOR);
}
<bean id="itemWriter" class="org.spr...FlatFileItemWriter">
<property name="resource" value="file:target/test-outputs/output.txt" />
<property name="lineAggregator">
<bean class="org.spr...PassThroughLineAggregator"/>
</property>
</bean>
屬性提取器 FieldExtractor
- 從文件中讀取一行.
- 將這一行字符串傳遞給 LineTokenizer#tokenize() 方法, 以獲取 FieldSet 對象
- 將分詞器返回的 FieldSet 傳給一個 FieldSetMapper 映射器, 然後將 ItemReader#read() 方法得到的結果 return。
- 將要寫入的對象傳遞給 writer
- 將領域對象的屬性域轉換爲數組
- 將結果數組合並(aggregate)爲一行字符串
public interface FieldExtractor<T> {
Object[] extract(T item);
}
FieldExtractor 的實現類應該根據傳入對象的屬性創建一個數組, 稍後使用分隔符將各個元素寫入文件,或者作爲 field-width line 的一部分.
PassThroughFieldExtractor
BeanWrapperFieldExtractor
BeanWrapperFieldExtractor<Name> extractor = new BeanWrapperFieldExtractor<Name>();
extractor.setNames(new String[] { "first", "last", "born" });
String first = "Alan";
String last = "Turing";
int born = 1912;
Name n = new Name(first, last, born);
Object[] values = extractor.extract(n);
assertEquals(first, values[0]);
assertEquals(last, values[1]);
assertEquals(born, values[2]);
這個 extractor 實現只有一個必需的屬性,就是 names , 裏面用來存放要映射字段的名字。 就像 BeanWrapperFieldSetMapper 需要字段名稱來將 FieldSet 中的 field 映射到對象的 setter 方法一樣, BeanWrapperFieldExtractor 需要 names 映射 getter 方法來創建一個對象數組。值得注意的是, names的順序決定了field在數組中的順序。
分隔符文件(Delimited File)寫入示例
public class CustomerCredit {
private int id;
private String name;
private BigDecimal credit;
//getters and setters removed for clarity
}
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
<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>
在這種情況下, 本章前面提到過的 BeanWrapperFieldExtractor 被用來將 CustomerCredit 中的 name 和 credit 字段轉換爲一個對象數組, 然後在各個字段之間用逗號分隔寫入文件。
固定寬度的(Fixed Width)文件寫入示例
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource" ref="outputResource" />
<property name="lineAggregator">
<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>
<property name="format" value="%-9s%-2.0f" />
處理文件創建(Handling File Creation)
(exception)。
但是文件的寫入就沒那麼簡單了。乍一看可能會覺得跟 FlatFileItemWriter 一樣簡單直接粗暴: 如果文件存在則拋出異常, 如果
不存在則創建文件並開始寫入。
但是, 作業的重啓有可能會有BUG。 在正常的重啓情景中, 約定與前面所想的恰恰相反: 如果文件存在, 則從已知的最後一個
正確位置開始寫入, 如果不存在, 則拋出異常。
如果此作業(Job)的文件名每次都是一樣的那怎麼辦? 這時候可能需要刪除已存在的文件(重啓則不刪除)。 因爲有這些可能性,
FlatFileItemWriter 有一個屬性 shouldDeleteIfExists 。將這個屬性設置爲 true , 打開 writer 時會將已有的同名文件刪除。
1.7 XML Item Readers and Writers
1.7.1 StaxEventItemReader
<?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>
- Root Element Name 片段根元素的名稱就是要映射的對象。上面的示例代表的是 trade 的值。
- Resource Spring Resource 代表了需要讀取的文件。
- Unmarshaller Spring OXM提供的Unmarshalling 用於將 XML片段映射爲對象.
<bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader">
<property name="fragmentRootElementName" value="trade" />
<property name="resource" value="data/iosample/input/input.xml" />
<property name="unmarshaller" ref="tradeMarshaller" />
</bean>
<bean id="tradeMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<util:map id="aliases">
<entry key="trade" value="org.springframework.batch.sample.domain.Trade" />
<entry key="price" value="java.math.BigDecimal" />
<entry key="name" value="java.lang.String" />
</util:map>
</property>
</bean>
當 reader 讀取到XML資源的一個新片段時(匹配默認的標籤名稱)。reader 根據這個片段構建一個獨立的XML(或至少看起來是這樣),並將 document 傳給反序列化器(通常是一個Spring OXM Unmarshaller 的包裝類)將XML映射爲一個Java對象。
StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader()
Resource resource = new ByteArrayResource(xmlResource.getBytes())
Map aliases = new HashMap();
aliases.put("trade","org.springframework.batch.sample.domain.Trade");
aliases.put("price","java.math.BigDecimal");
aliases.put("customer","java.lang.String");
Marshaller marshaller = new XStreamMarshaller();
marshaller.setAliases(aliases);
xmlStaxEventItemReader.setUnmarshaller(marshaller);
xmlStaxEventItemReader.setResource(resource);
xmlStaxEventItemReader.setFragmentRootElementName("trade");
xmlStaxEventItemReader.open(new ExecutionContext());
boolean hasNext = true
CustomerCredit credit = null;
while (hasNext) {
credit = xmlStaxEventItemReader.read();
if (credit == null) {
hasNext = false;
}
else {
System.out.println(credit);
}
}
1.7.2 StaxEventItemWriter
<bean id="itemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter">
<property name="resource" ref="outputResource" />
<property name="marshaller" ref="customerCreditMarshaller" />
<property name="rootTagName" value="customers" />
<property name="overwriteOutput" value="true" />
</bean>
<bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<util:map id="aliases">
<entry key="customer" value="org.springframework.batch.sample.domain.CustomerCredit" />
<entry key="credit" value="java.math.BigDecimal" />
<entry key="name" value="java.lang.String" />
</util:map>
</property>
</bean>
StaxEventItemWriter staxItemWriter = new StaxEventItemWriter()
FileSystemResource resource = new FileSystemResource("data/outputFile.xml")
Map aliases = new HashMap();
aliases.put("customer","org.springframework.batch.sample.domain.CustomerCredit");
aliases.put("credit","java.math.BigDecimal");
aliases.put("name","java.lang.String");
Marshaller marshaller = new XStreamMarshaller();
marshaller.setAliases(aliases);
staxItemWriter.setResource(resource);
staxItemWriter.setMarshaller(marshaller);
staxItemWriter.setRootTagName("trades");
staxItemWriter.setOverwriteOutput(true);
ExecutionContext executionContext = new ExecutionContext();
staxItemWriter.open(executionContext);
CustomerCredit Credit = new CustomerCredit();
trade.setPrice(11.39);
credit.setName("Customer1");
staxItemWriter.write(trade);
1.8 多個數據輸入文件
file-1.txt
file-2.txt
ignored.txt
file-1.txt 和 file-2.txt 具有相同的格式, 根據業務需求需要一起處理. 可以通過 MuliResourceItemReader 使用 通配符的形式來讀取這兩個文件:
<bean id="multiResourceReader" class="org.spr...MultiResourceItemReader">
<property name="resources" value="classpath:data/input/file-*.txt" />
<property name="delegate" ref="flatFileItemReader" />
</bean>
1.9 數據庫(Database)
1.9.1 基於Cursor的ItemReaders
Spring Batch 基於 cursor 的 ItemReaders 在初始化時打開遊標, 每次調用 read 時則將遊標向前移動一行, 返回一個可用於進行處理的映射對象。最好將會調用 close 方法, 以確保所有資源都被釋放。
Spring 的 JdbcTemplate 的解決辦法, 是通過回調模式將 ResultSet 中所有行映射之後,在返回調用方法前關閉結果集來處理的。
但是,在批處理的時候就不一樣了, 必須得等 step 執行完成才能調用close。下圖描繪了基於遊標的ItemReader是如何處理的,使用的SQL語句非常簡單, 而且都是類似的實現方式:
JdbcCursorItemReader
CREATE TABLE CUSTOMER (
ID BIGINT IDENTITY PRIMARY KEY,
NAME VARCHAR(45),
CREDIT FLOAT
);
public class CustomerCreditRowMapper implements RowMapper {
public static final String ID_COLUMN = "id";
public static final String NAME_COLUMN = "name";
public static final String CREDIT_COLUMN = "credit";
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
CustomerCredit customerCredit = new CustomerCredit();
customerCredit.setId(rs.getInt(ID_COLUMN));
customerCredit.setName(rs.getString(NAME_COLUMN));
customerCredit.setCredit(rs.getBigDecimal(CREDIT_COLUMN));
return customerCredit;
}
}
//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());
JdbcCursorItemReader itemReader = new JdbcCursorItemReader();
itemReader.setDataSource(dataSource);
itemReader.setSql("SELECT ID, NAME, CREDIT from CUSTOMER");
itemReader.setRowMapper(new CustomerCreditRowMapper());
int counter = 0;
ExecutionContext executionContext = new ExecutionContext();
itemReader.open(executionContext);
Object customerCredit = new Object();
while(customerCredit != null){
customerCredit = itemReader.read();
counter++;
}
itemReader.close(executionContext);
<bean id="itemReader" class="org.spr...JdbcCursorItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="sql" value="select ID, NAME, CREDIT from CUSTOMER"/>
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
</property>
</bean>
HibernateCursorItemReader
HibernateTemplate , Spring Batch開發者也面臨同樣的選擇。HibernateCursorItemReader 是 Hibernate 的遊標實現。 其實在批處理中使用 Hibernate 那是相當有爭議。這很大程度上是因爲 Hibernate 最初就是設計了用來開發在線程序的。
但也不是說Hibernate就不能用來進行批處理。最簡單的解決辦法就是使用一個 StatelessSession (無狀態會話), 而不使用標準 session 。這樣就去掉了在批處理場景中 Hibernate 那些惱人的緩存、髒檢查等等。
更多無狀態會話與正常hibernate會話之間的差異, 請參考你使用的 hibernate 版本對應的文檔。HibernateCursorItemReader 允許您聲明一個HQL語句, 並傳入 SessionFactory , 然後每次調用 read 時就會返回一個對象, 和 JdbcCursorItemReader 一樣。下面的示例配置也使用和 JDBC reader 相同的數據庫表:
HibernateCursorItemReader itemReader = new HibernateCursorItemReader();
itemReader.setQueryString("from CustomerCredit");
//For simplicity sake, assume sessionFactory already obtained.
itemReader.setSessionFactory(sessionFactory);
itemReader.setUseStatelessSession(true);
int counter = 0;
ExecutionContext executionContext = new ExecutionContext();
itemReader.open(executionContext);
Object customerCredit = new Object();
while(customerCredit != null){
customerCredit = itemReader.read();
counter++;
}
itemReader.close(executionContext);
hibernate 映射文件正確的話。 useStatelessSession 屬性的默認值爲 true , 這裏明確設置的目的只是爲了引起你的注意,我們可以通過他來進行切換。 還值得注意的是 可以通過 setFetchSize 設置底層 cursor 的 fetchSize 屬性 。與JdbcCursorItemReader一樣,配置很簡單:
<bean id="itemReader" class="org.springframework.batch.item.database.HibernateCursorItemReader">
<property name="sessionFactory" ref="sessionFactory" />
<property name="queryString" value="from CustomerCredit" />
</bean>
StoredProcedureItemReader
- 作爲一個 ResultSet 返回(SQL Server, Sybase, DB2, Derby 以及 MySQL支持)
- 作爲一個 out 參數返回 ref-cursor (Oracle和PostgreSQL使用這種方式)
- 作爲存儲函數(stored function)的返回值
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="sp_customer_credit"/>
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
</property>
</bean>
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="sp_customer_credit"/>
<property name="refCursorPosition" value="1"/>
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
</property>
</bean>
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="sp_customer_credit"/>
<property name="function" value="true"/>
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
</property>
</bean>
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="spring.cursor_func"/>
<property name="parameters">
<list>
<bean class="org.springframework.jdbc.core.SqlOutParameter">
<constructor-arg index="0" value="newid"/>
<constructor-arg index="1">
<util:constant static-field="oracle.jdbc.OracleTypes.CURSOR"/>
</constructor-arg>
</bean>
<bean class="org.springframework.jdbc.core.SqlParameter">
<constructor-arg index="0" value="amount"/>
<constructor-arg index="1">
<util:constant static-field="java.sql.Types.INTEGER"/>
</constructor-arg>
</bean>
<bean class="org.springframework.jdbc.core.SqlParameter">
<constructor-arg index="0" value="custid"/>
<constructor-arg index="1">
<util:constant static-field="java.sql.Types.INTEGER"/>
</constructor-arg>
</bean>
</list>
</property>
<property name="refCursorPosition" value="1"/>
<property name="rowMapper" ref="rowMapper"/>
<property name="preparedStatementSetter" ref="parameterSetter"/>
</bean>
1.9.2 可分頁的 ItemReader
JdbcPagingItemReader
SqlPagingQueryProviderFactoryBean 需要指定一個 select 子句以及一個 from 子句(clause). 當然還可以選擇提供 where子句. 這些子句加上所需的排序列 sortKey 被組合成爲一個 SQL 語句(statement).
在 reader 被打開以後, 每次調用 read 方法則返回一個 item,和其他的 ItemReader一樣. 使用分頁是因爲可能需要額外的行.
<bean id="itemReader" class="org.spr...JdbcPagingItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="queryProvider">
<bean class="org.spr...SqlPagingQueryProviderFactoryBean">
<property name="selectClause" value="select id, name, credit"/>
<property name="fromClause" value="from customer"/>
<property name="whereClause" value="where status=:status"/>
<property name="sortKey" value="id"/>
</bean>
</property>
<property name="parameterValues">
<map>
<entry key="status" value="NEW"/>
</map>
</property>
<property name="pageSize" value="1000"/>
<property name="rowMapper" ref="customerMapper"/>
</bean>
' parameterValues '屬性可用來爲查詢指定參數映射map。如果在where子句中使用了命名參數,那麼這些entry的key應該和命名參數一一對應。如果使用傳統的 '?' 佔位符, 則每個entry的key就應該是佔位符的數字編號,和JDBC佔位符一樣索引都是從1開始。
JpaPagingItemReader
JpaPagingItemReader 允許您聲明一個JPQL語句,並傳入一個 EntityManagerFactory 。然後就和其他的 ItemReader 一樣,每次調用它的 read 方法都會返回一個 item. 當需要更多實體,則內部就會自動發生分頁。下面是一個示例配置,和上面的JDBC reader一樣,都是 'customer credit':
<bean id="itemReader" class="org.spr...JpaPagingItemReader">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="queryString" value="select c from CustomerCredit c"/>
<property name="pageSize" value="1000"/>
</bean>
IbatisPagingItemReader
下面是和上面的示例同樣功能的配置,使用IbatisPagingItemReader來讀取CustomerCredits:
<bean id="itemReader" class="org.spr...IbatisPagingItemReader">
<property name="sqlMapClient" ref="sqlMapClient"/>
<property name="queryId" value="getPagedCustomerCredits"/>
<property name="pageSize" value="1000"/>
</bean>
<select id="getPagedCustomerCredits" resultMap="customerCreditResult">
select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize#
</select>
<select id="getPagedCustomerCredits" resultMap="customerCreditResult">
select * from (
select * from (
select t.id, t.name, t.credit, ROWNUM ROWNUM_ from customer t order by id
)) where ROWNUM_ <![CDATA[ > ]]> ( #_page# * #_pagesize# )
) where ROWNUM <![CDATA[ <= ]]> #_pagesize#
</select>
1.9.3 Database ItemWriters
1.10 重用已存在的 Service
<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" />
特別需要注意的是, targetMethod 必須和 read 方法行爲對等: 如果不存在則返回null, 否則返回一個 Object。 其他的值會使框架不知道何時該結束處理, 或者引起無限循環或不正確的失敗,這取決於 ItemWriter 的實現。 ItemWriter 的實現同樣簡單:
<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" />
1.11 輸入校驗
public interface Validator {
void validate(Object value) throws ValidationException;
}
約定是如果對象無效則 validate 方法拋出一個異常, 如果對象合法那就正常返回。 Spring Batch 提供了開箱即用的ItemProcessor:
<bean class="org.springframework.batch.item.validator.ValidatingItemProcessor">
<property name="validator" ref="validator" />
</bean>
<bean id="validator" class="org.springframework.batch.item.validator.SpringValidator">
<property name="validator">
<bean id="orderValidator" class="org.springmodules.validation.valang.ValangValidator">
<property name="valang">
<value>
<![CDATA[
{ orderId : ? > 0 AND ? <= 9999999999 : 'Incorrect order ID' : 'error.order.id' }
{ totalLines : ? = size(lineItems) : 'Bad count of order lines'
: 'error.order.lines.badcount'}
{ customer.registered : customer.businessCustomer = FALSE OR ? = TRUE
: 'Business customer must be registered'
: 'error.customer.registration'}
{ customer.companyName : customer.businessCustomer = FALSE OR ? HAS TEXT
: 'Company name for business customer is mandatory'
:'error.customer.companyname'}
]]>
</value>
</property>
</bean>
</property>
</bean>
1.12 不保存執行狀態
<bean id="playerSummarizationSource" class="org.spr...JdbcCursorItemReader">
<property name="dataSource" ref="dataSource" />
<property name="rowMapper">
<bean class="org.springframework.batch.sample.PlayerSummaryMapper" />
</property>
<property name="saveState" value="false" />
<property name="sql">
<value>
SELECT games.player_id, games.year_no, SUM(COMPLETES),
SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD),
SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS),
SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)
from games, players where players.player_id =
games.player_id group by games.player_id, games.year_no
</value>
</property>
</bean>
1.13 創建自定義 ItemReaders 與 ItemWriters
1.13.1 自定義 ItemReader 示例
public class CustomItemReader<T> implements ItemReader<T>{
List<T> items;
public CustomItemReader(List<T> items) {
this.items = items;
}
public T read() throws Exception, UnexpectedInputException,NoWorkFoundException, ParseException {
if (!items.isEmpty()) {
return items.remove(0);
}
return null;
}
}
List<String> items = new ArrayList<String>();
items.add("1");
items.add("2");
items.add("3");
ItemReader itemReader = new CustomItemReader<String>(items);
assertEquals("1", itemReader.read());
assertEquals("2", itemReader.read());
assertEquals("3", itemReader.read());
assertNull(itemReader.read());
使 ItemReader 支持重啓
如果需要保存狀態信息,那應該使用 ItemStream 接口:
public class CustomItemReader<T> implements ItemReader<T>, ItemStream {
List<T> items;
int currentIndex = 0;
private static final String CURRENT_INDEX = "current.index";
public CustomItemReader(List<T> items) {
this.items = items;
}
public T read() throws Exception, UnexpectedInputException,ParseException {
if (currentIndex < items.size()) {
return items.get(currentIndex++);
}
return null;
}
public void open(ExecutionContext executionContext) throws ItemStreamException {
if(executionContext.containsKey(CURRENT_INDEX)){
currentIndex = new Long(executionContext.getLong(CURRENT_INDEX)).intValue();
}else{
currentIndex = 0;
}
}
public void update(ExecutionContext executionContext) throws ItemStreamException {
executionContext.putLong(CURRENT_INDEX, new Long(currentIndex).longValue());
}
public void close() throws ItemStreamException {}
}
ExecutionContext executionContext = new ExecutionContext();
((ItemStream)itemReader).open(executionContext);
assertEquals("1", itemReader.read());
((ItemStream)itemReader).update(executionContext);
List<String> items = new ArrayList<String>();
items.add("1");
items.add("2");
items.add("3");
itemReader = new CustomItemReader<String>(items);
((ItemStream)itemReader).open(executionContext);
assertEquals("2", itemReader.read());
還值得注意的是 ExecutionContext 中使用的 key 不應該過於簡單。這是因爲 ExecutionContext 被一個 Step 中的所有ItemStreams 共用。在大多數情況下,使用類名加上 key 的方式應該就足以保證唯一性。然而,在極端情況下, 同一個類的多個ItemStream 被用在同一個Step中時( 如需要輸出兩個文件的情況),就需要更加具備唯一性的name標識。出於這個原因,SpringBatch 的許多 ItemReader 和 ItemWriter 實現都有一個 setName() 方法, 允許覆蓋默認的 key name。
1.13.2 自定義 ItemWriter 示例
public class CustomItemWriter<T> implements ItemWriter<T> {
List<T> output = TransactionAwareProxyFactory.createTransactionalList();
public void write(List<? extends T> items) throws Exception {
output.addAll(items);
}
public List<T> getOutput() {
return output;
}
}
讓 ItemWriter 支持重新啓動
實際開發中, 如果自定義 ItemWriter restartable(支持重啓),則會委託另一個 writer(例如, 在寫入文件時), 否則會寫入到關係型數據庫(支持事務的資源)中, 此時 ItemWriter 不需要 restartable特性,因爲自身是無狀態的。 如果你的 writer 有狀態, 則應該實現2個接口: ItemStream 和 ItemWriter 。 請記住, writer客戶端需要知道 ItemStream 的存在, 所以需要在 xml 配置文件中將其註冊爲 stream.