Spring batch教程 之 配置Step

 正如在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 來返回對象, 並跟蹤記錄當前行,以備有重啓的情況, 存儲基本統計信息,並提供一些事務增強特性,關於事物將在稍後解釋。
ItemReader 是一個通用輸入操作的基本接口:

   public interface ItemReader<T> {

      T read() throws Exception, UnexpectedInputException, ParseException;

}

read 是ItemReader中最根本的方法; 每次調用它都會返回一個 Item 或 null(如果沒有更多item)。每個 item條目, 一般對應文件中的一行(line), 或者對應數據庫中的一行(row), 也可以是XML文件中的一個元素(element)。 一般來說, 這些item都可以被映射爲一個可用的domain對象(如 Trade, User 等等), 但也不是強制要求(最偷懶的方式,返回一個Map)。

一般約定 ItemReader 接口的實現都是向前型的(forward only). 但如果底層資源是事務性質的(如JMS隊列),並且發生回滾(rollback), 那麼下一次調用 read 方法有可能會返回和前次邏輯上相等的結果(對象)。值得一提的是, 處理過程中如果沒有items, ItemReader 不應該拋出異常。例如,數據庫 ItemReader 配置了一條查詢語句, 返回結果數爲0, 則第一次調用read方法將返回null。

1.2 ItemWriter


ItemWriter 在功能上類似於 ItemReader,但屬於相反的操作。 資源仍然需要定位,打開和關閉, 區別就在於在於ItemWriter 執行的是寫入操作(write out), 而不是讀取。 在使用數據庫或隊列的情況下,寫入操作對應的是插入( insert ),更新( update ),或發送( send )。 序列化輸出的格式依賴於每個批處理作業自己的定義。

和 ItemReader 接口類似, ItemWriter 也是個相當通用的接口:

    public interface ItemWriter<T> {

         void write(List<? extends T> items) throws Exception;

}

類比於ItemReader中的read,write方法是ItemWriter 接口的根本方法; 只要傳入的items列表是打開的,那麼它就會嘗試着將其寫入(write out)。 因爲一般來說,items 將要被批量寫入到一起,然後再輸出, 所以 write 方法接受一個List 參數,而不是單個對象(item)。list輸出後,在write方法返回(return)之前,對緩衝執行刷出(flush)操作是很必要的。例如,如果使用Hibernate DAO時,對每個對象要調用一次DAO寫操作, 操作完成之後, 方法 return 之前,writer就應該關閉hibernate的Session會話。

1.3 ItemProcessor


ItemReader 和 ItemWriter 接口對於每個任務來說都是非常必要的, 但如果想要在寫出數據之前執行某些業務邏輯操作時要怎麼辦呢? 一個選擇是對讀取(reading)和寫入(writing)使用組合模式(composite pattern): 創建一個 ItemWriter 的子類實現, 內部包含另一個 ItemWriter 對象的引用(對於 ItemReader 也是類似的). 示例如下:

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;
       }
}

上面的類中包含了另一個ItemWriter引用,通過代理它來實現某些業務邏輯。 這種模式對於 ItemReader 也是一樣的道理, 但也可能持有內部 ItemReader 所擁有的多個數據輸入對象的引用。 在ItemWriter中如果我們想要自己控制 write 的調用也可能需要持有其他引用。

但假如我們只想在對象實際被寫入之前 “改造” 一下傳入的item, 就沒必要實現ItemWriter和執行 write 操作: 我們只需要這個將被修改的item對象而已。 對於這種情況, Spring Batch提供了 ItemProcessor 接口:

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
        }
}

在上面的簡單示例中,有兩個類: Foo和Bar, 以及實現了ItemProcessor接口的FooProcessor類。因爲是demo,所以轉換很簡單, 在實際使用中可能執行轉換爲任何類型, 響應的操作請讀者根據需要自己編寫。 BarWriter將被用於寫出Bar對象,如果傳入其他類型的對象可能會拋出異常。 同樣,如果 FooProcessor 傳入的參數不是 Foo 也會拋出異常。FooProcessor可以注入到某個Step中:

<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


在很多情況下執行單個轉換就可以了, 但假如想要將多個 ItemProcessors "串聯(chain)" 在一起要怎麼實現呢? 我們可以使用前面提到的組合模式(composite pattern)來完成。 接着前面單一轉換的示例, 我們將Foo轉換爲Bar,然後再轉換爲Foobar類型,並執行寫出:

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
    }
}

可以將 FooProcessor 和 BarProcessor “串聯”在一起來生成 Foobar 對象,如果用 Java代碼表示,那就像下面這樣:

CompositeItemProcessor<Foo,Foobar> compositeProcessor = new CompositeItemProcessor<Foo,Foobar>();
List itemProcessors = new ArrayList();
itemProcessors.add(new FooTransformer());
itemProcessors.add(new BarTransformer());
compositeProcessor.setDelegates(itemProcessors);

就和前面的示例類似,複合處理器也可以配置到Step中:

<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


item processor 的典型應用就是在數據傳給ItemWriter之前進行過濾(filter out)。 過濾(Filtering)是一種有別於跳過(skipping)的行爲; skipping表明某幾行記錄是無效的,而 filtering 則只是表明某條記錄不應該寫入(written)。

例如, 某個批處理作業,從一個文件中讀取三種不同類型的記錄: 準備 insert 的記錄、準備 update 的記錄,需要 delete 的記錄。如果系統中不允許刪除記錄, 那麼我們肯定不希望將 “delete” 類型的記錄傳遞給 ItemWriter。 但因爲這些記錄又不是損壞的信息(bad records), 我們只想將其過濾掉,而不是跳過。 因此,ItemWriter只會收到 "insert" 和 "update"的記錄。

要過濾某條記錄, 只需要 ItemProcessor 返回“ null ” 即可. 框架將自動檢測結果爲“ null ”的情況, 不會將該item 添加到傳給ItemWriter的list中。 像往常一樣, 在 ItemProcessor 中拋出異常將會導致跳過(skip)。

1.3.3 容錯(Fault Tolerance)


當某一個分塊回滾時, 讀取後已被緩存的那些item可能會被重新處理。 如果一個step被配置爲支持容錯(通常使用 skip跳過 或retry重試處理),使用的所有 ItemProcessor 都應該實現爲冪等的(idempotent)。 通常ItemProcessor對已經處理過的輸入數據不執行任何修改, 而只更新需要處理的實例。

1.4 ItemStream


ItemReader 和 ItemWriter 都爲各自的目的服務, 但他們之間有一個共同點, 就是都需要與另一個接口配合。 一般來說,作爲批處理作業作用域範圍的一部分,readers 和 writers 都需要打開(open),關閉(close),並需要某種機制來持久化自身的狀態:

public interface ItemStream {

      void open(ExecutionContext executionContext) throws ItemStreamException;

      void update(ExecutionContext executionContext) throws ItemStreamException;

      void close() throws ItemStreamException;

}

在描述每種方法之前,我們應該提到ExecutionContext。ItemReader的客戶端也應該實現ItemStream,在任何 read 之前調用open以打開需要的文件或數據庫連接等資源。實現ItemWriter也有類似的限制/約束,即需要同時實現ItemStream。如之前所述,如果將數據存放在ExecutionContext中,那麼它可以在某個時刻用來啓動 ItemReader 或 ItemWriter,而不是在初始狀態時。對應的, 應該確保在調用open之後的適當位置調用 close 來安全地釋放所有分配的資源。調用update主要是爲了確保當前持有的所有狀態都被加載到所提供的 ExecutionContext中。 update 一般在提交之前調用,以確保當前狀態被持久化到數據庫之中。

在特殊情況下, ItemStream 的客戶端是一個Step(由 Spring Batch Core 決定), 會爲每個 StepExecution 創建一個ExecutionContext,以允許用戶存儲特定部分的執行狀態, 一般來說如果同一個JobInstance重啓了,則預期它將會在重啓後被返回。對於熟悉 Quartz的人來說, 邏輯上非常像是Quartz的JobDataMap。

1.5 委託模式(Delegate Pattern)與註冊Step


請注意, CompositeItemWriter是委託模式的一個示例, 這在Spring Batch中很常見的。 委託自身可以實現回調接口StepListener。如果實現了,那麼他們就會被當作Job中Step的一部分與 Spring Batch Core 結合使用, 然後他們基本上必定需要手動註冊到Step中。

一個 reader, writer, 或 processor,如果實現了 ItemStream / StepListener接口,就會被自動組裝到 Step 中。 但因爲delegates 並不爲 Step 所知, 因此需要被注入(作爲listeners監聽器或streams流,或兩者都可):

<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)


最常見的批量數據交換機制是使用純文本平面文件(flat file)。 XML由統一約定好的標準來定義文件結構(即XSD), 與XML等格式不同, 想要閱讀純文本平面文件必須先了解其組成結構。一般來說,純文本平面文件分兩種類型: 有分隔的類型(Delimited) 與固定長度類型(Fixed Length)。有分隔的文件中各個字段由分隔符進行間隔, 比如英文逗號(,)。而固定長度類型的文件每個字段都有固定的長度。

1.6.1 The FieldSet(字段集)


當在Spring Batch中使用純文本文件時, 不管是將其作爲輸入還是輸出, 最重要的一個類就是 FieldSet。許多架構和類庫會抽象出一些方法/類來輔助你從文件讀取數據, 但是這些方法通常返回 String 或者 String[] 數組, 很多時候這確實是些半成品。 而 FieldSet 是Spring Batch中專門用來將文件綁定到字段的抽象。它允許開發者和使用數據庫差不多的方式來使用數據輸入文件入。 FieldSet 在概念上非常類似於Jdbc的 ResultSet 。 FieldSet 只需要一個參數: 即token數組 String[] 。另外,您還可以配置字段的名稱, 然後就可以像使用 ResultSet 一樣, 使用 index 或者 name 都可以取得對應的值:

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);

在 FieldSet 接口可以返回很多類型的對象/數據, 如 Date , long , BigDecimal 等。 FieldSet 最大的優勢在於,它對文本輸入文件提供了統一的解析。 不是每個批處理作業採用不同的方式進行解析,而一直是一致的, 不論是在處理格式異常引起的錯誤,還是在進行簡單的數據轉換。

1.6.2 FlatFileItemReader


平面文件(flat file)是最多包含二維(表格)數據的任意類型的文件。在 Spring Batch 框架中 FlatFileItemReader 類負責讀取平面文件, 該類提供了用於讀取和解析平面文件的基本功能。FlatFileItemReader 主要依賴兩個東西: Resource 和LineMapper。LineMapper接口將在下一節詳細討論。 resource 屬性代表一個 Spring Core Resource(Spring核心資源)。關於如何創建這一類 bean 的文檔可以參考Spring框架, Chapter Resources。所以本文檔就不再深入講解創建 Resource 對象的細節。但可以找到一個文件系統資源的簡單示例,如下所示:

Resource resource = new FileSystemResource("resources/trades.csv");


在複雜的批處理環境中,目錄結構通常由EAI基礎設施管理, 並且會建立放置區(drop zones),讓外部接口將文件從ftp移動到批處理位置, 反之亦然。文件移動工具(File moving utilities)超出了spring batch架構的範疇, 但在批處理作業中包括文件移動步驟這種事情那也是很常見的。 批處理架構只需要知道如何定位需要處理的文件就足夠了。Spring Batch 將會從這個起始點開始,將數據傳輸給數據管道。當然, Spring Integration也提供了很多這一類的服務。

FlatFileItemReader 中的其他屬性讓你可以進一步指定數據如何解析:

FlatFileItemReader 的屬性(Properties):



LineMapper


就如同 RowMapper 在底層根據 ResultSet 構造一個 Object 並返回, 平面文件處理過程中也需要將一行 String 轉換並構造成Object:

public interface LineMapper<T> {

       T mapLine(String line, int lineNumber) throws Exception;
}

FlatFileItemReader


基本的約定是, 給定當前行以及和它關聯的行號(line number), mapper 應該能夠返回一個領域對象。這類似於在 RowMapper中每一行也有一個 line number 相關聯, 正如 ResultSet 中的每一行(Row)都有其綁定的 row number。這允許行號能被綁定到生成的領域對象以方便比較(identity comparison)或者更方便進行日誌記錄。

但與 RowMapper 不同的是, LineMapper 只能取得原始行的String值, 正如上面所說, 給你的是一個半成品。 這行文本值必須先被解析爲 FieldSet, 然後纔可以映射爲一個對象,如下所述。

LineTokenizer


對將每一行輸入轉換爲 FieldSet 這種操作的抽象是很有必要的, 因爲可能會有各種平面文件格式需要轉換爲 FieldSet。在Spring Batch中, 對應的接口是 LineTokenizer:

public interface LineTokenizer {

       FieldSet tokenize(String line);

}

使用 LineTokenizer 的約定是, 給定一行輸入內容(理論上 String 可以包含多行內容), 返回一個表示該行的 FieldSet 對象。這個FieldSet接着會傳遞給 FieldSetMapper。Spring Batch 包括以下LineTokenizer實現:

  • DelmitedLineTokenizer 適用於處理使用分隔符(delimiter)來分隔一條數據中各個字段的文件。最常見的分隔符是逗號(comma),但管道或分號也經常使用。
  • FixedLengthTokenizer 適用於記錄中的字段都是“固定寬度(fixed width)”的文件。每種記錄類型中,每個字段的寬度必須先定義。
  • PatternMatchingCompositeLineTokenizer 通過使用正則模式匹配,來決定對特定的某一行應該使用 LineTokenizers 列表中的哪一個來執行字段拆分。

FieldSetMapper


FieldSetMapper 接口只定義了一個方法, mapFieldSet , 這個方法接收一個 FieldSet 對象,並將其內容映射到一個 object中。根據作業需要, 這個對象可以是自定義的 DTO , 領域對象, 或者是簡單數組。FieldSetMapper 與 LineTokenizer 結合使用以將資源文件中的一行數據轉化爲所需類型的對象:

public interface FieldSetMapper<T> {

      T mapFieldSet(FieldSet fieldSet);

}

這和JdbcTemplate中的RowMapper是一樣的道理。


DefaultLineMapper


既然讀取平面文件的接口已經定義好了,那很明顯我們需要執行以下三個步驟:

  1. 從文件中讀取一行。
  2. 將讀取的字符串傳給 LineTokenizer#tokenize() 方法,以獲取一個 FieldSet。
  3. 將解析後的 FieldSet 傳給 FieldSetMapper ,然後將 ItemReader#read() 方法執行的結果返回給調用者。

上面的兩個接口代表了兩個不同的任務: 將一行文本轉換爲 FieldSet, 以及把 FieldSet 映射爲一個領域對象。 因爲LineTokenizer 的輸入對應着 LineMapper 的輸入(一行), 並且 FieldSetMapper 的輸出對應着 LineMapper 的輸出, 所以SpringBatch 提供了一個使用LineTokenizer和FieldSetMapper的默認實現。DefaultLineMapper 就是大多數情況下用戶所需要的:

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;
      }
}

上面的功能由一個默認實現類來提供,而不是 reader 本身內置的(以前版本的框架這樣幹), 讓用戶可以更靈活地控制解析過程,特別是需要訪問原始行的時候。

文件分隔符讀取簡單示例


下面的例子用來說明一個實際的領域情景。這個批處理作業將從如下文件中讀取 football player(足球運動員) 信息:

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"

該文件的內容將被映射爲領域對象 Player:

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...
}

爲了將FieldSet映射爲Player對象, 需要定義一個FieldSetMapper,返回player對象:

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調用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();

每調用一次read方法,都會讀取文件中的一行,並返回一個新的Player對象。如果到達文件結尾,則會返回null。

根據Name映射 Fields


有一個額外的功能, DelimitedLineTokenizer 和 FixedLengthTokenizer 都支持,在功能上類似於 Jdbc 的 ResultSet。字段的名稱可以注入到這些 LineTokenizer 實現以提高映射函數的讀取能力。首先, 平面文件中所有字段的列名會注入給tokenizer:

tokenizer.setNames(new String[] {"ID", "lastName","firstName","position","birthYear","debutYear"});

FieldSetMapper 可以像下面這樣使用此信息:

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


很多時候, 創建一個 FieldSetMapper 就跟 JdbcTemplate 裏編寫 RowMapper 一樣繁瑣。Spring Batch通過使用JavaBean規範,提供了一個 FieldSetMapper 來自動將字段映射到對應setter的屬性域。還是使用足球的例子,
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" />

對於 FieldSet 中的每個條目(entry), mapper都會在Player對象的新實例中查找相應的setter (因此,需要指定 prototype scope),和 Spring容器 查找 setter匹配屬性名是一樣的方式。FieldSet 中每個可用的字段都會被映射, 然後返回組裝好的 Player 對象,不需要再手寫代碼。

Fixed Length File Formats


到這一步,我們討論了帶分隔符的文件, 但實際應用中可能只有一半左右是這種文件。還有很多機構使用固定長度形式的平面文件。固定長度文件的示例如下:

UK21341EAH4121131.11customer1
UK21341EAH4221232.11customer2
UK21341EAH4321333.11customer3
UK21341EAH4421434.11customer4
UK21341EAH4521535.11customer5

雖然看起來像是一個很長的字段,但實際上代表了4個分開的字段:

  1. ISIN : 唯一標識符,訂購的商品編碼 - 佔12字符。
  2. Quantity : 訂購的商品數量 - 佔3字符。
  3. Price : 商品的價格 - 佔5字符。
  4. Customer : 訂購商品的顧客Id - 佔9字符。
配置好 FixedLengthLineTokenizer 以後, 每個字段的長度必須用範圍(range)的形式指定:

<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>

因爲 FixedLengthLineTokenizer 使用的也是 LineTokenizer 接口, 所以返回值同樣是 FieldSet, 和使用分隔符基本上是一樣的。這也就可以使用同樣的方式來處理其輸出, 例如使用 BeanWrapperFieldSetMapper。

注意:
要支持上面這種範圍式的語法需要使用專門的屬性編輯器: RangeArrayPropertyEditor , 可以在ApplicationContext 中配置。當然,這個 bean 在批處理命名空間中的 ApplicationContext 裏已經自動聲明瞭。

單文件中含有多種類型數據的處理


前面所有的文件讀取示例,爲簡單起見都做了一個關鍵性假設: 在同一個文件中的所有記錄都具有相同的格式。但情況有時候並非如此。其實在一個文件包含不同的格式的記錄是很常見的,需要使用不同的拆分方式,映射到不同的對象中。下面是一個文件中的片段,僅作演示:

USER;Smith;Peter;;T;20014539;F
LINEA;1044391041ABC037.49G201XX1383.12H
LINEB;2134776319DEF422.99M005LI

這個文件中有三種類型的記錄, "USER", "LINEA", 以及 "LINEB"。 一行 "USER" 對應一個 User 對象。 "LINEA" 和 "LINEB"對應的都是 Line 對象, 只是 "LINEA" 包含的信息比“LINEB”要多。

ItemReader分別讀取每一行, 當然我們必須指定不同的 LineTokenizer 和 FieldSetMapper 以便ItemWriter 能獲得到正確的item。 PatternMatchingCompositeLineMapper 就是專門拿來幹這個事的, 可以通過模式映射到對應的 LineTokenizer 和FieldSetMapper:

<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>

在這個示例中, "LINEA" 和 "LINEB" 使用獨立的 LineTokenizer,但使用同一個 FieldSetMapper.

PatternMatchingCompositeLineMapper 使用 PatternMatcher 的 match 方法來爲每一行選擇正確的代理(delegate)。PatternMatcher 支持兩個有特殊的意義通配符(wildcard): 問號(“ ? ”, question mark) 將匹配 1 個字符(注意不是0-1次), 而星號(“ * ”,asterisk)將匹配 0 到多個 字符。

請注意,在上面的配置中,所有以星號結尾的 pattern , 使他們變成了行的有效前綴。 PatternMatcher 總是匹配最具體的可能模式, 而不是按配置的順序從上往下來。所以如果 " LINE* " 和 " LINEA* " 都配置爲 pattern, 那麼 " LINEA " 將會匹配到" LINEA* ", 而 " LINEB " 將匹配到 " LINE* "。此外,單個星號(“ * ”)可以作爲默認匹配所有行的模式,如果該行不匹配其他任何模式的話。

<entry key="*" value-ref="defaultLineTokenizer" />

還有一個 PatternMatchingCompositeLineTokenizer 可用來單獨解析。

Flat File 的異常處理


在解析一行時, 可能有很多情況會導致異常被拋出。很多平面文件不是很完整, 或者裏面的某些記錄格式不正確。許多用戶會選擇忽略這些錯誤的行, 只將這個問題記錄到日誌, 比如原始行,行號。稍後可以人工審查這些日誌,也可以由另一個批處理作業來檢查。出於這個原因,Spring Batch提供了一系列的異常類: FlatFileParseException ,和 FlatFileFormatException 。

FlatFileParseException 是由 FlatFileItemReader 在讀取文件時解析錯誤而拋出的。 FlatFileFormatException 是由實現了LineTokenizer 接口的類拋出的, 表明在拆分字段時發生了一個更具體的錯誤。

IncorrectTokenCountException

 
DelimitedLineTokenizer 和 FixedLengthLineTokenizer 都可以指定列名(column name), 用來創建一個FieldSet。但如果column name 的數量和 拆分時找到的列數目, 則不會創建 FieldSet,只會拋出 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());
}

因爲 tokenizer 配置了4列的名稱,但在這個文件中只找到 3 個字段, 所以會拋出 IncorrectTokenCountException 異常。

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());
}

上面配置的範圍是: 1-5 , 6-10 , 以及 11-15 , 因此預期的總長度是15。但在這裏傳入的行的長度是 5 ,所以會導致IncorrectLineLengthException 異常。之所以直接拋出異常, 而不是先去映射第一個字段的原因是爲了更早發現處理失敗, 而不再調用 FieldSetMapper 來讀取第2列。但是呢,有些情況下, 行的長度並不總是固定的。 出於這個原因, 可以通過設置'strict' 屬性的值,不驗證行的寬度:

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.setStrict(false) 。這個設置告訴 tokenizer 在對一行進行解析(tokenizing)時不要去管(enforce)行的長度。然後就正確地創建了一個 FieldSet並返回。當然,剩下的值就只會包含空的token值。

1.6.3 FlatFileItemWriter


將數據寫入到純文本文件也必須解決和讀取文件時一樣的問題。 在事務中,一個 step 必須通過分隔符或採用固定長度的格式將數據寫出去.

LineAggregator


與 LineTokenizer 接口的處理方式類似, 寫入文件時也需要有某種方式將一條記錄的多個字段組織拼接成單個 String,然後再將string寫入文件. Spring Batch 對應的接口是 LineAggregator :

public interface LineAggregator<T> {

       public String aggregate(T item);

}

接口 LineAggregator 與 LineTokenizer 相互對應. LineTokenizer 接收 String ,處理後返回一個 FieldSet 對象, 而LineAggregator 則是接收一條記錄,返回對應的 String.

PassThroughLineAggregator


LineAggregator 接口最基礎的實現類是 PassThroughLineAggregator , 這個簡單實現僅僅是將接收到的對象調用 toString() 方法的值返回:

public class PassThroughLineAggregator<T> implements LineAggregator<T> {

     public String aggregate(T item) {
         return item.toString();
     }

}

上面的實現對於需要直接轉換爲string的時候是很管用的,但是 FlatFileItemWriter 的一些優勢也是很有必要的,比如 事務,以及支持重啓特性等.


簡單的文件寫入示例


既然已經有了 LineAggregator 接口以及其最基礎的實現, PassThroughLineAggregator, 那就可以解釋基礎的寫出流程了:

  1. 將要寫出的對象傳遞給 LineAggregator 以獲取一個字符串(String).
  2.  將返回的 String 寫入配置指定的文件中.

下面是 FlatFileItemWriter 中對應的代碼:

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


上面的示例可以應對最基本的文件寫入情景。但使用 FlatFileItemWriter 時可能更多地是需要將某個領域對象寫到文件,因此必須轉換到單行之中。 在讀取文件時,有以下步驟:

  1. 從文件中讀取一行.
  2. 將這一行字符串傳遞給 LineTokenizer#tokenize() 方法, 以獲取 FieldSet 對象
  3. 將分詞器返回的 FieldSet 傳給一個 FieldSetMapper 映射器, 然後將 ItemReader#read() 方法得到的結果 return。

文件的寫入也很類似, 但步驟正好相反:

  1. 將要寫入的對象傳遞給 writer
  2. 將領域對象的屬性域轉換爲數組
  3. 將結果數組合並(aggregate)爲一行字符串

因爲框架沒辦法知道需要將領域對象的哪些字段寫入到文件中,所以就需要有一個 FieldExtractor 來將對象轉換爲數組:

public interface FieldExtractor<T> {
    Object[] extract(T item);
}

FieldExtractor 的實現類應該根據傳入對象的屬性創建一個數組, 稍後使用分隔符將各個元素寫入文件,或者作爲 field-width line 的一部分.


PassThroughFieldExtractor


在很多時候需要將一個集合(如 array、Collection, FieldSet等)寫出到文件。 從集合中“提取”一個數組那真的是非常簡單: 直接進行簡單轉換即可。 因此在這種場合PassThroughFieldExtractor 就派上用場了。應該注意,如果傳入的對象不是集合類型的,那麼 PassThroughFieldExtractor 將返回一個數組, 其中只包含提取的單個對象。

BeanWrapperFieldExtractor


與文件讀取一節中所描述的 BeanWrapperFieldSetMapper 一樣, 通常使用配置來指定如何將領域對象轉換爲一個對象數組是比較好的辦法, 而不用自己寫個方法來進行轉換。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)寫入示例


最基礎的平面文件格式是將所有字段用分隔符(delimiter)來進行分隔(separated)。這可以通過 DelimitedLineAggregator 來完成。下面的例子把一個表示客戶信用額度的領域對象寫出:

public class CustomerCredit {
     private int id;
     private String name;
     private BigDecimal credit;
     //getters and setters removed for clarity
}

因爲使用到了領域對象,所以必須提供 FieldExtractor 接口的實現,當然也少不了要使用的分隔符:

<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)文件寫入示例


平面文件的格式並不是只有採用分隔符這種類型。許多人喜歡對每個字段設置一定的寬度,這樣就能區分各個字段了,這種做法通常被稱爲“固定寬度, fixed width”。 Spring Batch 通過 FormatterLineAggregator 支持這種文件的寫入。使用上面描述的CustomerCredit 領域對象, 則可以對它進行如下配置:

<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>

上面的示例大部分看起來是一樣的, 只有 format 屬性的值不同:

<property name="format" value="%-9s%-2.0f" />

底層實現採用 Java 5 提供的 Formatter 。Java的 Formatter (格式化) 基於C語言的 printf 函數功能。關於如何配置formatter 請參考 Formatter 的javadoc.

處理文件創建(Handling File Creation)


FlatFileItemReader 與文件資源的關係很簡單。在初始化 reader 時,如果文件存在則打開, 如果文件不存在那就拋出一個異常
(exception)。

但是文件的寫入就沒那麼簡單了。乍一看可能會覺得跟 FlatFileItemWriter 一樣簡單直接粗暴: 如果文件存在則拋出異常, 如果
不存在則創建文件並開始寫入。

但是, 作業的重啓有可能會有BUG。 在正常的重啓情景中, 約定與前面所想的恰恰相反: 如果文件存在, 則從已知的最後一個
正確位置開始寫入, 如果不存在, 則拋出異常。

如果此作業(Job)的文件名每次都是一樣的那怎麼辦? 這時候可能需要刪除已存在的文件(重啓則不刪除)。 因爲有這些可能性,
FlatFileItemWriter 有一個屬性 shouldDeleteIfExists 。將這個屬性設置爲 true , 打開 writer 時會將已有的同名文件刪除。


1.7 XML Item Readers and Writers


Spring Batch爲讀取XML映射爲Java對象以及將Java對象寫爲XML記錄提供了事務基礎。

[注意]XML流的限制 StAX API 被用在其他XML解析引擎不適合批處理請求 I/O 時的情況(DOM方式把整個輸入文件加載到內存中, 而SAX方式在解析過程中需要用戶提供回調)。

讓我們仔細看看在Spring Batch中 XML輸入和輸出是如何運行的。 首先,有一些不同於文件讀取和寫入的概念,但在Spring Batch XML處理中是很常見的。在處理XML時, 並不像讀取文本文件(FieldSets)時採取分隔符標記逐行讀取的方式, 而是假定XML資源是對應於單條記錄的文檔片段(' fragments ')的集合:



圖 3.1: XML 輸入文件

“ trade ”標籤在上面的場景中是根元素“root element”。 在' <trade> '和' </trade> '之間的一切都被認爲是一個 文檔片段' fragment '。 Spring Batch使用 Object/XML映射(OXM)將 fragments 綁定到對象。 但 Spring Batch 並不依賴某個特定的XML綁定技術。 Spring OXM 委託是最典型的用途, 其爲常見的OXM技術提供了統一的抽象。 Spring OXM 依賴是可選的, 如有必要,你也可以自己實現 Spring Batch 的某些接口。 OXM支持的技術間的關係如下圖所示:



圖 3.2: OXM Binding

上面介紹了OXM以及如何使用XML片段來表示記錄, 接着讓我們仔細瞭解下 readers 和 writers 。

1.7.1 StaxEventItemReader


StaxEventItemReader 提供了從XML輸入流進行記錄處理的典型設置。 首先,我們來看一下 StaxEventItemReader能處理的一組XML記錄。

<?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>

能被處理的XML記錄需要滿足下列條件:

  • 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>

請注意,在上面的例子中,我們選用一個 XStreamMarshaller, 裏面接受一個id爲 aliases 的 map, 將首個entry的 key 值作爲文檔片段的name(即根元素), 將 value 作爲綁定的對象類型。類似於FieldSet, 後面的其他元素映射爲對象內部的字段名/值對。在配置文件中,我們可以像下面這樣使用Spring配置工具來描述所需的alias:

<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對象。

總之,這個過程類似於下面的Java代碼,其中配置了 Spring的注入功能:

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


輸出與輸入相對應. StaxEventItemWriter 需要 1個 Resource , 1個 marshaller 以及 1個 rootTagName . Java對象傳遞給marshaller(通常是標準的Spring OXM marshaller), marshaller 使用自定義的事件writer寫入Resource, 並過濾由OXM工具爲每條 fragment 產生的 StartDocument 和 EndDocument事件。我們用 MarshallingEventWriterSerializer 示例來顯示這一點。Spring配置如下所示:

<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>

上面配置了3個必需的屬性,以及1個可選屬性 overwriteOutput = true , (本章前面提到過) 用來指定一個已存在的文件是否可以被覆蓋。應該注意的是, writer 使用的 marshaller 和前面講的 reading 示例中是完全相同的:

<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>

我們用一段Java代碼來總結所討論的知識點, 並演示如何通過代碼手動設置所需的屬性:

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 多個數據輸入文件


在單個 Step 中處理多個輸入文件是很常見的需求。如果這些文件都有相同的格式, 則可以使用 MultiResourceItemReader來進行處理(支持 XML/或 純文本文件)。 假如某個目錄下有如下3個文件:

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>

delegate 引用的是一個簡單的 FlatFileItemReader。上面的配置將會從兩個輸入文件中讀取數據,處理回滾以及重啓場景。應該注意的是,所有 ItemReader 在添加額外的輸入文件後(如本示例),如果重新啓動則可能會導致某些潛在的問題。 官方建議是每個批作業處理獨立的目錄,一直到成功完成爲止。


1.9 數據庫(Database)


和大部分企業應用一樣,數據庫也是批處理系統存儲數據的核心機制。 但批處理與其他應用的不同之處在於,批處理系統一般都運行於大規模數據集基礎上。 如果一條SQL語句返回100萬行, 則結果集可能全部存放在內存中m直到所有行全部讀完。Spring Batch提供了兩種類型的解決方案來處理這個問題: 遊標(Cursor) 和 可分頁的數據庫ItemReaders.

1.9.1 基於Cursor的ItemReaders


使用遊標(cursor)是大多數批處理開發人員默認採用的方法, 因爲它是處理有關係的數據“流”在數據庫級別的解決方案。Java的 ResultSet 類其本質就是用面向對象的遊標處理機制。 ResultSet 維護着一個指向當前數據行的cursor。調用 ResultSet的 next 方法則將遊標移到下一行。

Spring Batch 基於 cursor 的 ItemReaders 在初始化時打開遊標, 每次調用 read 時則將遊標向前移動一行, 返回一個可用於進行處理的映射對象。最好將會調用 close 方法, 以確保所有資源都被釋放。

Spring 的 JdbcTemplate 的解決辦法, 是通過回調模式將 ResultSet 中所有行映射之後,在返回調用方法前關閉結果集來處理的。

但是,在批處理的時候就不一樣了, 必須得等 step 執行完成才能調用close。下圖描繪了基於遊標的ItemReader是如何處理的,使用的SQL語句非常簡單, 而且都是類似的實現方式:





這個例子演示了基本的處理模式。 數據庫中有一個 “ FOO ” 表,它有三個字段: ID , NAME , 以及 BAR , select 查詢所有ID大於1但小於7的行。這樣的話遊標起始於 ID 爲 2的行(第1行)。這一行的結果會被映射爲一個Foo對象。再次調用read()則將光標移動到下一行, 也就是ID爲3的Foo。 在所有行讀取完畢之後這些結果將會被寫出去, 然後這些對象就會被垃圾回收(假設沒有其他引用指向他們)。

JdbcCursorItemReader


JdbcCursorItemReader 是基於 cursor 的Jdbc實現。它直接使用ResultSet,需要從數據庫連接池中獲取連接來執行SQL語句。我們的示例使用下面的數據庫表:

CREATE TABLE CUSTOMER (
   ID BIGINT IDENTITY PRIMARY KEY,
   NAME VARCHAR(45),
   CREDIT FLOAT
);

我們一般使用領域對象來對應到每一行, 所以用 RowMapper 接口的實現來映射 CustomerCredit 對象:

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;
       }
}

一般來說Spring的用戶對 JdbcTemplate 都不陌生,而 JdbcCursorItemReader 使用其作爲關鍵API接口, 我們一起來學習如何通過 JdbcTemplate 讀取這一數據, 看看它與 ItemReader 有何區別。 爲了演示方便, 我們假設CUSTOMER表有1000行數據。第一個例子將使用 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());

當執行完上面的代碼, customerCredits 這個 List 中將包含 1000 個 CustomerCredit 對象。 在 query 方法中, 先從DataSource 獲取一個連接, 然後用來執行給定的SQL, 獲取結果後對 ResultSet 中的每一行調用一次 mapRow 方法。 讓我們來對比一下 JdbcCursorItemReader 的實現:

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);

運行這段代碼後 counter 的值將變成 1000。如果上面的代碼將返回的 customerCredit 放入 List, 則結果將和使用JdbcTemplate 的例子完全一致。 但是呢, 使用 ItemReader 的強大優勢在於, 它允許數據項變成 “流式(streamed)”。 調用一次 read 方法, 通過ItemWriter寫出數據對象, 然後再通過 read 獲取下一項。 這使得 item 讀取和寫出可以進行 “分塊(chunks)”, 並且週期性地提交, 這纔是高性能批處理的本質。此外,它可以很容易地通過配置注入到某個 Spring Batch Step中:

<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>

因爲在Java中有很多種不同的方式來打開遊標, 所以 JdbcCustorItemReader 有許多可以設置的屬性 :



HibernateCursorItemReader


使用 Spring 的程序員需要作出一個重要的決策,即是否使用ORM解決方案,這決定了是否使用 JdbcTemplate 或
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);

這裏配置的 ItemReader 將以完全相同的方式返回CustomerCredit對象,和 JdbcCursorItemReader 沒有區別, 如果
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


有時候使用存儲過程來獲取遊標數據是很有必要的。 StoredProcedureItemReader 和 JdbcCursorItemReader 其實差不多,只是不再執行一個查詢來獲取遊標,而是執行一個存儲過程, 由存儲過程返回一個遊標。 存儲過程有三種返回遊標的方式:

  1. 作爲一個 ResultSet 返回(SQL Server, Sybase, DB2, Derby 以及 MySQL支持)
  2. 作爲一個 out 參數返回 ref-cursor (Oracle和PostgreSQL使用這種方式)
  3. 作爲存儲函數(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>

這個例子依賴於存儲過程提供一個 ResultSet 作爲返回結果(方式1)。

如果存儲過程返回一個ref-cursor(方式2),那麼我們就需要提供返回的ref-cursor(out 參數)的位置。下面的示例中,第一個參數是返回的ref-cursor:

<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>

如果存儲函數的返回值是一個遊標(方式 3), 則需要將 function 屬性設置爲 true , 默認爲 false 。如下面所示:

<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>

在所有情況下,我們都需要定義 RowMapper 以及 DataSource, 還有存儲過程的名字。

如果存儲過程/函數需要傳入參數, 那麼必須聲明並通過 parameters 屬性來設置值。下面是一個關於 Oracle 的示例, 其中聲明瞭三個參數。 第一個是 out 參數,用來返回 ref-cursor, 第二第三個參數是 in 型參數, 類型都是 INTEGER :

<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>

除了參數聲明, 我們還需要指定一個 PreparedStatementSetter 實現來設置參數值。這和上面的 JdbcCursorItemReader 一樣。

1.9.2 可分頁的 ItemReader


另一種是使用數據庫遊標執行多次查詢,每次查詢只返回一部分結果。 我們將這一部分稱爲一頁(a page)。 分頁時每次查詢必須指定想要這一頁的起始行號和想要返回的行數。

JdbcPagingItemReader


分頁 ItemReader 的一個實現是 JdbcPagingItemReader 。 JdbcPagingItemReader 需要一個 PagingQueryProvider 來負責提供獲取每一頁所需的查詢SQL。由於每個數據庫都有不同的分頁策略, 所以我們需要爲各種數據庫使用對應的PagingQueryProvider 。 也有自動檢測所使用數據庫類型的 SqlPagingQueryProviderFactoryBean ,會根據數據庫類型選用適當的 PagingQueryProvider 實現。 這簡化了配置,同時也是推薦的最佳實踐。

SqlPagingQueryProviderFactoryBean 需要指定一個 select 子句以及一個 from 子句(clause). 當然還可以選擇提供 where子句. 這些子句加上所需的排序列 sortKey 被組合成爲一個 SQL 語句(statement).

在 reader 被打開以後, 每次調用 read 方法則返回一個 item,和其他的 ItemReader一樣. 使用分頁是因爲可能需要額外的行.

下面是一個類似 'customer credit' 示例的例子,使用上面提到的基於 cursor的ItemReaders:

<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>

這裏配置的ItemReader將返回CustomerCredit對象, 必須指定使用的RowMapper。 ' pageSize '屬性決定了每次數據庫查詢返回的實體數量。

' parameterValues '屬性可用來爲查詢指定參數映射map。如果在where子句中使用了命名參數,那麼這些entry的key應該和命名參數一一對應。如果使用傳統的 '?' 佔位符, 則每個entry的key就應該是佔位符的數字編號,和JDBC佔位符一樣索引都是從1開始。


JpaPagingItemReader


另一個分頁ItemReader的實現是 JpaPagingItemReader 。JPA沒有 Hibernate 中StatelessSession 之類的概念,所以我們必須使用JPA規範提供的其他功能。因爲JPA支持分頁,所以在使用JPA來處理分頁時這是一種很自然的選擇。讀取每頁後, 實體將會分離而且持久化上下文將會被清除,以允許在頁面處理完成後實體會被垃圾回收。

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>

這裏配置的ItemReader和前面所說的 JdbcPagingItemReader 返回一樣的 CustomerCredit對象, 假設 Customer 對象有正確的JPA註解或者ORM映射文件。 ' pageSize ' 屬性決定了每次查詢時讀取的實體數量。


IbatisPagingItemReader


如果使用 IBATIS/MyBatis, 則可以使用 IbatisPagingItemReader, 顧名思義, 也是一種實現分頁的ItemReader。IBATIS不對分頁提供直接支持, 但通過提供一些標準變量就可以爲IBATIS查詢提供分頁支持。

下面是和上面的示例同樣功能的配置,使用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>

上述 IbatisPagingItemReader 配置引用了一個IBATIS查詢,名爲“getPagedCustomerCredits”。如果使用MySQL,那麼查詢XML應該類似於下面這樣。

<select id="getPagedCustomerCredits" resultMap="customerCreditResult">
select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize#
</select>

_skiprows 和 _pagesize 變量都是 IbatisPagingItemReader 提供的,還有一個 _page 變量,需要時也可以使用。分頁查詢的語法根據數據庫不同使用。下面是使用Oracle的一個例子(但我們需要使用CDATA來包裝某些特殊符號,因爲是放在XML文檔中嘛):

<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


雖然文本文件和XML都有自己特定的 ItemWriter, 但數據庫和他們並不一樣。這是因爲事務提供了所需的全部功能。 對於文件來說 ItemWriters 是必要的, 因爲如果需要事務特性,他們必須充當這種角色, 跟蹤輸出的 item,並在適當的時間flushing/clearing。使用數據庫時不需要這個功能,因爲寫已經包含在事務之中。 用戶可以自己創建實現ItemWriter接口的DAO, 或使用一個處理常見問題的自定義ItemWriter,無論哪種方式,都不會有任何問題。 需要注意的一件事是批量輸出時的性能和錯誤處理能力。 在使用hibernate作爲ItemWriter 時是最常見的, 但在使用Jdbc batch 模式時可能也會存在同樣的問題。批處理數據庫輸出沒有任何固有的缺陷,如果我們注意 flush 並且數據沒有錯誤的話。 但是,在寫出時如果發生了什麼錯誤,就可能會引起混亂,因爲沒有辦法知道是哪個item引起的異常, 甚至是否某個單獨的 item 負有責任,如下圖所示:



如果 items 在輸出之前有緩衝, 則遇到任何錯誤將不會立刻拋出, 直到緩衝區刷新之後,提交之前纔會拋出。例如, 我們假設每一塊寫出20個item, 第15個 item 會拋出 DataIntegrityViolationException 。如果與 Step 有關, 則20項數據都會寫入成功, 因爲沒有辦法知道會出現錯誤,直到全部寫入完成。一旦調用 Session#flush(), 就會清空緩衝區buffer, 而異常也將被放出來。在這一點上, Step無能爲力, 事務也必須回滾。 通常, 異常會導致 item 被跳過(取決於 skip/retry 策略), 然後該item就不會被輸出。 然而,在批處理的情況下, 是沒有辦法知道到底是哪一項引起的問題, 在錯誤發生時整個緩衝區都將被寫出。解決這個問題的唯一方法就是在每一個 item之後 flush一下:



這種用法是很常見的, 尤其是在使用Hibernate時,ItemWriter的簡單實現建議, 在每次調用 write() 之後執行 flush。這樣做可以讓跳過 items 變得可靠, 而Spring Batch 在錯誤發生後會在內部關注適當粒度的ItemWriter調用。


1.10 重用已存在的 Service


批處理系統通常是與其他應用程序相結合的方式使用。最常見的是與一個在線應用系統結合, 但也支持與瘦客戶端集成,通過移動每個程序所使用的批量數據。由於這些原因,所以很多用戶想要在批處理作業中重用現有的DAO或其他服務。Spring容器通過注入一些必要的類就可以實現這些重用。但可能需要現有的服務作爲 ItemReader 或者 ItemWriter, 也可以適配另一個Spring Batch類, 或其本身就是一個 step 主要的ItemReader。爲每個需要包裝的服務編寫一個適配器類是很簡單的, 而因爲這是很普遍的需求,所以 Spring Batch 提供了實現: ItemReaderAdapter 和 ItemWriterAdapter 。兩個類都實現了標準的Spring方法委託模式調用,設置也相當簡單。下面是一個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" />

特別需要注意的是, 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 輸入校驗


在本章中, 已經討論了很多種用來解析 input 的方法。 如果格式不對,那這些基本的實現都是拋出異常。 如果數據丟失一部分,FixedLengthTokenizer 也會拋出異常。 同樣, 使用 FieldSetMapper 時,如果讀取超出 RowMapper 索引範圍的值,又或者返回值類型不匹配,都會拋出異常。 所有的異常都會在 read 返回之前拋出。 然而, 他們不能確定返回的item是否是合法的。 例如, 如果其中一個字段是 age , 很顯然不能是負數。 解析爲數字是沒問題的, 因爲確實存在這個數, 所以就不會拋出異常。 因爲當下已經有大量的第三方驗證框架, 所以 Spring Batch 並不提供另一個驗證框架, 而是提供了一個非常簡單的接口, 其他框架可以實現這個接口來提供兼容:

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 it is valid.Spring Batch provides an out of the box ItemProcessor:

約定是如果對象無效則 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>

這個示例展示了一個簡單的 ValangValidator, 用來校驗 order 對象。 這樣寫目的是爲了儘可能多地演示如何使用 Valang 來添加校驗程序。

1.12 不保存執行狀態


默認情況下,所有 ItemReader 和 ItemWriter 在提交之前都會把當前狀態信息保存到 ExecutionContext 中。 但有時我們又不希望保存這些信息。 例如,許多開發者使用處理指示器(process indicator)讓數據庫讀取程序 '可重複運行(rerunnable)'。 在數據表中添加一個附加列來標識該記錄是否已被處理。 當某條記錄被讀取/寫入時,就將標誌位從 false 變爲 true , 然後只要在SQL語句的where子句中包含一個附加條件, 如 " where PROCESSED_IND = false ", 就可確保在任務重啓後只查詢到未處理過的記錄。 這種情況下,就不需要保存任何狀態信息, 比如當前 row number 什麼的, 因爲在重啓後這些信息都沒用了。 基於這種考慮, 所有的 readers 和 writers 都含有一個 saveState 屬性:

<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>

上面配置的這個 ItemReader 在任何情況下都不會將 entries(狀態信息)存放到 ExecutionContext 中.

1.13 創建自定義 ItemReaders 與 ItemWriters


到目前爲止,本章已將 Spring Batch 中基本的讀取(reading)和寫入(writing)概念講完, 還對一些常用的實現進行了討論。然而,這些都是相當普通的, 還有很多潛在的場景可能沒有現成的實現。本節將通過一個簡單的例子,來演示如何創建自定義的 ItemReader 和 ItemWriter ,並且如何正確地實現和使用。 ItemReader 同時也將 ItemStream , 以說明如何讓reader(讀取器)或writer(寫入器)支持重啓(restartable)。

1.13.1 自定義 ItemReader 示例


爲了實現這個目的,我們實現一個簡單的 ItemReader , 從給定的list中讀取數據。 我們將實現最基本的 ItemReader 功能, read:

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;
       }
}

這是一個簡單的類, 傳入一個 items list, 每次讀取時刪除其中的一條並返回。 如果list裏面沒有內容,則將返回null, 從而滿足ItemReader 的基本要求, 測試代碼如下所示:

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 支持重啓


現在剩下的問題就是讓 ItemReader 變爲可重啓的。到目前這一步,如果發生掉電之類的情況,那麼必須重新啓動ItemReader,而且是從頭開始。在很多時候這是允許的,但有時侯更好的處理辦法是讓批處理作業在上次中斷的地方重新開始。判斷的關鍵是根據 reader 是有狀態的還是無狀態的。 無狀態的 reader 不需要考慮重啓的情況, 但有狀態的則需要根據其最後一個已知的狀態來重新啓動。出於這些原因, 官方建議儘可能地讓 reader 成爲無狀態的,使開發者不需要考慮重新啓動的情況。

如果需要保存狀態信息,那應該使用 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 {}
}

每次調用 ItemStream 的 update 方法時, ItemReader 的當前 index 都會被保存到給定的 ExecutionContext 中,key 爲' current.index '。 當調用 ItemStream 的 open 方法時, ExecutionContext會檢查是否包含該 key 對應的條目。 如果找到key, 那麼當前索引 index 就好移動到該位置。這是一個相當簡單的例子,但它仍然符合通用原則:

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());


大多數ItemReaders具有更加複雜的重啓邏輯。 例如 JdbcCursorItemReader , 存儲了遊標(Cursor)中最後所處理的行的row id。

還值得注意的是 ExecutionContext 中使用的 key 不應該過於簡單。這是因爲 ExecutionContext 被一個 Step 中的所有ItemStreams 共用。在大多數情況下,使用類名加上 key 的方式應該就足以保證唯一性。然而,在極端情況下, 同一個類的多個ItemStream 被用在同一個Step中時( 如需要輸出兩個文件的情況),就需要更加具備唯一性的name標識。出於這個原因,SpringBatch 的許多 ItemReader 和 ItemWriter 實現都有一個 setName() 方法, 允許覆蓋默認的 key name。


1.13.2 自定義 ItemWriter 示例


自定義實現 ItemWriter 和上一小節所講的 ItemReader 有很多方面是類似, 但也有足夠多的不同之處。 但增加可重啓特性在本質上是一樣的, 所以本節的示例就不再討論這一點。和 ItemReader 示例一樣, 爲了簡單我們使用的參數也是 List :

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 支持重新啓動,我們將會使用和 ItemReader 相同的過程, 實現並添加 ItemStream 接口來同步 execution context。 在示例子中我們可能要記錄處理過的items數量,並添加爲到 footer 記錄。 我們可以在 ItemWriter 的實現類中同時實現 ItemStream , 以便在 stream 重新打開時從執行上下文中取回原來的數據重建計數器。

實際開發中, 如果自定義 ItemWriter restartable(支持重啓),則會委託另一個 writer(例如, 在寫入文件時), 否則會寫入到關係型數據庫(支持事務的資源)中, 此時 ItemWriter 不需要 restartable特性,因爲自身是無狀態的。 如果你的 writer 有狀態, 則應該實現2個接口: ItemStream 和 ItemWriter 。 請記住, writer客戶端需要知道 ItemStream 的存在, 所以需要在 xml 配置文件中將其註冊爲 stream.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章