1. 简介

在 Spring Batch 作业执行过程中,默认情况下,任何阶段出现异常都会导致当前 Step 直接失败。但在实际开发中,我们往往希望对某些特定异常采取“跳过当前数据项”的策略,而不是直接中断整个批处理流程。

本文将介绍在 Spring Batch 中实现跳过逻辑的两种方式,帮助你在面对脏数据或可容忍异常时,让作业更健壮、更具容错能力。✅


2. 使用场景

为了便于演示,我们沿用之前 Spring Batch 入门文章 中提到的一个基于块(chunk-oriented)的简单作业:将 CSV 格式的金融交易数据转换为 XML 输出。

2.1 输入数据

我们在原始 CSV 文件中加入几条“问题数据”:

username, user_id, transaction_date, transaction_amount
devendra, 1234, 31/10/2015, 10000
john, 2134, 3/12/2015, 12321
robin, 2134, 2/02/2015, 23411
, 2536, 3/10/2019, 100
mike, 9876, 5/11/2018, -500
, 3425, 10/10/2017, 9999

可以看到:

  • 第4行和第7行:username 字段为空 ❌
  • 第6行:transaction_amount 为负数(-500),不符合业务规则 ❌

接下来我们将配置作业,让这些“脏数据”被自动跳过,而不是导致整个 Step 失败。


3. 配置跳过限制与可跳过异常

3.1 使用 skipskipLimit

Spring Batch 提供了简单粗暴但非常实用的跳过机制:通过 .skip() 指定哪些异常可以跳过,再用 .skipLimit() 设置最多跳过多少条记录。

⚠️ 注意:必须先调用 .faultTolerant() 才能启用跳过功能,否则配置无效。

示例代码如下:

@Bean
public Step skippingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, 
  ItemProcessor<Transaction, Transaction> processor,
  ItemWriter<Transaction> writer) throws ParseException {
    return new StepBuilder("skippingStep", jobRepository)
      .<Transaction, Transaction> chunk(10, transactionManager)
      .reader(itemReader(invalidInputCsv))
      .processor(processor)
      .writer(writer)
      .faultTolerant()
      .skipLimit(2)
      .skip(MissingUsernameException.class)
      .skip(NegativeAmountException.class)
      .build();
}

上面这段配置的含义是:

  • ✅ 当发生 MissingUsernameExceptionNegativeAmountException 时,跳过当前项
  • ✅ 最多允许跳过 2 条记录
  • ❌ 如果同类或不同类异常累计触发第 3 次,则 Step 直接失败

💡 跳过发生在读取、处理或写入任一阶段。只要抛出被声明为可跳过的异常,当前 item 就会被丢弃并计入 skip 计数器。


3.2 使用 noSkip 实现黑名单式控制

有时候我们不关心“哪些异常能跳”,反而更关注“哪些异常必须中断流程”。比如:大多数异常都可以容忍跳过,唯独 XML 解析错误不能接受

这时可以用 skip(Exception.class) 配合 noSkip() 实现“白名单跳过 + 黑名单阻断”模式。

@Bean
public Step skippingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, 
  ItemProcessor<Transaction, Transaction> processor,
  ItemWriter<Transaction> writer) throws ParseException {
    return new StepBuilder("skippingStep", jobRepository)
      .<Transaction, Transaction> chunk(10, transactionManager)
      .reader(itemReader(invalidInputCsv))
      .processor(processor)
      .writer(writer)
      .faultTolerant()
      .skipLimit(2)
      .skip(Exception.class)
      .noSkip(SAXException.class)
      .build();
}

上述配置表示:

  • ✅ 除 SAXException 外,所有异常在限额内都可跳过
  • ❌ 一旦发生 SAXException,立即终止 Step,不管是否在 skip 限额内
  • 🔁 skip()noSkip() 的调用顺序不影响最终行为

📌 这种写法适合异常种类繁多但只有少数关键异常需要严格处理的场景,避免写一堆 skip()


4. 使用自定义 SkipPolicy

当简单的异常分类 + 数量限制无法满足业务需求时,就需要上“高级玩法”——实现 SkipPolicy 接口来自定义跳过判断逻辑。

4.1 为什么需要自定义?

举个真实踩坑场景:
你允许跳过金额为负的交易,但仅限于小额(比如 -100 元以内),如果是 -5000 元的异常大额负数,很可能是系统 bug,必须报警并中断作业。

此时仅靠 .skip(NegativeAmountException.class) 显然不够用了。

4.2 实现 CustomSkipPolicy

public class CustomSkipPolicy implements SkipPolicy {

    private static final int MAX_SKIP_COUNT = 2;
    private static final int INVALID_TX_AMOUNT_LIMIT = -1000;

    @Override
    public boolean shouldSkip(Throwable throwable, int skipCount) 
      throws SkipLimitExceededException {

        // 缺少用户名:只要没超限就跳过
        if (throwable instanceof MissingUsernameException && skipCount < MAX_SKIP_COUNT) {
            return true;
        }

        // 负金额:需进一步判断金额大小
        if (throwable instanceof NegativeAmountException && skipCount < MAX_SKIP_COUNT) {
            NegativeAmountException ex = (NegativeAmountException) throwable;
            if (ex.getAmount() < INVALID_TX_AMOUNT_LIMIT) {
                return false; // 超出阈值,不允许跳过
            } else {
                return true;  // 小额负数,允许跳过
            }
        }

        return false;
    }
}

4.3 在 Step 中使用自定义策略

@Bean
public Step skippingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, 
     ItemProcessor<Transaction, Transaction> processor,
     ItemWriter<Transaction> writer) throws ParseException {
     return new StepBuilder("skippingStep", jobRepository)
       .<Transaction, Transaction> chunk(10, transactionManager)
       .reader(itemReader(invalidInputCsv))
       .processor(processor)
       .writer(writer)
       .faultTolerant()
       .skipPolicy(new CustomSkipPolicy())
       .build();
}

📌 关键点:

  • 不再使用 .skip().noSkip(),而是用 .skipPolicy()
  • 必须仍保留 .faultTolerant() 启用容错
  • shouldSkip() 方法接收异常对象和当前已跳过次数,返回 true 表示跳过,false 表示应失败

✅ 优势:灵活度极高,可结合业务上下文、异常属性、统计信息等做复杂决策。
❌ 缺点:代码量增加,需注意线程安全(通常无状态实现即可)。


5. 总结

方式 适用场景 灵活性 推荐指数
skip() + skipLimit() 异常类型明确、规则简单 ⭐⭐ ✅✅✅✅
noSkip() 配合 skip(Exception) 大部分异常可容忍,少数致命 ⭐⭐⭐ ✅✅✅✅
自定义 SkipPolicy 需要基于异常内容动态决策 ⭐⭐⭐⭐⭐ ✅✅✅✅✅(特定场景)

💡 经验建议

  • 日常开发中,优先使用 skip/skipLimit 组合,够用又清晰
  • 只有当跳过逻辑涉及异常字段值、累计趋势、外部状态时,才考虑 SkipPolicy
  • 别忘了监控跳过的 item 数量!可通过 StepExecution.getSkipCount() 做告警

所有示例代码已托管至 GitHub:https://github.com/techblog-tutorials/spring-batch-skip-logic


原始标题:Configuring Skip Logic in Spring Batch