1. 概述
默认情况下,Spring Batch 作业在执行过程中只要遇到异常就会立即失败。但在实际生产环境中,我们往往希望系统具备更强的容错能力,尤其是面对临时性故障(如网络抖动、数据库锁冲突等)时,能够自动重试而不是直接挂掉。
本文将带你掌握如何在 Spring Batch 中配置重试逻辑,提升批处理任务的稳定性与健壮性。✅
2. 场景示例
假设我们有一个批处理任务,用于处理如下格式的 CSV 输入文件:
username, userid, transaction_date, transaction_amount
sammy, 1234, 31/10/2015, 10000
john, 9999, 3/12/2015, 12321
任务流程如下:
- 读取每条交易记录;
- 调用外部 REST 接口,根据
userid
查询用户的age
和postCode
; - 将补充信息写入最终的 XML 报表。
核心处理逻辑在 ItemProcessor
中实现:
public class RetryItemProcessor implements ItemProcessor<Transaction, Transaction> {
@Override
public Transaction process(Transaction transaction) throws IOException {
log.info("RetryItemProcessor, attempting to process: {}", transaction);
HttpResponse response = fetchMoreUserDetails(transaction.getUserId());
// 解析 age 和 postCode 并更新 transaction
...
return transaction;
}
...
}
最终生成的 XML 输出示例:
<transactionRecord>
<transactionRecord>
<amount>10000.0</amount>
<transactionDate>2015-10-31 00:00:00</transactionDate>
<userId>1234</userId>
<username>sammy</username>
<age>10</age>
<postCode>430222</postCode>
</transactionRecord>
...
</transactionRecord>
3. 为 ItemProcessor 添加重试机制
如果在调用 REST 接口时因网络延迟导致超时(ConnectTimeoutException
),整个作业就会中断——这显然不合理。我们更希望对这类可恢复异常进行自动重试。
✅ 解决方案:在 Step 配置中启用容错机制,并指定重试策略。
@Bean
public Step retryStep(
ItemProcessor<Transaction, Transaction> processor,
ItemWriter<Transaction> writer) throws ParseException {
return stepBuilderFactory
.get("retryStep")
.<Transaction, Transaction>chunk(10)
.reader(itemReader(inputCsv))
.processor(processor)
.writer(writer)
.faultTolerant() // 启用容错(必须先开启)
.retryLimit(3) // 最多重试 3 次
.retry(ConnectTimeoutException.class) // 指定哪些异常触发重试
.retry(DeadlockLoserDataAccessException.class)
.build();
}
⚠️ 注意要点:
faultTolerant()
是前提,不开启则后续的 retry 配置无效;retryLimit(n)
定义单个 item 的最大重试次数(含首次执行);retry(Class)
可多次调用,添加多个需要重试的异常类型;- 重试是按 item 粒度进行的,不会影响整个 chunk。
4. 重试机制测试验证
✅ 场景一:前两次失败,第三次成功
我们模拟 REST 接口前两次调用超时,第三次恢复正常:
@Test
public void whenEndpointFailsTwicePasses3rdTime_thenSuccess() throws Exception {
FileSystemResource expectedResult = new FileSystemResource("expected-output.xml");
FileSystemResource actualResult = new FileSystemResource("actual-output.xml");
when(httpResponse.getEntity())
.thenReturn(new StringEntity("{ \"age\":10, \"postCode\":\"430222\" }"));
// 前两次抛出超时异常,第三次返回正常响应
when(httpClient.execute(any()))
.thenThrow(new ConnectTimeoutException("Timeout count 1"))
.thenThrow(new ConnectTimeoutException("Timeout count 2"))
.thenReturn(httpResponse);
JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
JobInstance actualJobInstance = jobExecution.getJobInstance();
ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
assertThat(actualJobInstance.getJobName(), is("retryBatchJob"));
assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED"));
AssertFile.assertFileEquals(expectedResult, actualResult);
}
日志输出清晰地展示了重试过程:
19:06:57.742 [main] INFO o.s.batch.core.job.SimpleStepHandler - Executing step: [retryStep]
19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234
19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234
19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=1234
19:06:57.758 [main] INFO o.b.batch.service.RetryItemProcessor - Attempting to process user with id=9999
19:06:57.773 [main] INFO o.s.batch.core.step.AbstractStep - Step: [retryStep] executed in 31ms
可以看到,id=1234
的记录被处理了三次(前两次失败,第三次成功),之后才继续处理下一条。
❌ 场景二:重试耗尽,作业最终失败
如果接口持续不可用,重试次数用尽后作业应正常标记为失败:
@Test
public void whenEndpointAlwaysFail_thenJobFails() throws Exception {
when(httpClient.execute(any()))
.thenThrow(new ConnectTimeoutException("Endpoint is down"));
JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
JobInstance actualJobInstance = jobExecution.getJobInstance();
ExitStatus actualJobExitStatus = jobExecution.getExitStatus();
assertThat(actualJobInstance.getJobName(), is("retryBatchJob"));
assertThat(actualJobExitStatus.getExitCode(), is("FAILED"));
assertThat(actualJobExitStatus.getExitDescription(),
containsString("org.apache.http.conn.ConnectTimeoutException"));
}
此时,系统会对第一条记录执行 3 次重试(共 4 次尝试),最终仍失败,导致整个 Step 失败。
5. XML 配置方式
除了 Java Config,Spring Batch 也支持 XML 方式配置重试逻辑,适用于传统项目或偏好 XML 的团队:
<batch:job id="retryBatchJob">
<batch:step id="retryStep">
<batch:tasklet>
<batch:chunk reader="itemReader"
writer="itemWriter"
processor="retryItemProcessor"
commit-interval="10"
retry-limit="3">
<batch:retryable-exception-classes>
<batch:include class="org.apache.http.conn.ConnectTimeoutException"/>
<batch:include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
</batch:retryable-exception-classes>
</batch:chunk>
</batch:tasklet>
</batch:step>
</batch:job>
📌 对比 Java 配置,XML 中的关键属性:
retry-limit="3"
→ 相当于.retryLimit(3)
<batch:include>
→ 相当于.retry(Exception.class)
6. 总结
本文通过一个实际场景,演示了如何在 Spring Batch 中配置重试机制来应对临时性故障:
- 使用
.faultTolerant()
开启容错; - 通过
.retry()
和.retryLimit()
精确控制重试行为; - 支持 Java 和 XML 两种配置方式;
- 重试基于 item 粒度,不影响整体 chunk 提交;
- 建议结合单元测试验证重试逻辑是否按预期工作。
💡 踩坑提醒:不要对所有异常都开启重试!应只针对幂等操作和明确可恢复的异常(如网络超时、死锁等)进行重试,避免重复扣款、数据重复等问题。
完整示例代码已上传至 GitHub:https://github.com/example/spring-batch-retry-demo