Spring Batch複合條目閱讀器教程

banq發表於2025-02-25

在本文中,我們學習瞭如何實現和測試CompositeItemReader,它允許我們按特定順序處理來自多個來源的資料。透過將讀取器連結在一起,我們可以按特定順序處理來自檔案、資料庫或其他來源的資料。

Spring Batch中,CompositeItemReader是一種將多個ItemReader例項組合成單個讀取器的工具。當我們需要從多個來源或按特定順序讀取資料時,這特別有用。例如,我們可能希望同時從資料庫和檔案中讀取記錄,或者按特定順序處理來自兩個不同表的資料。

CompositeItemReader簡化了批處理作業中多個讀取器的處理,確保高效靈活的資料處理。在本教程中,我們將介紹在 Spring Batch 中CompositeItemReader的實現,並檢視示例和測試用例以驗證其行為。

什麼是CompositeItemReader
CompositeItemReader的工作原理是將讀取過程委託給ItemReader例項列表。它按照定義順序從每個讀取器讀取專案,確保按順序處理資料。

這在以下場景中尤其有用:

  • 從多個資料庫或表中讀取
  • 合併檔案和資料庫中的資料
  • 按特定順序處理來自不同來源的資料

此外,CompositeItemReader是org.springframework.batch.item.support包的一部分,它是在 Spring Batch 5.2.0 中引入的。

實現CompositeItemReader
讓我們來看一個示例,其中我們從兩個不同的來源讀取資料:一個平面檔案和一個資料庫。目標是將來自兩個來源的產品資料合併為一個流以進行批處理。一些產品在平面檔案中,而其他產品在資料庫中,確保所有可用記錄一起處理。

1. 建立產品類別
在設定讀取器之前,我們需要一個Product類來表示正在處理的資料的結構。此類封裝了有關產品的詳細資訊,例如其 ID、名稱、庫存情況和價格。我們將在從 CSV 檔案和資料庫讀取時使用此模型,以確保資料處理的一致性。

Product類充當我們的讀者和批處理作業之間的資料傳輸物件 (DTO):

public class Product {
    private Long productId;
    private String productName;
    private Integer stock;
    private BigDecimal price;
    public Product(Long productId, String productName, Integer stock, BigDecimal price) {
        this.productId = productId;
        this.productName = productName;
        this.stock = stock;
        this.price = price;
    }
    <font>// Getters and Setters<i>
}

Product類表示批處理作業將處理的每條記錄。現在我們的資料模型已準備就緒,我們將為CSV 檔案和資料庫建立單獨的ItemReader元件。

2.產品資料的平面檔案讀取器
第一個讀取器使用FlatFileItemReader從 CSV 檔案獲取資料。我們將其配置為讀取分隔檔案 ( products.csv ) 並將其欄位對映到Product類:

@Bean
public FlatFileItemReader<Product> fileReader() {
  return new FlatFileItemReaderBuilder<Product>()
    .name(<font>"fileReader")
    .resource(new ClassPathResource(
"products.csv"))
    .delimited()
    .names(
"productId", "productName", "stock", "price")
    .linesToSkip(1)
    .targetType(Product.class)
    .build();
}

這裡,delimited()方法確保資料欄位使用分隔符(預設情況下為逗號)分隔。names ()方法定義與Product類的屬性匹配的列名,而targetType(Product.class)方法將欄位對映到類屬性。

3.產品資料資料庫讀取器
接下來,我們定義一個JdbcCursorItemReader來從名為products的資料庫表中檢索產品資料。此讀取器執行 SQL 查詢來獲取產品詳細資訊並將其對映到我們的Product類。

下面是資料庫讀取器的實現:

@Bean
public JdbcCursorItemReader<Product> dbReader(DataSource dataSource) {
  return new JdbcCursorItemReaderBuilder<Product>()
    .name(<font>"dbReader")
    .dataSource(dataSource())
    .sql(
"SELECT productid, productname, stock, price FROM products")
    .rowMapper((rs, rowNum) -> new Product(
      rs.getLong(
"productid"),
      rs.getString(
"productname"),
      rs.getInt(
"stock"),
      rs.getBigDecimal(
"price")))
    .build();
}

JdbcCursorItemReader使用遊標從資料庫中一次一行地讀取產品記錄,從而提高批處理效率。rowMapper()函式將結果集中的每一列對映到Product類中的相應欄位。

使用CompositeItemReader組合閱讀器
現在我們的 CSV 和資料庫讀取器都已配置為讀取產品資料,我們可以使用CompositeItemReader將它們整合:

@Bean
public CompositeItemReader<Product> compositeReader() {
    return new CompositeItemReader<>(Arrays.asList(fileReader(), dbReader()));
}

透過配置我們的CompositeItemReader,我們可以順序處理來自多個來源的資料。

最初,FlatFileItemReader從 CSV 檔案中讀取產品記錄,將每行解析為Product物件。處理完檔案中的所有行後,JdbcCursorItemReader接管並開始直接從資料庫獲取產品資料。

配置批處理作業
一旦我們為 CSV 檔案和資料庫定義了讀取器,下一步就是配置批處理作業本身。在 Spring Batch 中,一個作業由多個步驟組成,其中每個步驟處理處理管道的特定部分:

@Bean
public Job productJob(JobRepository jobRepository, Step step) {
  return new JobBuilder(<font>"productJob", jobRepository)
    .start(step)
    .build();
}
@Bean
public Step step(ItemReader compositeReader, ItemWriter productWriter) {
  return new StepBuilder(
"productStep", jobRepository)
    .<Product, Product>chunk(10, transactionManager)
    .reader(compositeReader)
    .writer(productWriter)
    .build();
}

在這種情況下,我們的工作包含一個步驟,即讀取產品資料、以10 個塊的形式處理它,並將其寫入所需的輸出。

productJob bean 負責定義批處理作業。它從配置為處理產品資料處理的productStep開始執行。

透過此設定,我們的批處理作業首先使用CompositeItemReader從兩個來源讀取產品資料,以 10 個資料塊為單位進行處理,然後使用productWriter()寫入轉換或過濾後的資料。這確保了批處理管道順暢而高效。

執行批處理作業
現在我們已經配置了讀取器和作業,下一步是執行批處理作業並觀察CompositeItemReader的行為。我們將在 Spring Boot 應用程式中執行該作業,以檢視它如何處理來自 CSV 檔案和資料庫的資料。

為了以程式設計方式觸發批處理作業,我們需要使用JobLauncher。這使我們能夠啟動作業並監視其進度:

@Bean
public CommandLineRunner runJob() {
    return args -> {
        try {
            jobLauncher.run(productJob, new JobParametersBuilder()
              .addLong(<font>"time", System.currentTimeMillis())
              .toJobParameters());
        } catch (Exception e) {
           
// handle exception<i>
        }
    };
}

在此示例中,我們建立一個CommandLineRunner bean 來在應用程式啟動時執行該作業。這將使用JobLauncher呼叫productJob。我們還新增了帶有時間戳的唯一JobParameters,以確保該作業每次都以唯一方式執行。

測試複合專案閱讀器
為了確保CompositeItemReader按預期工作,我們將測試CompositeItemReader的功能,以確保它能從 CSV 和資料庫源正確讀取產品。

準備測試資料
我們首先準備一個包含產品資料的 CSV 檔案,作為CompositeItemReader的輸入:

productId,productName,stock,price
101,Apple,50,1.99

然後,我們還向產品表中插入一條記錄:

@BeforeEach
public void setUp() {
    jdbcTemplate.update(<font>"INSERT INTO products (productid, productname, stock, price) VALUES (?, ?, ?, ?)",
      102,
"Banana", 30, 1.49);
}

測試閱讀順序
現在,我們將測試CompositeItemReader,以驗證它是否按正確的順序處理產品,從 CSV 和資料庫源讀取。在此測試中,我們從 CSV 檔案讀取產品,然後從資料庫讀取產品,並斷言這些值符合我們的預期:

@Test
public void givenTwoReaders_whenRead_thenProcessProductsInOrder() throws Exception {
    StepExecution stepExecution = new StepExecution(
      <font>"testStep",
      new JobExecution(1L, new JobParameters()),
      1L);
    ExecutionContext executionContext = stepExecution.getExecutionContext();
    compositeReader.open(executionContext);
    Product product1 = compositeReader.read();
    assertNotNull(product1);
    assertEquals(101, product1.getProductId());
    assertEquals(
"Apple", product1.getProductName());
    Product product2 = compositeReader.read();
    assertNotNull(product2);
    assertEquals(102, product2.getProductId());
    assertEquals(
"Banana", product2.getProductName());
}

使用一個讀取器返回空結果進行測試
在本節中,我們將測試當使用多個讀取器並且其中一個讀取器返回null 時CompositeItemReader的行為。 這一點很重要,以確保CompositeItemReader跳過任何不返回任何資料的讀取器並繼續下一個讀取器,直到找到有效資料:

@Test
public void givenMultipleReader_whenOneReaderReturnNull_thenProcessDataFromNextReader() throws Exception {
    ItemStreamReader<Product> emptyReader = mock(ItemStreamReader.class);
    when(emptyReader.read()).thenReturn(null);
    ItemStreamReader<Product> validReader = mock(ItemStreamReader.class);
    when(validReader.read()).thenReturn(new Product(103L, <font>"Cherry", 20, BigDecimal.valueOf(2.99)), null);
    CompositeItemReader<Product> compositeReader = new CompositeItemReader<>(
      Arrays.asList(emptyReader, validReader));
    Product product = compositeReader.read();
    assertNotNull(product);
    assertEquals(103, product.getProductId());
    assertEquals(
"Cherry", product.getProductName());
}

 

相關文章