持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情。
SpringBatch从入门到精通-2-StepScope作用域和用法【掘金日新计划】
SpringBatch从入门到精通-3-并行处理【掘金日新计划】
SpringBatch从入门到精通-3.2-并行处理-远程分区【掘金日新计划】
SpringBatch从入门到精通-3.3-并行处理-远程分区(消息聚合)【掘金日新计划】
SpringBatch从入门到精通-4 监控和指标【掘金日新计划】
SpringBatch从入门到精通-4.2 监控和指标-原理【掘金日新计划】
SpringBatch从入门到精通-5 数据源配置相关【掘金日新计划】
SpringBatch从入门到精通-5.2 数据源配置相关-原理【掘金日新计划】
SpringBatch从入门到精通-6 读和写处理【掘金日新计划】
SpringBatch从入门到精通-6.1 读和写处理-实战【掘金日新计划】
SpringBatch从入门到精通-6.2 读和写处理-实战2【掘金日新计划】
SpringBatch从入门到精通-6.3 读和写处理-实战3【掘金日新计划】
SpringBatch从入门到精通-6.4 读和写处理-实战4【掘金日新计划】
常见的批处理模式
一些批处理作业可以完全由 Spring Batch 中的现成组件组装而成。例如,可以将ItemReader和ItemWriter实现配置为涵盖广泛的场景。但是,在大多数情况下,必须编写自定义代码。应用程序开发人员的主要 API 入口点是Tasklet、 ItemReader、ItemWriter和各种侦听器接口。大多数简单的批处理作业都可以使用来自 Spring Batch 的现成输入ItemReader,但通常情况下,在处理和编写过程中存在自定义问题,需要开发人员实现ItemWriter或者 ItemProcessor。
记录项目处理和失败
一个常见的用例是需要在一个步骤中逐项对错误进行特殊处理,可能会记录到特殊通道或将记录插入数据库。面向块的(从步骤工厂 bean 创建)允许用户使用简单的 for errors on和for errors on来Step实现这个用例。以下代码片段说明了记录读取和写入失败的侦听器:readError 或者WriteError
public class ItemFailureLoggerListener extends ItemListenerSupport {
private static Log logger = LogFactory.getLog("item.error");
public void onReadError(Exception ex) {
logger.error("Encountered error on read", e);
}
public void onWriteError(Exception ex, List<? extends Object> items) {
logger.error("Encountered error on write", ex);
}
}
实现了这个监听器后,它必须注册一个步骤。
以下示例显示如何使用步骤 Java 注册侦听器:
Java 配置
@Bean
public Step simpleStep() {
return this.stepBuilderFactory.get("simpleStep")
...
.listener(new ItemFailureLoggerListener())
.build();
}
如果侦听器在onError()方法中执行任何操作,则它必须位于将要回滚的事务中。如果您需要在方法中使用事务资源,例如数据库,onError()请考虑向该方法添加声明性事务,并将其传播属性的值设置为REQUIRES_NEW.
出于业务原因手动停止作业
Spring Batch 通过JobOperator接口提供了一个方法stop(),但这实际上是供操作员使用的,而不是应用程序程序员使用的。有时,从业务逻辑中停止作业执行更方便或更有意义。
最简单的做法是抛出一个RuntimeException(既不会无限期重试也不会跳过)。例如,可以使用自定义异常类型,如下例所示:
public class PoisonPillItemProcessor<T> implements ItemProcessor<T, T> {
@Override
public T process(T item) throws Exception {
if (isPoisonPill(item)) {
throw new PoisonPillException("Poison pill detected: " + item);
}
return item;
}
}
停止执行步骤的另一种简单方法是从 中返回null, ItemReader如以下示例所示:
public class EarlyCompletionItemReader implements ItemReader<T> {
private ItemReader<T> delegate;
public void setDelegate(ItemReader<T> delegate) { ... }
public T read() throws Exception {
T item = delegate.read();
if (isEndItem(item)) {
return null; // end the step here
}
return item;
}
}
前面的示例实际上依赖于这样一个事实,即CompletionPolicy当要处理的项目是 时,该策略的默认实现会发出一个完整的批次信号null。可以实施更复杂的完成策略并将其注入Step到SimpleStepFactoryBean.
以下示例显示了如何将完成策略注入 Java 中的步骤:
Java 配置
@Bean
public Step simpleStep() {
return this.stepBuilderFactory.get("simpleStep")
.<String, String>chunk(new SpecialCompletionPolicy())
.reader(reader())
.writer(writer())
.build();
}
另一种方法是在 中设置一个标志, 在项目处理之间由框架中StepExecution的实现检查。Step为了实现这个替代方案,我们需要访问当前的StepExecution,这可以通过实现 StepListener并将其注册到Step. 以下示例显示了一个设置标志的侦听器:
public class CustomItemWriter extends ItemListenerSupport implements StepListener {
private StepExecution stepExecution;
public void beforeStep(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
public void afterRead(Object item) {
if (isPoisonPill(item)) {
stepExecution.setTerminateOnly();
}
}
}
设置标志后,默认行为是step抛出 JobInterruptedException. 这种行为可以通过 StepInterruptionPolicy. 但是,唯一的选择是抛出或不抛出异常,因此这始终是作业的异常结束。
添加页脚记录
通常,在写入FlatFile时,必须在所有处理完成后将“页脚”记录附加到文件末尾。这可以使用 Spring Batch 提供的接口FlatFileFooterCallback 来实现。(FlatFileFooterCallback )是 可选属性, FlatFileItemWriter可以添加到项目编写器中。
下面的例子展示了如何在 Java中使用FlatFileHeaderCallback 和headerCallback
Java 配置
@Bean
public FlatFileItemWriter<String> itemWriter(Resource outputResource) {
return new FlatFileItemWriterBuilder<String>()
.name("itemWriter")
.resource(outputResource)
.lineAggregator(lineAggregator())
.headerCallback(headerCallback())
.footerCallback(footerCallback())
.build();
}
页脚回调接口只有一个方法,当必须编写页脚时调用,如下面的接口定义所示:
public interface FlatFileFooterCallback {
void writeFooter(Writer writer) throws IOException;
}
编写摘要页脚
涉及页脚记录的一个常见要求是在输出过程中汇总信息并将此信息附加到文件末尾。此页脚通常用作文件的摘要或提供校验和。
例如,如果批处理作业正在将Trade记录写入平面文件,并且要求将所有记录的总金额Trades放在页脚中,则ItemWriter可以使用以下实现:
public class TradeItemWriter implements ItemWriter<Trade>,
FlatFileFooterCallback {
private ItemWriter<Trade> delegate;
private BigDecimal totalAmount = BigDecimal.ZERO;
public void write(List<? extends Trade> items) throws Exception {
BigDecimal chunkTotal = BigDecimal.ZERO;
for (Trade trade : items) {
chunkTotal = chunkTotal.add(trade.getAmount());
}
delegate.write(items);
// After successfully writing all items
totalAmount = totalAmount.add(chunkTotal);
}
public void writeFooter(Writer writer) throws IOException {
writer.write("Total Amount Processed: " + totalAmount);
}
public void setDelegate(ItemWriter delegate) {...}
}
这TradeItemWriter存储了一个totalAmount值,该值随着写入的amount 每个项目的增加而增加。Trade处理完最后一个后Trade,框架调用 writeFooter,将totalAmount放入文件中。请注意,该write方法使用了一个临时变量 ,chunkTotal它存储了块中的总 Trade金额。这样做是为了确保如果 write方法中发生跳过,totalAmount则保持不变。只有在write 方法结束时,一旦我们保证不会抛出异常,我们才会更新 totalAmount.
为了writeFooter调用该方法,TradeItemWriter(实现FlatFileFooterCallback)必须连接FlatFileItemWriter到 footerCallback.
以下示例显示了如何TradeItemWriter在 Java 中连接:
Java 配置
@Bean
public TradeItemWriter tradeItemWriter() {
TradeItemWriter itemWriter = new TradeItemWriter();
itemWriter.setDelegate(flatFileItemWriter(null));
return itemWriter;
}
@Bean
public FlatFileItemWriter<String> flatFileItemWriter(Resource outputResource) {
return new FlatFileItemWriterBuilder<String>()
.name("itemWriter")
.resource(outputResource)
.lineAggregator(lineAggregator())
.footerCallback(tradeItemWriter())
.build();
}
TradeItemWriter到目前为止,只有在Step不可重新启动的情况下,编写的方式才能正确运行。这是因为该类是有状态的(因为它存储了 totalAmount),但totalAmount没有持久化到数据库中。因此,在重新启动的情况下无法检索它。为了使这个类可重新启动,ItemStream接口应该与方法open和 一起实现update,如下例所示:
public void open(ExecutionContext executionContext) {
if (executionContext.containsKey("total.amount") {
totalAmount = (BigDecimal) executionContext.get("total.amount");
}
}
public void update(ExecutionContext executionContext) {
executionContext.put("total.amount", totalAmount);
}
totalAmountupdate 方法在该对象被持久化到 ExecutionContext数据库之前存储最新版本。open 方法从 中检索任何现有totalAmount的ExecutionContext并将其用作处理的起点,从而允许TradeItemWriter在重新启动时从上次Step运行时停止的地方继续。
多行记录
虽然对于平面文件来说,通常情况下每条记录都被限制在一行中,但一个文件可能具有跨越多行且具有多种格式的记录是很常见的。以下文件摘录显示了这种安排的示例:
HEA;0013100345;2007-02-15
NCU;史密斯;彼得;;T;20014539;F
坏;;橡树街 31/A;;小镇;00235;IL;US
FOT;2;2;267.34
以“HEA”开头的行和以“FOT”开头的行之间的所有内容都被视为一条记录。为了正确处理这种情况,必须考虑以下几点:
- 不是一次读取一条记录,而是ItemReader必须将多行记录的每一行作为一个组读取,以便可以将其传递给ItemWriter原封不动的。
- 每种线型可能需要进行不同的标记。
因为一条记录跨越多行,而且我们可能不知道有多少行,所以ItemReader必须小心始终读取整条记录。为此,ItemReader应将自定义实现为 FlatFileItemReader.
以下示例显示了如何ItemReader在 Java 中实现自定义:
Java 配置
@Bean
public MultiLineTradeItemReader itemReader() {
MultiLineTradeItemReader itemReader = new MultiLineTradeItemReader();
itemReader.setDelegate(flatFileItemReader());
return itemReader;
}
@Bean
public FlatFileItemReader flatFileItemReader() {
FlatFileItemReader<Trade> reader = new FlatFileItemReaderBuilder<>()
.name("flatFileItemReader")
.resource(new ClassPathResource("data/multiLine.txt"))
.lineTokenizer(orderFileTokenizer())
.fieldSetMapper(orderFieldSetMapper())
.build();
return reader;
}
为了确保每一行都被正确标记,这对于固定长度的输入尤其重要,PatternMatchingCompositeLineTokenizer可以在 delegate 上使用FlatFileItemReader。
以下示例显示如何确保在 Java 中正确标记每一行:
@Bean
public PatternMatchingCompositeLineTokenizer orderFileTokenizer() {
PatternMatchingCompositeLineTokenizer tokenizer =
new PatternMatchingCompositeLineTokenizer();
Map<String, LineTokenizer> tokenizers = new HashMap<>(4);
tokenizers.put("HEA*", headerRecordTokenizer());
tokenizers.put("FOT*", footerRecordTokenizer());
tokenizers.put("NCU*", customerLineTokenizer());
tokenizers.put("BAD*", billingAddressLineTokenizer());
tokenizer.setTokenizers(tokenizers);
return tokenizer;
}
这个包装器必须能够识别记录的结尾,以便它可以不断地调用read()它的委托,直到到达结尾。对于读取的每一行,包装器应该构建要返回的项目。到达页脚后,可以将项目退回以交付到ItemProcessor和ItemWriter,如以下示例所示:
private FlatFileItemReader<FieldSet> delegate;
public Trade read() throws Exception {
Trade t = null;
for (FieldSet line = null; (line = this.delegate.read()) != null;) {
String prefix = line.readString(0);
if (prefix.equals("HEA")) {
t = new Trade(); // Record must start with header
}
else if (prefix.equals("NCU")) {
Assert.notNull(t, "No header was found.");
t.setLast(line.readString(1));
t.setFirst(line.readString(2));
...
}
else if (prefix.equals("BAD")) {
Assert.notNull(t, "No header was found.");
t.setCity(line.readString(4));
t.setState(line.readString(6));
...
}
else if (prefix.equals("FOT")) {
return t; // Record must end with footer
}
}
Assert.isNull(t, "No 'END' was found.");
return null;
}
执行系统命令
许多批处理作业需要从批处理作业中调用外部命令。这样的过程可以由调度程序单独启动,但是关于运行的通用元数据的优势将会丢失。此外,多步骤作业也需要拆分为多个作业。
由于需求如此普遍,Spring Batch 提供了Tasklet调用系统命令的实现。
以下示例显示了如何在 Java 中调用外部命令:
Java 配置
@Bean
public SystemCommandTasklet tasklet() {
SystemCommandTasklet tasklet = new SystemCommandTasklet();
tasklet.setCommand("echo hello");
tasklet.setTimeout(5000);
return tasklet;
}
未找到输入时处理步骤完成
在许多批处理场景中,在数据库或文件中找不到要处理的行并不例外。被Step简单地认为没有找到工作并完成读取 0 个项目。如果即使存在输入也没有写出任何内容,这可能会导致一些混乱(如果文件命名错误或出现类似问题,通常会发生这种情况)。出于这个原因,应该检查元数据本身以确定框架需要处理多少工作。但是,如果没有找到输入被认为是异常的呢?在这种情况下,以编程方式检查元数据是否没有处理任何项目并导致失败是最好的解决方案。因为这是一个常见的用例,所以 Spring Batch 提供了一个具有此功能的侦听器,如以下类定义所示NoWorkFoundStepExecutionListener:
public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {
public ExitStatus afterStep(StepExecution stepExecution) {
if (stepExecution.getReadCount() == 0) {
return ExitStatus.FAILED;
}
return null;
}
}
前面在“afterStep”阶段StepExecutionListener检查了readCount属性 StepExecution以确定是否没有读取任何项目。如果是这种情况,FAILED则返回退出代码,指示Step应该失败。否则null返回,不影响Step.
将数据传递给未来的步骤
将信息从一个步骤传递到另一个步骤通常很有用。这可以通过ExecutionContext. 问题是有两个ExecutionContexts:一个在 Step级别,一个在Job级别。Step ExecutionContext只适用再step期间,Job ExecutionContext贯穿整个Job。另一方面,Step ExecutionContext每次Step提交一个chunk时都会更新,而 Job ExecutionContext仅在每个Step 结束时更新。
这种分离的结果是所有数据都必须放在执行Step ExecutionContext时Step。这样做可确保在Step运行时正确存储数据。如果数据存储在 中Job ExecutionContext,则在Step执行期间不会持久保存。如果Step失败,则该数据将丢失。
public class SavingItemWriter implements ItemWriter<Object> {
private StepExecution stepExecution;
public void write(List<? extends Object> items) throws Exception {
// ...
ExecutionContext stepContext = this.stepExecution.getExecutionContext();
stepContext.put("someKey", someObject);
}
@BeforeStep
public void saveStepExecution(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
}
为了使数据可供将来使用Steps,必须Job ExecutionContext在步骤完成后将其“提升”到。Spring Batch 提供了 ExecutionContextPromotionListener用于此目的。侦听器必须配置与ExecutionContext必须提升的数据相关的键。它还可以选择性地配置一个退出代码模式列表,其中应该进行提升(这COMPLETED是默认设置)。与所有侦听器一样,它必须在Step.
Java 配置
@Bean
public Job job1() {
return this.jobBuilderFactory.get("job1")
.start(step1())
.next(step1())
.build();
}
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.<String, String>chunk(10)
.reader(reader())
.writer(savingWriter())
.listener(promotionListener())
.build();
}
@Bean
public ExecutionContextPromotionListener promotionListener() {
ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
listener.setKeys(new String[] {"someKey"});
return listener;
}
最后,必须从 中检索保存的值Job ExecutionContext,如下例所示:
public class RetrievingItemWriter implements ItemWriter<Object> {
private Object someObject;
public void write(List<? extends Object> items) throws Exception {
// ...
}
@BeforeStep
public void retrieveInterstepData(StepExecution stepExecution) {
JobExecution jobExecution = stepExecution.getJobExecution();
ExecutionContext jobContext = jobExecution.getExecutionContext();
this.someObject = jobContext.get("someKey");
}
}
代码位置: github.com/jackssybin/…