ExecutionContext 详解
本章定位:深入理解 ExecutionContext——Spring Batch 的"中间记事本",Step 间数据传递和断点续传的关键机制。
定义与作用
ExecutionContext 是 Spring Batch 中的键值对集合(类似 Map),用于在批处理执行过程中存储和传递状态信息。它分为两个级别:
| 级别 | 作用域 | 存储位置 | 生命周期 |
|---|---|---|---|
| Job ExecutionContext | 整个 Job 内所有 Step 共享 | BATCH_JOB_EXECUTION_CONTEXT | 从 Job 启动到结束 |
| Step ExecutionContext | 单个 Step 内部 | BATCH_STEP_EXECUTION_CONTEXT | 从 Step 启动到结束(重启时恢复) |
飞翔科技架构师白歌的比喻:
"Job ExecutionContext 是贴在集装箱上的运单——所有环节都能看到。Step ExecutionContext 是每个工人自己的笔记本——记到第几行了、当前页到哪了,别人不需要知道,但下次接着干时必须能拿出来继续用。"
核心原理
ExecutionContext 的读写和持久化时机
put vs putInt / putLong
ExecutionContext ctx = stepExecution.getExecutionContext();
// ✅ 推荐:类型安全的方法,值不可变
ctx.putInt("current.line", 542);
ctx.putLong("total.processed", 5000L);
ctx.putString("last.processed.id", "STU-00499");
// ⚠️ 谨慎使用:put() 存入 Object,值被序列化后可能丢失类型信息
ctx.put("config", someComplexObject);
完整示例
场景一:飞翔科技——Step 间数据传递
背景:小崔的作业有两个 Step:Step 1 统计无效记录数,Step 2 根据无效记录数决定是否发送告警。
// Step 1: 统计无效记录并存入 Job ExecutionContext
@Component
public class InvalidRecordCounter implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
int invalidCount = countInvalidRecords();
System.out.println("发现无效记录: " + invalidCount + " 条");
// 存入 Job ExecutionContext(跨 Step 共享)
chunkContext.getStepContext().getStepExecution()
.getJobExecution().getExecutionContext()
.putInt("invalid.count", invalidCount);
return RepeatStatus.FINISHED;
}
}
// Step 2: 从 Job ExecutionContext 读取并决策
@Component
public class AlertDecisionTasklet implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
// 从 Job ExecutionContext 读取
ExecutionContext jobCtx = chunkContext.getStepContext()
.getStepExecution().getJobExecution().getExecutionContext();
int invalidCount = jobCtx.getInt("invalid.count", 0);
System.out.println("Job Context 中的无效记录数: " + invalidCount);
if (invalidCount > 100) {
System.out.println("发送告警: 无效记录数超过阈值");
// 发送告警逻辑
} else {
System.out.println("无效记录在容忍范围内");
}
return RepeatStatus.FINISHED;
}
}
运行结果:
Step 1 [countStep]: 发现无效记录: 156 条
Step 1 [countStep]: COMPLETED
Step 2 [alertStep]: Job Context 中的无效记录数: 156
Step 2 [alertStep]: 发送告警: 无效记录数超过阈值
Step 2 [alertStep]: COMPLETED
场景二:断点续传——从失败点恢复
@Component
public class ResumableItemReader implements ItemReader<String>, ItemStream {
private int currentLine = 0;
private List<String> lines;
@Override
public void open(ExecutionContext executionContext) {
// 重启时从 ExecutionContext 恢复上次位置
if (executionContext.containsKey("current.line")) {
currentLine = executionContext.getInt("current.line");
System.out.println("从第 " + currentLine + " 行恢复");
} else {
currentLine = 0;
}
lines = Files.readAllLines(Paths.get("data.txt"));
}
@Override
public String read() {
if (currentLine >= lines.size()) return null;
return lines.get(currentLine++);
}
@Override
public void update(ExecutionContext executionContext) {
// 每个 Chunk 提交后,框架调用此方法保存进度
executionContext.putInt("current.line", currentLine);
}
@Override
public void close() { /* 无需特殊处理 */ }
}
操作前后对比:
| 场景 | 不使用 ExecutionContext | 使用 ExecutionContext |
|---|---|---|
| 处理到第 542 行时崩溃 | 从头重新处理 542 行 | 从第 542 行继续 |
| 10 万条数据处理 | 崩溃后全重做 | 仅重做崩溃 Chunk 的 50 条 |
| 处理时间 | O(N) 每次都重跑全量 | O(N) 仅一次 + 崩溃后补少量 |
易错场景与避坑
反例一:在 ExecutionContext 中存大对象
// ❌ 存入整个对象列表 → 序列化开销大,可能导致 OOM
ExecutionContext ctx = stepExecution.getExecutionContext();
ctx.put("all.orders", orderList); // 5 万条 Order 对象!
正确做法:只存恢复点(行号、ID),不存完整数据。
反例二:忘记实现 ItemStream
// ❌ Reader 没实现 ItemStream → 框架无法调用 open/update/close
public class MyReader implements ItemReader<String> {
// 只实现了 read() → 无法保存/恢复状态
}
正确做法:需要状态管理的 Reader/Writer 必须实现 ItemStream,在 open() 中恢复,在 update() 中保存。
面试高频考点
Q1:ExecutionContext 何时被持久化?
每个 Chunk 提交后,框架调用
ItemStream.update(),然后 JobRepository 将 Step ExecutionContext 序列化到BATCH_STEP_EXECUTION_CONTEXT表。Job ExecutionContext 在 Job 执行结束时持久化。
Q2:如何跨 Step 传递数据?
通过 Job ExecutionContext:
stepExecution.getJobExecution().getExecutionContext().put("key", value)。Job 内的所有 Step 都能访问这个共享的 ExecutionContext。
上一章:Step 配置下一章:ItemReader 详解