Spring batch教程 之 讀取CSV文件並寫入MySQL數據庫

原文作者: Steven Haines - 技術架構師

編寫批處理程序來處理GB級別數據量無疑是種海嘯般難以面對的任務,但我們可以用Spring Batch將其拆解爲小塊小塊的(chunk)。 Spring Batch 是Spring框架的一個模塊,專門設計來對各種類型的文件進行批量處理。 本文先講解一個簡單的作業—— 將產品列表從CSV文件中讀取出來,然後導入MySQL數據庫中; 然後我們一起研究 Spring Batch 模塊的批處理功能(/性能),如單/多處理單元(processors), 同時輔以多個微線程(tasklets); 最後簡要介紹Spring Batch對跳過記錄(skipping), 重試記錄(retrying),以及批處理作業的重啓(restarting )等彈性工具。

如果你曾在Java企業系統中用批處理來處理過成千上萬的數據交換,那你就知道工作負載是怎麼回事。 批處理系統要處理龐大無比的數據量,處理單條記錄失敗的情況,還要管理中斷,在重啓動後不要再去處理那些已經執行過的部分。


對於沒有相關經驗的初學者,下面是需要批處理的一些場景,並且如果使用Spring Batch 很可能會節省你很多寶貴的時間:

  • 接收的文件缺少了一部分需要的信息,你需要讀取並解析整個文件,調用某個服務來獲得缺少的那部分信息,然後寫入到某
  • 個輸出文件,供其他批處理程序使用。
  • 如果執行環境中發生了一個錯誤,則將失敗信息寫入數據庫。 有專門的程序每隔15分鐘來遍歷一次失敗信息,如果標記爲
  • 可以重試,那就再執行一次。
  • 在工作流中,你希望其他系統在收到事件消息時,來調用某個特定服務。 如果其他系統沒有調用這個服務,那麼一段時間後
  • 需要自動清理過期數據,以避免影響到正常的業務流程。
  • 每天收到員工信息更新的文件,你需要爲新員工建立相關檔案和賬號(artifacts)。
  • 有些定製訂單的服務。 你需要在每天晚上執行批處理程序來生成清單文件,並將它們發送到相應的供應商手上。
作業與分塊: Spring Batch 範例

Spring Batch 有很多組成部分,我們先來看批量作業中的核心處理。 可以將一個作業分成下面3個不同的步驟:
  1. 讀取數據
  2. 對數據進行各種處理
  3. 對數據進行寫操作
例如,我們可以打開一個CSV格式的數據文件,對文件中的數據執行某些處理,然後將數據寫入數據庫。 在Spring Batch中, 您需要配置一個讀取程序 reader 來讀取文件中的數據(每次一行), 然後並將每一行數據傳遞給 processor 進行處理, 處理器將會將結果收集並分組爲“塊 chunks”, 並把這些記錄發送給 writer ,在這裏是插入到數據庫中。 可以參考圖1所示的週期。

圖1 Spring Batch批處理的基本邏輯

Spring Batch實現了常見輸入源的 readers, 極大地簡化了批處理過程.包括 CSV文件, XML文件、數據庫、文件中的JSON記錄,甚至是 JMS; 同樣也實現了對應的 writers。 如有需要,創建自定義的 readers and writers 也是相當簡單的。

首先,讓我們一起配置一個 file reader 來讀取 CSV文件,將其內容映射到一個對象中,並將生成的對象插入數據庫中。

讀取並處理CVS文件

Spring Batch 內置的reader, org.springframework.batch.item.file.FlatFileItemReader 將文件解析爲許多單獨的行。 它需要一個純文本文件的引用,文件開頭要忽略的行數(通常是頭信息), 以及一個將單行轉換爲一個對象的 line mapper. 行映射器需要一個分割字符串的分詞器,用來將一行劃分爲各個組成字段, 以及一個field set mapper,根據字段值構建一個對FlatFileItemReader 的配置如下所示:

清單1 一個Spring Batch 配置文件

<bean id="productReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
<!-- <property name="resource" value="file:./sample.csv" /> -->
<property name="resource" value="file:#{jobParameters['inputFile']}" />
   <property name="linesToSkip" value="1" />
   <property name="lineMapper">
       <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
       <property name="lineTokenizer">
           <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
       <property name="names" value="id,name,description,quantity" />
       </bean>
    </property>
    <property name="fieldSetMapper">
       <bean class="com.geekcap.javaworld.springbatchexample.simple.reader.ProductFieldSetMapper" />
    </property>
</property>
</bean>

讓我們來看看這些組件。首先,圖2顯示了他們之間的關係:讀取並處理CVS文件

圖2 FlatFileItemReader的組件

Resources: resource 屬性指定了要讀取的文件。 註釋掉的 resource 使用了文件的相對路徑,也就是批處理作業工作目錄下的 sample.csv 。 作業參數 InputFile 就更可愛了: job parameters允許在運行時動態指定相關參數。 在使用 import 文件的情況下,在運行時才決定使用哪個參數比起在編譯時就固定要靈活好用得多。

Lines to skip: linesToSkip 屬性告訴 file reader 有多少標題行需要跳過。 CSV文件經常包含標題信息,如列名稱,在文件的第一行,所以在本例中,我們讓reader 跳過文件的第一行。

Line mapper: lineMapper 負責將每行記錄轉換成一個對象。 需要依賴兩個組件:
  • LineTokenizer 指定了如何將一行拆分爲多個字段。 本例中我們列出了CSV文件中的列名。
  • fieldSetMapper 從字段值構造一個對象。 在我們的例子中構建了一個 Product對象,屬性包括 id, name, description, 以及quantity 字段。
請注意,雖然Spring Batch爲我們提供的基礎框架,但我們仍需要設置字段映射的邏輯。 清單2顯示了 Product 對象的源碼,也就是我們準備構建的對象。

清單2 Product.java

package com.geekcap.javaworld.springbatchexample.simple.model;
/**
* 代表產品的簡單值對象(POJO)
*/
public class Product
{
private int id;
private String name;
private String description;
private int quantity;
public Product() {
}
public Product(int id, String name, String description, int quantity) {
this.id = id;
this.name = name;
this.description = description;
this.quantity = quantity;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

Product 類是一個簡單的POJO,包含4個字段。 清單3顯示了 ProductFieldSetMapper 類的源代碼。

清單3 ProductFieldSetMapper.java

package com.geekcap.javaworld.springbatchexample.simple.reader;
import com.geekcap.javaworld.springbatchexample.simple.model.Product;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.validation.BindException;
/**
* 根據 CSV 文件中的字段集合構建 Product 對象
*/
public class ProductFieldSetMapper implements FieldSetMapper<Product>
{
@Override
public Product mapFieldSet(FieldSet fieldSet) throws BindException {
Product product = new Product();
product.setId( fieldSet.readInt( "id" ) );
product.setName( fieldSet.readString( "name" ) );
product.setDescription( fieldSet.readString( "description" ) );
product.setQuantity( fieldSet.readInt( "quantity" ) );
return product;
}
}

ProductFieldSetMapper 類繼承自 fieldSetMapper ,它只定義了一個方法: mapFieldSet(). mapper映射器將每一行解析成一個FieldSet(包含命名好的字段), 然後傳遞給 mapFieldSet() 方法。 該方法負責組建一個對象來表示 CSV文件中的一行。 在本例中,我們通過 FieldSet 的各種 read 方法 構建了一個Product實例.

寫入數據庫

在讀取文件之後,我們得到了一組 Product ,下一步就是將其寫入數據庫。 技術上允許我們將這些數據連接到一個 processing step,對數據做一些處理之類的,爲簡單起見,我們只將數據寫到數據庫中。 清單4顯示了 ProductItemWriter 類的源代碼。

清單4 ProductItemWriter.java

package com.geekcap.javaworld.springbatchexample.simple.writer;
import com.geekcap.javaworld.springbatchexample.simple.model.Product;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* Writes products to a database
*/
public class ProductItemWriter implements ItemWriter<Product>
{
private static final String GET_PRODUCT = "select * from PRODUCT where id = ?";
private static final String INSERT_PRODUCT = "insert into PRODUCT (id,name,description,quantity) values (?,?,?,?)";
private static final String UPDATE_PRODUCT = "update PRODUCT set name = ?, description = ?,quantity = ? where id = ?";
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void write(List<? extends Product> products) throws Exception
{
for( Product product : products )
{
List<Product> productList = jdbcTemplate.query(GET_PRODUCT, new Object[] {product.getId()}, new RowMapper<Product>() {
@Override
public Product mapRow( ResultSet resultSet, int rowNum ) throws SQLException {
Product p = new Product();
p.setId( resultSet.getInt( 1 ) );
p.setName( resultSet.getString( 2 ) );
p.setDescription( resultSet.getString( 3 ) );
p.setQuantity( resultSet.getInt( 4 ) );
return p;
}
});
if( productList.size() > 0 )
{
jdbcTemplate.update( UPDATE_PRODUCT, product.getName(), product.getDescription(), product.getQuantity(), product.getId() }
else
{
jdbcTemplate.update( INSERT_PRODUCT, product.getId(), product.getName(), product.getDescription(), product.getQuantity() }
}
}
}

ProductItemWriter 類繼承(extends, 其實繼承和實現 implements 沒有本質區別.) ItemWriter 並實現了其唯一的方法:write() . write() 方法接受一個泛型繼承 Product 的 list . Spring Batch 使用“chunking”策略實現其 writers , 意思就是讀取時是一次執行一個item, 而寫入時是將一組數據一塊寫。 如下面的job配置所示,您可以(通過 commit-interval )完全控制每次想要一起寫的item的數量。 在上面的例子中, write() 方法做了這些事:
  1. 它執行一個 SQL SELECT 語句來根據指定的 id 檢索 Product.
  2. 如果 SELECT 返回一條記錄, 則 write() 中執行一個 update 使用新value來更新數據庫中的記錄.
  3. 如果 SELECT 沒有返回記錄, 則 write() 執行 INSERT 將產品信息添加到數據庫中.
ProductItemWriter 類使用Spring的 JdbcTemplate 類,它在 applicationContext.xml 文件中定義並通過自動裝配機制注入到ProductItemWriter 類。 如果你沒有用過 Jdbctemplate 類,可以把它理解爲是 JDBC 接口的一個封裝. 與數據庫進行交互的模板設計模式 的實現. 代碼應該很容易讀懂, 如果你想了解更多信息, 請查看 SpringJdbcTemplate 的 javadoc。

與 application context 文件組裝

到目前爲止我們已經建立了一個 Product 領域對象, 一個 ProductFieldSetMapper 類, 用來將CSV文件中的每一行轉換爲一個對象, 以及一個 ProductItemWriter 類, 來將對象寫入數據庫。 下面我們需要配置 Spring Batch 來將這些東西組裝在一起。清單5 顯示了 applicationContext.xml 文件的源代碼, 這裏面定義了我們需要的bean。

清單 5. applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<context:annotation-config />
<!-- Component scan to find all Spring components -->
<context:component-scan base-package="com.geekcap.javaworld.springbatchexample" />
<!-- Data source - connect to a MySQL instance running on the local machine -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/spring_batch_example"/>
<property name="username" value="sbe"/>
<property name="password" value="sbe"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- Create job-meta tables automatically -->
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="org/springframework/batch/core/schema-drop-mysql.sql" />
<jdbc:script location="org/springframework/batch/core/schema-mysql.sql" />
</jdbc:initialize-database>
<!-- Job Repository: used to persist the state of the batch job -->
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
<property name="transactionManager" ref="transactionManager" />
</bean>
<!-- Job Launcher: creates the job and the job state before launching it -->
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
<!-- Reader bean for our simple CSV example -->
<bean id="productReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
<!-- <property name="resource" value="file:./sample.csv" /> -->
<property name="resource" value="file:#{jobParameters['inputFile']}" />
<!-- Skip the first line of the file because this is the header that defines the fields -->
<property name="linesToSkip" value="1" />
<!-- Defines how we map lines to objects -->
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<!-- The lineTokenizer divides individual lines up into units of work -->
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<!-- Names of the CSV columns -->
<property name="names" value="id,name,description,quantity" />
</bean>
</property>
<!-- The fieldSetMapper maps a line in the file to a Product object -->
<property name="fieldSetMapper">
<bean class="com.geekcap.javaworld.springbatchexample.simple.reader.ProductFieldSetMapper" />
</property>
</bean>
</property>
</bean>
<bean id="productWriter" class="com.geekcap.javaworld.springbatchexample.simple.writer.ProductItemWriter" />
</beans>

注意,將 job 配置從 application/environment 中分離出來使我們能夠將 job 從一個環境移到另一個環境 而不需要重新定義一個
job。 清單5中定義了下面這些bean:

  • dataSource : 示例程序連接到MySQL,所以數據庫連接池配置爲連接到一個名爲 spring_batch_example 的MySQL數據庫,地址爲本機(localhost),具體設置參見下文。
  • transactionmanager : Spring事務管理器, 用於管理MySQL事務。
  • jdbctemplate : 該類提供了與JDBC connections交互的模板設計模式實現。 這是一個 Helper 類,用來簡化我們使用數據庫。在實際的項目中一般會使用某種ORM工具, 例如Hibernate,上面再包裝一個服務層, 但本示例中我想讓它儘可能地簡單。
  • jobrepository : MapJobRepositoryFactoryBean 是 Spring Batch 管理 job 狀態的組件。 在這裏它使用前面配置的
  • jdbctemplate 將 job 信息存儲到MySQL數據庫中。
  • jobLauncher : 這是啓動和管理 Spring Batch 作業工作流的組件,。
  • productReader : 在job中這個 bean 負責執行讀操作。
  • productWriter : 這個bean 負責將 Product 實例寫入數據庫。
請注意, jdbc:initialize-database 節點包含了兩個用來創建所需數據庫表的Spring Batch 腳本。 這些腳本我文件位於Spring Batch core 的JAR文件中(由Maven自動引入了)對應的路徑下。 JAR文件中包含了許多數據庫對應的腳本, 比如MySQL、Oracle、SQL Server,等等。 這些腳本負責在運行 job 時創建需要的schema。 在本示例中,它刪除(drop)表,然後再創建(create)表, 你可以試着運行一下。 如果在生產環境中, 你應該將SQL文件提取出來,然後手動執行 —— 畢竟生產環境一般創建了就不會刪除。

Spring Batch 中的 Lazy scope

你可能已經注意到 productReader 這個bean指定了爲一個值爲“step”的scope屬性。step scope 是Spring框架的作用域之一,主要用於Spring Batch。 它本質上是一個lazy scope, 告訴Spring在首次訪問時才創建bean。在本例中,我們需要使用step scope 是因爲使用了 job 參數的"InputFile"值, 這個值在應用程序啓動時是不存在的。使用 step scope 使Spring Batch在創建這個bean時能夠找到"InputFile"值。

定義job

清單6顯示了 file-import-job.xml 文件, 該文件定義了實際的 job 作業。

清單6 file-import-job.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:batch="http://www.springframework.org/schema/batch"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd">
<!-- Import our beans -->
<import resource="classpath:/applicationContext.xml" />
<job id="simpleFileImportJob" xmlns="http://www.springframework.org/schema/batch">
<step id="importFileStep">
<tasklet>
<chunk reader="productReader" writer="productWriter" commit-interval="5" />
</tasklet>
</step>
</job>
</beans>

請注意,一個job可以包含0到多個step;一個step可以包含0到多個tasklet;一個tasklet可以包含0到多chunk, 如圖3所示。

圖3 Jobs, steps, tasklets和chunks的關係



在我們的示例中, simpleFileImportJob 包含一個名爲 importFileStep 的step。 importFileStep 包含一個未命名的 tasklet,tasklet又包含有一個 chunk。 chunk 引用了 productReader 和 productWriter 。 同時指定了一個屬性 commit-interval, 值爲 5 . 意思是每5條記錄就調用一次 writer。 該 step 利用 productReader 一次讀取5條產品記錄,然後將這些記錄傳遞給productWriter 寫出。 這一塊一直重複執行, 直到所有數據都處理完成爲止。

清單6 還還引入了 applicationContext.xml 文件,該文件包含所有需要的bean。 而 Jobs 通常在單獨的文件中定義; 這是因爲job 加載器在執行時需要一個 job 文件以及對應的 job name。 雖然可以講所有的東西揉進一個文件中,但很快變得臃腫難以維護,所以一般約定, 一個 job 定義在一個文件中, 同時引入所有依賴文件。

最後,你可能會注意到,job 節點 上定義了XML名稱空間( xmlns ) 。 這樣做是爲了不想在每個節點上再加上前綴 " batch: "。在節點級別定義的 namespace 會在該節點和所有子節點上生效。

構建並運行項目

清單7顯示了構建此示例項目的POM文件的內容

清單7 pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.geekcap.javaworld</groupId>
<artifactId>spring-batch-example</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-batch-example</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>3.2.1.RELEASE</spring.version>
<spring.batch.version>2.2.1.RELEASE</spring.batch.version>
<java.version>1.6</java.version>
</properties>
<dependencies>
<!-- Spring Dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
<version>${spring.batch.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-infrastructure</artifactId>
<version>${spring.batch.version}</version>
</dependency>
<!-- Apache DBCP-->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>

<!-- Testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>install</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<finalName>spring-batch-example</finalName>
</build>
</project>


上面的POM文件先引入了 Spring context, core, beans, 和 JDBC 框架/類庫, 然後引入 Spring Batch core 以及
infrastructure 依賴(包)。 這些依賴項就是 Spring 和 Spring Batch的基礎。 當然也引入了 Apache DBCP, 使我們能構建數據庫連接池和MySQL驅動。 plug-in 部分指定了使用Java 1.6進行編譯,並在 build 時將所有依賴項庫複製到 lib 目錄下。 我們可以使用下面的命令來構建項目:

          mvn clean install                                                      

Spring Batch連接到一個數據庫

現在我們的 job 已經設置好了, 如果想在生產環境中運行還需要將Spring Batch連接到數據庫。Spring Batch需要一些表, 用來記錄 job 的當前狀態和已經處理過的 record 列表。這樣,如果某個job確實需要重啓, 則可以從上次斷開的地方繼續執行。

Spring Batch 可以連接到任何你喜歡的數據庫,但爲了演示方便,我們在本示例中使用MySQL。 請下載MySQL並安裝後再執行下面的腳本。社區版是免費的,而且能滿足大多數人的需要。請根據你的操作系統選擇合適的版本下載安裝.然後可能需要手動啓動MySQL(Windows 一般自動啓動)。

安裝好MySQL後還需要創建數據庫以及相應的用戶(並賦予權限)。啓動命令行並進入MySQL的bin目錄啓動 mysql 客戶端,連接服務器後執行以下SQL命令(請注意,在Linux下可能需要使用 root 用戶執行 mysql 客戶端程序, 或者使用sudo 進行權限切換.


          create database spring_batch_example;                                 
          create user 'sbe'@'localhost' identified by 'sbe';                        
          grant all on spring_batch_example.* to 'sbe'@'localhost';                    


第一行SQL創建了一個名爲 spring_batch_example 的數據庫(database), 這個庫用來保存我們的 products 信息。第二行創建了一個名爲 sbe 的用戶 Spring Batch Example的縮寫,你也可以使用其他名字,只要配置得一致就行),密碼也指定爲 sbe 。最後一行將 spring_batch_example 數據庫上的所有權限賦予 sbe 用戶。

接下來,使用下面的命令創建 PRODUCT 表:

CREATE TABLE PRODUCT (
ID INT NOT NULL,
NAME VARCHAR(128) NOT NULL,
DESCRIPTION VARCHAR(128),
QUANTITY INT,
PRIMARY KEY(ID)
);

接着,我們在項目的 target 目錄下創建一個文件 sample.csv , 並填充一些數據(用英文逗號分隔):

id,name,description,quantity
1,Product One,This is product 1, 10
2,Product Two,This is product 2, 20
3,Product Three,This is product 3, 30
4,Product Four,This is product 4, 20
5,Product Five,This is product 5, 10
6,Product Six,This is product 6, 50
7,Product Seven,This is product 7, 80
8,Product Eight,This is product 8, 90

可以使用下面的命令啓動 batch job:

java -cp spring-batch-example.jar:./lib/* org.springframework.batch.core.launch.support.CommandLineJobRunner classpath         

CommandLineJobRunner 是 Spring Batch 框架中執行 job 的類。 它需要定義了 job的XML文件的名稱, 需要執行的job的名稱, 以及其他可選的一些自定義參數。 因爲 file-import-job.xml 在JAR文件的內部, 所以可以使用這種方式訪問:classpath:/jobs/file-import-job.xml 。 我們給需要執行的 Job指定了一個名稱 simpleFileImportJob 並傳入一個參數InputFile , 值爲 sample.csv 。如果執行不出錯, 輸出結果類似於下面這樣:

Nov 12, 2013 4:09:17 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6b4da8f4: startup date [Tue Nov 12 16:09:17 EST 2013]; Nov 12, 2013 4:09:17 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [jobs/file-import-job.xml]
Nov 12, 2013 4:09:18 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [applicationContext.xml]
Nov 12, 2013 4:09:19 PM org.springframework.beans.factory.support.DefaultListableBeanFactory registerBeanDefinition
INFO: Overriding bean definition for bean 'simpleFileImportJob': replacing [Generic bean: class [org.springframework.batch.core.configuration.Nov 12, 2013 4:09:19 PM org.springframework.beans.factory.support.DefaultListableBeanFactory registerBeanDefinition
INFO: Overriding bean definition for bean 'productReader': replacing [Generic bean: class [org.springframework.batch.item.file.FlatFileItemReader]; Nov 12, 2013 4:09:19 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@6aba4211: defining beans [org.Nov 12, 2013 4:09:19 PM org.springframework.batch.core.launch.support.SimpleJobLauncher afterPropertiesSet
INFO: No TaskExecutor has been set, defaulting to synchronous executor.
Nov 12, 2013 4:09:22 PM org.springframework.batch.core.launch.support.SimpleJobLauncher$1 run
INFO: Job: [FlowJob: [name=simpleFileImportJob]] launched with the following parameters: [{inputFile=sample.csv}]
Nov 12, 2013 4:09:22 PM org.springframework.batch.core.job.SimpleStepHandler handleStep
INFO: Executing step: [importFileStep]
Nov 12, 2013 4:09:22 PM org.springframework.batch.core.launch.support.SimpleJobLauncher$1 run
INFO: Job: [FlowJob: [name=simpleFileImportJob]] completed with the following parameters: [{inputFile=sample.csv}] and the following status: Nov 12, 2013 4:09:22 PM org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.support.ClassPathXmlApplicationContext@6b4da8f4: startup date [Tue Nov 12 16:09:17 EST 2013]; Nov 12, 2013 4:09:22 PM org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
INFO: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@6aba4211: defining

然後到數據庫中檢測一下 PRODUCT 表中是否正確保存了我們在 csv中指定的那幾條記錄(示例是8條)。

對 Spring Batch 執行批量處理

到這一步, 我們的示例程序已經從CSV文件中讀取數據,並將信息導入到了數據庫中。 雖然可以運行起來, 但有時想要對數據進行轉換或着過濾掉某些數據,然後再插入到數據庫中。 在本節中,我們將創建一個簡單的 processor ,並不覆蓋原有的product 數量,而是先從數據庫中查詢現有記錄, 然後將CSV文件中對應的數量添加到 product 中,後再寫入數據庫。

清單8顯示了 ProductItemProcessor 類的源代碼。

清單8 ProductItemProcessor.java

package com.geekcap.javaworld.springbatchexample.simple.processor;
import com.geekcap.javaworld.springbatchexample.simple.model.Product;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* Processor that finds existing products and updates a product quantity appropriately
*/
public class ProductItemProcessor implements ItemProcessor<Product,Product>
{
private static final String GET_PRODUCT = "select * from PRODUCT where id = ?";
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Product process(Product product) throws Exception
{
// Retrieve the product from the database
List<Product> productList = jdbcTemplate.query(GET_PRODUCT, new Object[] {product.getId()}, new RowMapper<Product>() {
@Override
public Product mapRow( ResultSet resultSet, int rowNum ) throws SQLException {
Product p = new Product();
p.setId( resultSet.getInt( 1 ) );
p.setName( resultSet.getString( 2 ) );
p.setDescription( resultSet.getString( 3 ) );
p.setQuantity( resultSet.getInt( 4 ) );
return p;
}
});
if( productList.size() > 0 )
{
// Add the new quantity to the existing quantity
Product existingProduct = productList.get( 0 );
product.setQuantity( existingProduct.getQuantity() + product.getQuantity() );
}
// Return the (possibly) update prduct
return product;
}
}

ProductItemProcessor 實現的接口 ItemProcessor<I,O> , 其中類型 I 表示傳遞給處理器的對象類型, 而 O 則表示處理器返回的對象類型。 在本例中,我們傳入一個 Product 對象,返回的也是一個 Product 對象。ItemProcessor 接口只定義了一個方法:process() , 在裏面我們根據給定的 id 執行一條 SELECT 語句從數據庫中獲取對應的Product 。 如果找到 Product 對象 ,則將該對象的數量加上新的數量。

processor 沒有做任何過濾,但如果 process() 方法返回 null , 則Spring Batch 將會忽略這個 item, 不將其發送給 writer.

將 processor 組裝到 job 中是非常簡單的。 首先,添加一個新的bean 到 applicationContext.xml 文件中:

<bean id="productProcessor" class="com.geekcap.javaworld.springbatchexample.simple.processor.ProductItemProcessor" />

接下來,在 chunk 中通過 processor 屬性來引用這個 bean:

<job id="simpleFileImportJob" xmlns="http://www.springframework.org/schema/batch">
     <step id="importFileStep">
        <tasklet>
            <chunk reader="productReader" processor="productProcessor" writer="productWriter" commit-interval="5" />
        </tasklet>
     </step>
</job>

編譯並執行 job, 如果不出錯, 就可以在數據庫中看到產品的數量發生了變化。

創建多個processors

前面我們定義了單個處理器,但某些情況下可能想要以適當的粒度來創建多個 item processor,然後按順序在同一個 chunk之中執行. 例如,可能需要一個過濾器來跳過數據庫中不存在的記錄,還需要一個 processor 來正確地管理 item 數量。 這時候, 我們可以使用Spring Batch中的 CompositeItemProcessor 來大顯身手. 使用步驟如下:

  1. 創建 processor 類
  2. 在applicationContext.xml中配置 bean
  3. 定義一個類型爲 org.springframework.batch.item.support.CompositeItemProcessor 的 bean,然後將其 delegates 設置爲你想執行的處理器bean的 list
  4. 讓 chunk 的 processor 屬性引用 CompositeItemProcessor

假設我們有一個 ProductFilterProcessor , 則可以像下面這樣指定 process :

<bean id="productFilterProcessor" class="com.geekcap.javaworld.springbatchexample.simple.processor.ProductFilterItemProcessor" />
<bean id="productProcessor" class="com.geekcap.javaworld.springbatchexample.simple.processor.ProductItemProcessor" />
<bean id="productCompositeProcessor" class="org.springframework.batch.item.support.CompositeItemProcessor">
<property name="delegates">
<list>
<ref bean="productFilterProcessor" />
<ref bean="productProcessor" />
</list>
</property>
</bean>

然後只需修改一下 job 配置即可,如下所示:

<job id="simpleFileImportJob" xmlns="http://www.springframework.org/schema/batch">
   <step id="importFileStep">
     <tasklet>
        <chunk reader="productReader" processor="productCompositeProcessor" writer="productWriter" commit-interval="5" />
     </tasklet>
   </step>
</job>

Tasklets(微線程)

分塊是一個非常好的策略,用來將 作業拆分成多塊: 依次讀取每一個 item , 執行處理, 然後將其按塊寫出。 但如果想執行某些只需要執行一次的線性操作該怎麼辦呢? 此時我們可以創建一個 tasklet 。 tasklet 可以執行各種操作/需求! 例如, 可以從FTP站點下載文件, 解壓/解密文件, 或者調用web服務來判斷文件處理是否已經執行。下面是創建一個 tasklet 的基本過程:

  1. 定義一個實現 org.springframework.batch.core.step.tasklet.Tasklet 接口的類。
  2. 實現 execute() 方法。
  3. 返回恰當的 org.springframework.batch.repeat.RepeatStatus 值: CONTINUABLE 或者是 FINISHED .
  4. 在 applicationContext.xml 文件中定義對應的 bean。
  5. 創建一個 step , 其中有一個子元素 tasklet 引用第4步定義的bean。
清單9 顯示了一個新的 tasklet 的源碼, 將我們的輸入文件拷貝到存檔目錄中。

清單9 ArchiveProductImportFileTasklet.java

package com.geekcap.javaworld.springbatchexample.simple.tasklet;
import org.apache.commons.io.FileUtils;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import java.io.File;
/**
* A tasklet that archives the input file
*/
public class ArchiveProductImportFileTasklet implements Tasklet
{
private String inputFile;
@Override
public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception
{
// Make our destination directory and copy our input file to it
File archiveDir = new File( "archive" );
FileUtils.forceMkdir( archiveDir );
FileUtils.copyFileToDirectory( new File( inputFile ), archiveDir );
// We're done...
return RepeatStatus.FINISHED;
}
public String getInputFile() {
return inputFile;
}
public void setInputFile(String inputFile) {
this.inputFile = inputFile;
}
}

ArchiveProductImportFileTasklet 類實現了 Tasklet 接口, 並實現了 execute() 方法。 其中使用Apache Commons I/O 工具庫的 FileUtils 類來創建一個新的 archive 目錄,然後將input file 拷貝到裏面。
將下面的 bean添加到 applicationContext.xml 文件中:

<bean id="archiveFileTasklet" class="com.geekcap.javaworld.springbatchexample.simple.tasklet.ArchiveProductImportFileTasklet" scope="step">
<property name="inputFile" value="#{jobParameters['inputFile']}" />
</bean>

注意, 我們傳入了一個名爲 inputFile 的 job 參數, 這個bean 設置了作用域範圍 scope="step" , 以確保在 bean 對象創建之前
需要的 job 參數都被定義。

清單10 顯示了更新後的job.

清單10 file-import-job.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:batch="http://www.springframework.org/schema/batch"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd">
<!-- Import our beans -->
<import resource="classpath:/applicationContext.xml" />
<job id="simpleFileImportJob" xmlns="http://www.springframework.org/schema/batch">
     <step id="importFileStep" next="archiveFileStep">
        <tasklet>
            <chunk reader="productReader" processor="productProcessor" writer="productWriter" commit-interval="5" />
        </tasklet>
     </step>
     <step id="archiveFileStep">
        <tasklet ref="archiveFileTasklet" />
     </step>
</job>
</beans>

清單10中添加了一個新的step, id爲 archiveFileStep , 然後在 importFileStep 中將 "next" 指向他。 "next" 參數允許我們控制job 中 step 的執行流程。 雖然超出了本文所需的範圍,但我們需要注意, 可以根據某個任務的執行結果狀態來決定下面執行哪個 step[也就是 job分支,類似於 if,switch 什麼的]. 的 archiveFileStep 只包含上面創建的那個 tasklet 。

彈性(Resiliency)

Spring Batch job resiliency提供了以下三個工具:
  1. Skip : 如果處理過程中某條記錄是錯誤的, 如CSV文件中格式不正確的行, 那麼可以直接跳過該對象, 繼續處理下一個。
  2. Retry : 如果出現錯誤,而很可能在幾毫秒後再次執行就能解決, 那麼可以讓 Spring Batch 對該元素重試一次/(或多次)。例如, 你可能想要更新數據庫中的某條, 但另一個查詢把這條記錄給鎖了的情況。 而根據業務設計,這個鎖將會很快被釋放, 而重新嘗試可能就會成功。
  3. Restart : 如果將 job 狀態存儲在數據庫中, 而一旦它執行失敗, 那麼就可以選擇重啓 job 實例, 並繼續上次的執行位置。
Skipping Items(跳過某項)

有時你可能想要跳過某些記錄, 比如 reader 讀取的無效記錄,或者處理/寫入過程中出現異常的對象。 要這樣做,我們可以指定兩個地方:

  • 在 chunk 元素上定義 skip-limit 屬性, 告訴Spring 最多允許跳過多少個 items,超過則 job 失敗(如果無效記錄很少那你可以接受,但如果無效記錄太多,那可能輸入數據就有問題了)。
  • 定義一個 skippable-exception-classes 列表, 用來判斷當前記錄是否可以跳過, 可以指定 include 元素來決定哪些異常發生時將會跳過當前記錄, 還可以指定 exclude 元素來決定哪些異常不會觸發 skip( 比如你想跳過某個異常層次父類, 但排除一或多個子類異常時)。
例如:

<job id="simpleFileImportJob" xmlns="http://www.springframework.org/schema/batch">
     <step id="importFileStep">
        <tasklet>
           <chunk reader="productReader" processor="productProcessor" writer="productWriter" commit-interval="5" skip-limit="10">
           <skippable-exception-classes>
                 <include class="org.springframework.batch.item.file.FlatFileParseException" />
           </skippable-exception-classes>
           </chunk>
        </tasklet>
     </step>
</job>


在這種情況下, 在處理某條記錄時如果拋出 FlatFileParseException 異常, 則這條記錄將被跳過。 如果超過10次 skip, 那麼job 失敗。

重試(Retrying Items)

在其他情況下, 有時發生的異常是可以重試的, 如由於數據庫鎖導致的失敗。 重試(Retry)的實現和跳過(Skip)非常相似:

  • 在 chunk 元素上定義 retry-limit 屬性, 告訴Spring 每個 item 最多允許重試多少次, 超過則認爲該記錄處理失敗。 如果不將重試與跳過組合起來使用,則某條記錄處理失敗, 則 job也被標記爲失敗。
  • 定義一個 retryable-exception-classes 列表, 用來判斷當前記錄是否可以重試; 可以指定 include 元素來決定哪些異常發生時當前記錄可以重試, 還可以指定 exclude 元素來決定哪些異常不會重試當前記錄.。
例如:

<job id="simpleFileImportJob" xmlns="http://www.springframework.org/schema/batch">
       <step id="importFileStep">
            <tasklet>
                <chunk reader="productReader" processor="productProcessor" writer="productWriter" commit-interval="5" retry-limit="5">
                   <retryable-exception-classes>
                      <include class="org.springframework.dao.OptimisticLockingFailureException" />
                   </retryable-exception-classes>
                </chunk>
            </tasklet>
       </step>
</job>


還可以將重試和可跳過的異常通過對應的 skippable exception class 與 retry exception 組合起來。 因此, 如果某個異常觸發了5次重試, 5次重試之後, 如果該異常也在 skippable 列表中, 那麼這條記錄將被跳過。 如果 exception 不在 skippable列表則會導致整個 job 失敗。

重啓 job

最後, 對於執行失敗的 job作業, 我們可以重新啓動,並讓他們從上次斷開的地方繼續執行。 要達到這一點, 只需要使用和上次一模一樣的參數來啓動 job, 則 Spring Batch 會自動從數據庫中找到這個實例然後繼續執行。也可以拒絕重啓, 或者參數控制某個 job 中的 一個 step 可以重啓的次數(一般來說多次重試都失敗了,那我們可能需要放棄。)

總結

某些業務問題使用批處理是最實在的解決方案, 而 Spring batch 框架提供了實現批處理作業的架構。 Spring Batch 將一個分塊模式定義爲三個階段: 讀取(read)、 處理(process)、 已經寫入(write),並且支持對常見資源的讀取和寫入。 本期的Open source Java projects 系列探討了 Spring Batch 是幹什麼的以及如何使用它。

我們先創建了一個簡單的 job 從CSV文件讀取 Product信息然後導入到數據庫, 接着添加 processor 來對 job 進行擴展: 用來管理 product 數量。 最後我們寫了一個單獨的 tasklet 來歸檔輸入文件。 雖然不是示例的一部分, 但Spring Batch 的彈性特徵是非常重要的, 所以我快速介紹了Spring Batch提供的三大彈性工具: skipping records, retrying records, 和 restarting batch jobs。

本文只是簡單介紹 Spring Batch 的皮毛, 但希望能讓你對使用 Spring Batch 執行批處理作業有一定的瞭解和認識。






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