针对批处理,Spring Batch提供ItemReader、ItemProcessor、ItemWriter三个核心组件,类似ETL的三个步骤。
ItemReader支持从多种数据源读入数据,接口定义如下:
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException;
}
泛型T表示读入的Item类型,一般是有明确意义的领域模型。通常情况下,read方法读入数据,返回item,如果没有更多数据,返回null。 在特殊的场景下,可以支持事务操作,例如对于JMS数据源,可以在事务回滚你的时候重新放回地队列中。
在数据库解决方案中,游标是一个核心概念,JDBS的ResultSet也使用了Cursor的概念,如:
基于Cursor的Reader将游标的概念转化为流的概念,例如配置一个CursorReader:
<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>
调用该Reader的read方法将逐条返回结果中的item,而无需关心底层细节
如果使用存储过程查询Item,则可以通过StoreProcedureItemReader完成,下面是一个简单例子:
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="sp_customer_credit"/>
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
</property>
</bean>
如果使用的是函数调用:
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="sp_customer_credit"/>
<property name="function" value="true"/>
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.CustomerCreditRowMapper"/>
</property>
</bean>
如果带有参数:
<bean id="reader" class="o.s.batch.item.database.StoredProcedureItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="procedureName" value="spring.cursor_func"/>
<property name="parameters">
<list>
<bean class="org.springframework.jdbc.core.SqlOutParameter">
<constructor-arg index="0" value="newid"/>
<constructor-arg index="1">
<util:constant static-field="oracle.jdbc.OracleTypes.CURSOR"/>
</constructor-arg>
</bean>
<bean class="org.springframework.jdbc.core.SqlParameter">
<constructor-arg index="0" value="amount"/>
<constructor-arg index="1">
<util:constant static-field="java.sql.Types.INTEGER"/>
</constructor-arg>
</bean>
<bean class="org.springframework.jdbc.core.SqlParameter">
<constructor-arg index="0" value="custid"/>
<constructor-arg index="1">
<util:constant static-field="java.sql.Types.INTEGER"/>
</constructor-arg>
</bean>
</list>
</property>
<property name="refCursorPosition" value="1"/>
<property name="rowMapper" ref="rowMapper"/>
<property name="preparedStatementSetter" ref="parameterSetter"/>
</bean>
JdbcPagingItemReader:
<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>
IbatisPagingItemReader:
<bean id="itemReader" class="org.spr...IbatisPagingItemReader">
<property name="sqlMapClient" ref="sqlMapClient"/>
<property name="queryId" value="getPagedCustomerCredits"/>
<property name="pageSize" value="1000"/>
</bean>
<select id="getPagedCustomerCredits" resultMap="customerCreditResult">
select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize#
</select>
_skiprows和_pagesize参数由Reader提供。
当Reader操作的实现已经有现成的代码时,可以通过适配器将其转化为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" />
但是有一点,也就是上述的generateFoo方法必须和read方法的协议保持一致,及返回值等其他约定必须符合read规范。
如果一条记录是否已经被处理,是通过数据源的某个字段来表示的,那么此时Reader将没有必要保存状态,例如当前行数,不然将导致重启的时候出错,因此将Reader的saveState置为false。
<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>
IteamWriter用于将处理后的数据写入到目的地。支持批量写出,接口如下:
public interface ItemWriter<T> {
void write(List<? extends T> items) throws Exception;
}
写入Item List后,需要进行的刷新、收尾操作都可以在这里完成,例如关闭Session等。
对于Reader和Writer,一般都只完成特定目的的读写操作,如果需要更复杂的读写操作,可以使用组合模式,创建更负责的综合的Reader/Writer,下面是一个例子:
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 {
//Add business logic here
itemWriter.write(item);
}
public void setDelegate(ItemWriter<T> itemWriter){
this.itemWriter = itemWriter;
}
}
Reader也一样,例如业务的一次处理需要一个复杂的Item,这些数据可能来自不同的数据源,或者来自同一数据源的不同表,可以通过组合多个Reader来完成。
需要注意的是,如果Step中Reader、Writer、Processor实现了ItemStream或者StepListener,则会自动地被注册到Step中,但是上述的例子中使用了代理的Writer,对于代理使用的itemWriter,Step是不知道的,因此需要显示注册,以便Step管理该Steam。
<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" />
文件的事务性是通过特殊的ItemWriter来保障的,但是在JDBC中,事务性由数据库本身来提供保障,因此没有必要提供特殊的Writer。数据库Wtriter执行过程如下:
但是在批处理中,批量的更新或者插入时,如果其中一条记录出错导致事务回滚,这时候无法知道是哪条具体的记录造成的。解决的唯一方案是每次都刷新:
Processor处理自定义的业务逻辑,完成转换、过滤等操作。例如:
public class Foo {}
public class Bar {
public Bar(Foo foo) {}
}
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 BarWriter implements ItemWriter<Bar>{
public void write(List<? extends Bar> bars) throws Exception {
//write bars
}
}
将这些配置到一个Step里:
<job id="ioSampleJob">
<step name="step1">
<tasklet>
<chunk reader="fooReader" processor="fooProcessor" writer="barWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
可以将多个ItemProcessor组合为一个Processor链条,通过使用CompositeItemProcessor:
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);
}
}
// 复合Processor
CompositeItemProcessor<Foo,Foobar> compositeProcessor =
new CompositeItemProcessor<Foo,Foobar>();
List itemProcessors = new ArrayList();
itemProcessors.add(new FooTransformer());
itemProcessors.add(new BarTransformer());
compositeProcessor.setDelegates(itemProcessors);
<job id="ioSampleJob">
<step name="step1">
<tasklet>
<chunk reader="fooReader" processor="compositeProcessor" writer="foobarWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
<bean id="compositeItemProcessor" class="org.springframework.batch.item.support.CompositeItemProcessor">
<property name="delegates">
<list>
<bean class="..FooProcessor" />
<bean class="..BarProcessor" />
</list>
</property>
</bean>
Skipping表示一个Item是非法的(invalid),需要跳过。而Filtering只是表示这个Item不应该被写到目标数据中。
可以通过返回null来表示被过滤。
Item流是对Reader和Writer的统一抽象,接口如下:
public interface ItemStream {
void open(ExecutionContext executionContext) throws ItemStreamException;
void update(ExecutionContext executionContext) throws ItemStreamException;
void close() throws ItemStreamException;
}
其中open和close都比较容易理解,update主要用于将状态信息更新到JobRepository,一般在提交事务的时候调用,确保框架任务状态的准确性。
Spring Batch的Validator组件用于在处理Item之前对Item进行校验,接口定义如下:
public interface Validator {
void validate(Object value) throws ValidationException;
}
将验证器配置到Processor:
<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>
如果Item合法,则正常返回,否则抛出异常。