原文作者: 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)。
- 有些定製訂單的服務。 你需要在每天晚上執行批處理程序來生成清單文件,並將它們發送到相應的供應商手上。
- 讀取數據
- 對數據進行各種處理
- 對數據進行寫操作
<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>
Lines to skip: linesToSkip 屬性告訴 file reader 有多少標題行需要跳過。 CSV文件經常包含標題信息,如列名稱,在文件的第一行,所以在本例中,我們讓reader 跳過文件的第一行。
Line mapper: lineMapper 負責將每行記錄轉換成一個對象。 需要依賴兩個組件:
- LineTokenizer 指定了如何將一行拆分爲多個字段。 本例中我們列出了CSV文件中的列名。
- fieldSetMapper 從字段值構造一個對象。 在我們的例子中構建了一個 Product對象,屬性包括 id, name, description, 以及quantity 字段。
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;
}
}
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;
}
}
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() }
}
}
}
- 它執行一個 SQL SELECT 語句來根據指定的 id 檢索 Product.
- 如果 SELECT 返回一條記錄, 則 write() 中執行一個 update 使用新value來更新數據庫中的記錄.
- 如果 SELECT 沒有返回記錄, 則 write() 執行 INSERT 將產品信息添加到數據庫中.
清單 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 實例寫入數據庫。
清單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>
清單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>
infrastructure 依賴(包)。 這些依賴項就是 Spring 和 Spring Batch的基礎。 當然也引入了 Apache DBCP, 使我們能構建數據庫連接池和MySQL驅動。 plug-in 部分指定了使用Java 1.6進行編譯,並在 build 時將所有依賴項庫複製到 lib 目錄下。 我們可以使用下面的命令來構建項目:
Spring Batch 可以連接到任何你喜歡的數據庫,但爲了演示方便,我們在本示例中使用MySQL。 請下載MySQL並安裝後再執行下面的腳本。社區版是免費的,而且能滿足大多數人的需要。請根據你的操作系統選擇合適的版本下載安裝.然後可能需要手動啓動MySQL(Windows 一般自動啓動)。
安裝好MySQL後還需要創建數據庫以及相應的用戶(並賦予權限)。啓動命令行並進入MySQL的bin目錄啓動 mysql 客戶端,連接服務器後執行以下SQL命令(請注意,在Linux下可能需要使用 root 用戶執行 mysql 客戶端程序, 或者使用sudo 進行權限切換.
create user 'sbe'@'localhost' identified by 'sbe';
grant all on spring_batch_example.* to 'sbe'@'localhost';
CREATE TABLE PRODUCT (
ID INT NOT NULL,
NAME VARCHAR(128) NOT NULL,
DESCRIPTION VARCHAR(128),
QUANTITY INT,
PRIMARY KEY(ID)
);
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
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
清單8顯示了 ProductItemProcessor 類的源代碼。
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;
}
}
<bean id="productProcessor" class="com.geekcap.javaworld.springbatchexample.simple.processor.ProductItemProcessor" />
<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>
- 創建 processor 類
- 在applicationContext.xml中配置 bean
- 定義一個類型爲 org.springframework.batch.item.support.CompositeItemProcessor 的 bean,然後將其 delegates 設置爲你想執行的處理器bean的 list
- 讓 chunk 的 processor 屬性引用 CompositeItemProcessor
<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 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>
- 定義一個實現 org.springframework.batch.core.step.tasklet.Tasklet 接口的類。
- 實現 execute() 方法。
- 返回恰當的 org.springframework.batch.repeat.RepeatStatus 值: CONTINUABLE 或者是 FINISHED .
- 在 applicationContext.xml 文件中定義對應的 bean。
- 創建一個 step , 其中有一個子元素 tasklet 引用第4步定義的bean。
清單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 id="archiveFileTasklet" class="com.geekcap.javaworld.springbatchexample.simple.tasklet.ArchiveProductImportFileTasklet" scope="step">
<property name="inputFile" value="#{jobParameters['inputFile']}" />
</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>
- Skip : 如果處理過程中某條記錄是錯誤的, 如CSV文件中格式不正確的行, 那麼可以直接跳過該對象, 繼續處理下一個。
- Retry : 如果出現錯誤,而很可能在幾毫秒後再次執行就能解決, 那麼可以讓 Spring Batch 對該元素重試一次/(或多次)。例如, 你可能想要更新數據庫中的某條, 但另一個查詢把這條記錄給鎖了的情況。 而根據業務設計,這個鎖將會很快被釋放, 而重新嘗試可能就會成功。
- Restart : 如果將 job 狀態存儲在數據庫中, 而一旦它執行失敗, 那麼就可以選擇重啓 job 實例, 並繼續上次的執行位置。
- 在 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>
- 在 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>
本文只是簡單介紹 Spring Batch 的皮毛, 但希望能讓你對使用 Spring Batch 執行批處理作業有一定的瞭解和認識。