🏆本文收录于《滚雪球学SpringBoot 3.x》,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
该专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接订阅 《Spring Boot实战合集》,一次订阅,持续学习,后续更新内容无需重复付费,适合长期收藏与系统进阶。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

全文目录:

一、为什么你需要 Spring Batch?

在很多初学者眼里,“批处理”听上去像是一个过时的概念:

  • 我有定时任务,定时扫表不就行了吗?
  • 我有消息队列,消息一条条消费不就行了吗?
  • 我有普通 Java 程序,写个 for 循环批量处理不就好了?

这些做法在小规模场景里往往都能工作,但一旦你面对下面这些问题,普通写法很快就会暴露出短板:

  • 数据量非常大,单次处理可能有几十万、几百万条记录。
  • 任务运行时间很长,可能跨分钟、跨小时甚至跨天。
  • 中途容易失败,失败后需要从上次进度继续,而不是从头再来。
  • 需要精确记录每次处理了多少条、成功多少、失败多少、跳过多少。
  • 需要对任务运行历史进行追踪,方便审计、运维、报警。
  • 需要将“读数据、加工数据、写数据”这件事标准化。

Spring Batch 就是为这些问题而生的。它不是“定时任务框架”,而是一套批处理作业框架。它关心的是:

  • 任务如何定义
  • 步骤如何拆分
  • 数据如何分批读取与写入
  • 失败后如何恢复
  • 执行过程如何追踪

这也是为什么 Spring Batch 常常出现在:

  • 数据迁移
  • 日终结算
  • 报表生成
  • 历史归档
  • 文件导入导出
  • ETL 作业
  • 大批量状态同步

这些场景里。

二、批处理到底适合解决什么问题?

先给结论:只要你的任务具备“离线、批量、可恢复、可审计”的特征,就很适合考虑 Spring Batch。

2.1 典型适用场景

1)大批量数据导入

比如把 Excel、CSV、JSON、数据库中的旧数据导入到新系统里。通常会涉及:

  • 读取外部文件
  • 校验数据合法性
  • 转换字段格式
  • 分批写入数据库
  • 记录失败行

2)大批量数据清洗

例如将历史订单数据按照新规则重新计算、去重、补字段、归档到新表。

3)跨系统同步

例如从 A 系统同步商品信息到 B 系统,并且要保证可重试、可恢复。

4)统计和报表

例如每天凌晨生成订单统计报表,并把结果存到报表库或导出成文件。

5)历史归档与清理

例如将一年前的数据迁移到历史表,然后清理主表。

2.2 不适合的场景

Spring Batch 不是银弹。下面这些场景通常不是它的最佳选择:

  • 低延迟在线请求:比如用户点击按钮后要立即返回结果。
  • 高频实时流处理:比如秒级、毫秒级的事件流。
  • 逻辑极其简单、一次性就能完成的小脚本。

一句话概括:

在线请求更关心“快”,批处理更关心“稳”。

三、Spring Batch 5.x 的核心模型总览

Spring Batch 最核心的思想,是把一个批任务拆成几个标准角色:

  • Job:整个批任务的容器
  • Step:Job 中的一个执行步骤
  • Chunk:Step 中常见的一种批处理提交单位
  • Tasklet:Step 的另一种执行方式,适合一次性逻辑
  • ItemReader / ItemProcessor / ItemWriter:Chunk 模式中的读、处理、写三件套
  • JobRepository:记录作业元数据
  • JobLauncher:负责启动 Job
  • JobExecution / StepExecution:作业与步骤的执行记录

下面先看一个最重要的结构图。

这个图背后的重点是:

  1. Job 不是干活的,它只是组织流程。
  2. Step 才是真正执行某段业务逻辑的地方。
  3. Chunk 是“分批处理”的模式。
  4. Tasklet 是“执行一次就结束”的模式。

四、Job、Step、Chunk、Tasklet 的职责拆解

这一部分是全文的核心,也是最容易混淆的地方。

4.1 Job:批处理作业的总入口

Job 可以理解为“一个完整的批任务定义”。它负责把多个 Step 串起来,定义执行顺序和流程控制。

你可以把 Job 想象成一部电影的总片名,而 Step 是每个镜头或章节。

Job 的特点:

  • 一个 Job 对应一个完整业务目标
  • 可以包含多个 Step
  • 可以按顺序执行,也可以做条件分支
  • 每次执行都会生成执行实例和执行记录

Job 本身不直接处理数据,它只是调度和组织。

4.2 Step:真正干活的步骤

Step 是 Job 中最小的业务执行单元。每一个 Step 都可以做一件明确的事情,比如:

  • 读取 CSV 文件
  • 校验记录
  • 写入数据库
  • 发送统计结果
  • 清理临时文件

一个 Job 可以只有一个 Step,也可以有很多 Step。

常见设计原则是:

  • 一个 Step 尽量只做一件事
  • Step 与 Step 之间通过上下文或中间表传递数据
  • Step 的职责越清晰,后期维护越容易

4.3 Chunk:读、处理、写的批量提交模型

Chunk 是 Spring Batch 中最经典的处理模式。

它的工作方式可以概括为:

  1. 读入一条数据
  2. 处理一条数据
  3. 累积到设定数量
  4. 一次性写入
  5. 提交事务
  6. 重复以上步骤

这里的“chunk size”就是每次提交多少条数据,比如 10、100、1000。

Chunk 的优势:

  • 事务边界清晰
  • 适合大批量数据
  • 失败后可恢复
  • 内置跳过、重试、监听能力强

Chunk 的典型三件套:

  • ItemReader:负责读
  • ItemProcessor:负责处理
  • ItemWriter:负责写

4.4 Tasklet:一次性执行逻辑

Tasklet 更像是“执行一段命令型逻辑”,例如:

  • 删除临时文件
  • 初始化目录
  • 执行一条 SQL
  • 调用外部接口
  • 发一封通知邮件

Tasklet 的特点:

  • 没有“读-处理-写”的固定结构
  • 更适合一次性、非批量的任务
  • 一般返回 RepeatStatus.FINISHED 表示结束

4.5 它们之间的关系

可以用一张图来理解:

结论非常简单:

  • Job 是总导演
  • Step 是分镜
  • Chunk 是流水线
  • Tasklet 是一次性动作

五、Chunk 模式:批处理最常见的执行模型

Chunk 模式是 Spring Batch 的灵魂。

5.1 Chunk 为什么适合大数据

假设你要处理 100 万条数据,如果每条都单独提交事务,性能会很差;如果一次性加载全部数据,又容易内存爆掉。

Chunk 的解决方案是:

  • 每次只处理一小批
  • 处理完再统一提交
  • 既控制内存,又兼顾效率

例如 chunk size = 100,那么执行过程可能是:

  • 读 100 条
  • 处理 100 条
  • 写 100 条
  • 提交事务
  • 再处理下一批

这样既减少数据库交互次数,也让失败恢复更容易。

5.2 Chunk 的事务边界

这是必须搞懂的点:

  • 一个 chunk 通常对应一个事务
  • 事务里如果某条数据出错,整个 chunk 可能回滚
  • 如果配置了跳过、重试策略,框架可以更智能地处理异常

所以 Chunk 不是简单的“每 100 条提交一次”,它还承担了事务控制

5.3 Chunk 的执行流程

相关示意图绘制如下,仅供参考:

这个过程体现了一个关键思想:

Spring Batch 的 Chunk 本质上是在“批量处理”和“事务一致性”之间找平衡。

六、Tasklet 模式:一次性任务的利器

Tasklet 不是 Chunk 的替代品,而是补充品。

6.1 什么时候用 Tasklet

适合以下场景:

  • 只执行一条 SQL
  • 只创建一个文件
  • 只做一个清理动作
  • 只发一次消息
  • 只写一个日志标记

比如,导入任务前要先清理临时目录,或者数据处理完成后要给运维发通知,这类动作很适合 Tasklet。

6.2 Tasklet 的核心接口

Spring Batch 的 Tasklet 本质上是一个函数式执行器。你通常会实现类似下面的逻辑:

  • 执行任务
  • 返回 RepeatStatus.FINISHED

如果返回继续状态,就可能被重复调用;如果返回结束状态,就退出当前 Step。

6.3 Tasklet 适合“命令式”任务

和 Chunk 相比,Tasklet 不强调“输入一批,处理一批,输出一批”,而强调“做完一件事就结束”。

所以不要把所有逻辑都塞进 Chunk。很多系统初期会犯一个错误:

明明只是一个目录清理任务,硬要搞成 Reader/Processor/Writer 三件套。

这样只会让代码更难维护。

七、Spring Boot 3.x + Spring Batch 5.x 项目搭建

下面进入实战部分。为了让代码更有参考价值,本章使用 Spring Boot 3.x + Spring Batch 5.x 的方式来搭建一个最小可运行项目。

7.1 依赖配置

下面以 Maven 为例。

<dependencies>
    <!-- Spring Web,可选:如果你希望通过接口触发 Job -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Batch -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-batch</artifactId>
    </dependency>

    <!-- JDBC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <!-- H2 数据库,方便本地演示 -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok,可选 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

7.2 application.yml

spring:
  datasource:
    url: jdbc:h2:mem:batchdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true
  batch:
    jdbc:
      initialize-schema: always
    job:
      enabled: false

logging:
  level:
    org.springframework.batch: info
    com.example.batch: debug

这里有两个重点:

  1. initialize-schema: always 用于自动初始化 Spring Batch 元数据表。
  2. job.enabled: false 表示应用启动时不自动执行 Job,方便我们手动触发。

7.3 数据表准备

我们准备一个目标表用于保存导入结果。

CREATE TABLE student_target (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    student_no VARCHAR(32),
    student_name VARCHAR(64),
    age INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

7.4 项目结构建议

建议把批处理项目按下面方式组织:

com.example.batch
├── BatchApplication.java
├── config
│   ├── BatchJobConfig.java
│   └── BatchCommonConfig.java
├── model
│   └── StudentRecord.java
├── reader
│   └── StudentItemReader.java
├── processor
│   └── StudentItemProcessor.java
├── writer
│   └── StudentItemWriter.java
├── listener
│   └── BatchJobListener.java
└── controller
    └── JobTriggerController.java

这样做的好处是:职责清晰,后面扩展多个 Job 也不会乱。

八、第一个可运行批处理案例:CSV 转数据库

这一章我们做一个真正能跑的例子:把 CSV 文件中的学生数据导入数据库。

这个案例会同时展示:

  • Job
  • Step
  • Chunk
  • ItemReader
  • ItemProcessor
  • ItemWriter
  • 监听器
  • 手动触发 Job

8.1 CSV 文件样例

放在 src/main/resources/input/students.csv

studentNo,studentName,age
S001,张三,18
S002,李四,20
S003,王五,19
S004,赵六,21

8.2 数据模型

package com.example.batch.model;

/**
 * 学生记录模型
 */
public class StudentRecord {

    private String studentNo;
    private String studentName;
    private Integer age;

    public String getStudentNo() {
        return studentNo;
    }

    public void setStudentNo(String studentNo) {
        this.studentNo = studentNo;
    }

    public String getStudentName() {
        return studentName;
    }

    public void setStudentName(String studentName) {
        this.studentName = studentName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

8.3 批处理配置类

package com.example.batch.config;

import com.example.batch.model.StudentRecord;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class BatchJobConfig {

    /**
     * 读取 CSV 文件
     */
    @Bean
    public FlatFileItemReader<StudentRecord> studentItemReader() {
        return new FlatFileItemReaderBuilder<StudentRecord>()
                .name("studentItemReader")
                .resource(new ClassPathResource("input/students.csv"))
                .linesToSkip(1)
                .delimited()
                .names("studentNo", "studentName", "age")
                .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{
                    setTargetType(StudentRecord.class);
                }})
                .build();
    }

    /**
     * 数据处理:这里做简单校验和转换
     */
    @Bean
    public ItemProcessor<StudentRecord, StudentRecord> studentItemProcessor() {
        return item -> {
            // 中文注释:对每条记录进行基础清洗
            if (item.getStudentName() == null || item.getStudentName().isBlank()) {
                return null; // 返回 null 表示过滤掉该条记录
            }
            item.setStudentName(item.getStudentName().trim());
            return item;
        };
    }

    /**
     * 批量写入数据库
     */
    @Bean
    public JdbcBatchItemWriter<StudentRecord> studentItemWriter(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<StudentRecord>()
                .dataSource(dataSource)
                .sql("INSERT INTO student_target(student_no, student_name, age) VALUES(:studentNo, :studentName, :age)")
                .beanMapped()
                .build();
    }

    /**
     * Step:Chunk 模式
     */
    @Bean
    public Step importStudentStep(JobRepository jobRepository,
                                  PlatformTransactionManager transactionManager,
                                  ItemReader<StudentRecord> studentItemReader,
                                  ItemProcessor<StudentRecord, StudentRecord> studentItemProcessor,
                                  ItemWriter<StudentRecord> studentItemWriter) {
        return new StepBuilder("importStudentStep", jobRepository)
                .<StudentRecord, StudentRecord>chunk(2, transactionManager)
                .reader(studentItemReader)
                .processor(studentItemProcessor)
                .writer(studentItemWriter)
                .build();
    }

    /**
     * Job:把 Step 串起来
     */
    @Bean
    public Job importStudentJob(JobRepository jobRepository,
                                Step importStudentStep,
                                BatchJobListener batchJobListener) {
        return new JobBuilder("importStudentJob", jobRepository)
                .listener(batchJobListener)
                .start(importStudentStep)
                .build();
    }
}

代码解析

这里最值得注意的点有三个。

第一,chunk(2, transactionManager)

表示每两条数据提交一次事务。真实项目里这个值需要结合:

  • 单条数据大小
  • 数据库压力
  • 内存占用
  • 失败恢复成本

综合评估后决定。

第二,Processor 返回 null

Spring Batch 允许 Processor 返回 null,表示过滤当前数据,不写入结果。这是一个很实用的特性,常用于:

  • 过滤非法数据
  • 过滤空记录
  • 过滤重复记录
第三,JobRepository 和 transactionManager

Spring Batch 5.x 明确强调 Job 元数据管理,所以 JobRepositoryPlatformTransactionManager 等配置非常关键。它们决定了作业执行记录、事务边界和恢复能力。

8.4 监听器

package com.example.batch.listener;

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.stereotype.Component;

/**
 * 作业监听器:用于记录作业开始和结束状态
 */
@Component
public class BatchJobListener extends JobExecutionListenerSupport {

    @Override
    public void beforeJob(JobExecution jobExecution) {
        // 中文注释:作业执行前打印日志
        System.out.println("批处理作业开始执行,jobName=" + jobExecution.getJobInstance().getJobName());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        // 中文注释:作业执行后打印状态
        System.out.println("批处理作业执行结束,status=" + jobExecution.getStatus());
    }
}

8.5 手动触发 Job 的接口

package com.example.batch.controller;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

@RestController
@RequestMapping("/batch")
public class JobTriggerController {

    private final JobLauncher jobLauncher;
    private final Job importStudentJob;

    public JobTriggerController(JobLauncher jobLauncher, Job importStudentJob) {
        this.jobLauncher = jobLauncher;
        this.importStudentJob = importStudentJob;
    }

    @PostMapping("/import-students")
    public String runImportJob() throws Exception {
        // 中文注释:增加唯一参数,避免同一 JobParameters 重复执行被判定为同一次实例
        JobParameters jobParameters = new JobParametersBuilder()
                .addString("requestId", LocalDateTime.now().toString())
                .toJobParameters();

        jobLauncher.run(importStudentJob, jobParameters);
        return "job started";
    }
}

代码解析

这里的 requestId 很重要。

Spring Batch 会用 JobParameters 来区分一次作业实例。如果参数完全相同,框架可能认为这次执行和上次是同一个实例,从而出现“不能重复运行”的问题。

所以手动触发 Job 时,通常会加一个唯一参数:

  • 时间戳
  • UUID
  • 请求流水号

8.6 启动类

package com.example.batch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

8.7 这个案例的执行逻辑

当你调用 /batch/import-students 时:

  1. Controller 触发 JobLauncher
  2. JobLauncher 启动 importStudentJob
  3. Job 进入 importStudentStep
  4. Reader 读 CSV
  5. Processor 做清洗
  6. Writer 批量写入数据库
  7. 监听器记录开始和结束

这就是一个完整的 Spring Batch 批处理链路。

九、进阶案例:失败重试、跳过与断点恢复

真正的批处理系统,最有价值的不是“能跑”,而是“跑崩了也能恢复”。

Spring Batch 在这方面非常强。

9.1 为什么断点恢复重要

假设你正在处理 100 万条数据,已经处理到第 60 万条时,机器宕机了。

如果没有断点恢复,你只能从头再来。

如果有断点恢复,你只需要从第 60 万条之后继续。

这对大任务非常重要,因为:

  • 节省时间
  • 节省资源
  • 降低重复写入风险
  • 避免重新扫全量数据

9.2 Spring Batch 如何支持恢复

Spring Batch 会把作业执行信息持久化到元数据表中,例如:

  • JobInstance
  • JobExecution
  • StepExecution
  • ExecutionContext

这些表记录了作业运行到哪里、读到哪里、成功多少、失败多少。

只要你的 Reader 支持状态保存,作业就可以在失败后从断点继续。

9.3 使用 ExecutionContext 保存状态

下面给一个简单思路:记录当前处理到哪一行。

package com.example.batch.reader;

import com.example.batch.model.StudentRecord;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStreamReader;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;
import org.springframework.core.io.ClassPathResource;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * 一个示例性的可恢复 Reader,用于演示断点恢复思想
 */
public class StatefulStudentReader implements ItemStreamReader<StudentRecord> {

    private BufferedReader bufferedReader;
    private int lineNumber = 0;
    private int currentIndex = 0;

    @Override
    public StudentRecord read() throws Exception {
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            currentIndex++;

            // 中文注释:跳过表头
            if (currentIndex == 1) {
                continue;
            }

            lineNumber = currentIndex;

            String[] parts = line.split(",");
            if (parts.length < 3) {
                // 中文注释:数据格式不正确,直接抛异常交给框架处理
                throw new IllegalArgumentException("CSV 数据格式错误,行号=" + currentIndex);
            }

            StudentRecord record = new StudentRecord();
            record.setStudentNo(parts[0]);
            record.setStudentName(parts[1]);
            record.setAge(Integer.parseInt(parts[2]));
            return record;
        }
        return null;
    }

    @Override
    public void open(ExecutionContext executionContext) throws Exception {
        // 中文注释:打开文件时恢复上次进度
        InputStream inputStream = new ClassPathResource("input/students.csv").getInputStream();
        bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

        int savedLine = executionContext.containsKey("lineNumber") ? executionContext.getInt("lineNumber") : 0;
        for (int i = 0; i < savedLine; i++) {
            bufferedReader.readLine();
            currentIndex++;
        }
    }

    @Override
    public void update(ExecutionContext executionContext) throws Exception {
        // 中文注释:保存当前进度,供下次恢复使用
        executionContext.putInt("lineNumber", lineNumber);
    }

    @Override
    public void close() throws Exception {
        if (bufferedReader != null) {
            bufferedReader.close();
        }
    }
}

代码解析

这个 Reader 的核心价值,不在于它是否“最优雅”,而在于它展示了 Spring Batch 断点恢复的思想:

  • open:恢复历史进度
  • update:保存当前进度
  • close:释放资源

这是 Batch 任务与普通脚本最大的不同之一。

9.4 Retry:重试机制

有些错误是临时性的,例如:

  • 网络抖动
  • 数据库短暂不可用
  • 外部接口超时

这种情况没必要直接失败,可以先重试几次。

Spring Batch 支持在 Step 里配置重试:

@Bean
public Step importStudentStep(JobRepository jobRepository,
                              PlatformTransactionManager transactionManager,
                              ItemReader<StudentRecord> studentItemReader,
                              ItemProcessor<StudentRecord, StudentRecord> studentItemProcessor,
                              ItemWriter<StudentRecord> studentItemWriter) {
    return new StepBuilder("importStudentStep", jobRepository)
            .<StudentRecord, StudentRecord>chunk(2, transactionManager)
            .reader(studentItemReader)
            .processor(studentItemProcessor)
            .writer(studentItemWriter)
            .faultTolerant()
            .retryLimit(3)
            .retry(RuntimeException.class)
            .readerIsTransactionalQueue()
            .build();
}

这里的含义是:

  • 遇到 RuntimeException 时重试
  • 最多重试 3 次

9.5 Skip:跳过坏数据

有些数据坏了,但整个任务不能因为少量坏数据而失败。比如 10 万条里只有 3 条格式不合法。

这时候可以配置跳过策略。

@Bean
public Step importStudentStep(JobRepository jobRepository,
                              PlatformTransactionManager transactionManager,
                              ItemReader<StudentRecord> studentItemReader,
                              ItemProcessor<StudentRecord, StudentRecord> studentItemProcessor,
                              ItemWriter<StudentRecord> studentItemWriter) {
    return new StepBuilder("importStudentStep", jobRepository)
            .<StudentRecord, StudentRecord>chunk(2, transactionManager)
            .reader(studentItemReader)
            .processor(studentItemProcessor)
            .writer(studentItemWriter)
            .faultTolerant()
            .skip(IllegalArgumentException.class)
            .skipLimit(10)
            .build();
}

这表示:

  • 遇到 IllegalArgumentException 可以跳过
  • 最多跳过 10 次

为什么 Skip 很重要

现实中,数据质量往往没那么完美。批处理系统不是追求“零错误数据”,而是追求:

  • 能识别错误
  • 能隔离错误
  • 能继续处理正确数据
  • 能把错误记录留下来供人工修复

十、如何做批任务监控?

批任务没有监控,就像没有仪表盘的飞机。

10.1 监控什么

通常需要关注:

  • Job 是否启动
  • Job 是否成功
  • 运行了多久
  • 处理了多少条
  • 成功多少条
  • 失败多少条
  • 哪个 Step 失败
  • 错误原因是什么
  • 是否发生重试
  • 是否发生跳过

10.2 Spring Batch 自带的元数据表

Spring Batch 会把执行信息写入数据库,这些元数据表非常关键。它们可以帮助你:

  • 查询历史执行记录
  • 分析失败原因
  • 恢复上次执行
  • 监控作业耗时

常见表包括:

  • BATCH_JOB_INSTANCE
  • BATCH_JOB_EXECUTION
  • BATCH_STEP_EXECUTION
  • BATCH_JOB_EXECUTION_PARAMS
  • BATCH_STEP_EXECUTION_CONTEXT
  • BATCH_JOB_EXECUTION_CONTEXT

10.3 如何读取 Job 执行结果

你可以通过 JobExecution 来查看执行状态:

JobExecution jobExecution = jobLauncher.run(job, jobParameters);

if (jobExecution.getStatus().isUnsuccessful()) {
    // 中文注释:任务失败时做报警或补偿处理
    System.out.println("任务失败,需要通知运维人员");
}

10.4 结合日志与告警

最实用的监控方案通常是三层:

  1. 日志:记录每个 Step 的开始结束、异常信息
  2. 数据库元数据:记录任务运行轨迹
  3. 告警系统:失败后通知短信、邮件、企业微信、钉钉

10.5 常见监控建议

  • 给 Job 名称统一命名规则
  • 给每个 Step 打唯一标识
  • 对大任务定期打印进度日志
  • 任务失败时记录输入参数
  • 任务成功时记录耗时和处理量

十一、与普通定时任务的区别

很多人第一次接触 Spring Batch 时,会问一个问题:

我已经有 @Scheduled 了,为什么还要 Spring Batch?

这个问题非常关键。

11.1 定时任务擅长什么?

普通定时任务适合:

  • 固定时间执行
  • 逻辑简单
  • 无需复杂恢复
  • 不需要复杂的执行历史

比如:

  • 每天凌晨同步一次配置
  • 每隔 5 分钟刷新缓存
  • 每小时统计一次在线人数

11.2 Spring Batch 擅长什么?

Spring Batch 适合:

  • 数据量大
  • 逻辑复杂
  • 需要分步骤
  • 需要事务控制
  • 需要重试和跳过
  • 需要失败恢复
  • 需要审计和监控

11.3 对比表

维度 普通定时任务 Spring Batch
核心目标 定时触发 批量可靠处理
数据规模 小到中等 中到超大
事务控制 手动处理 框架支持
失败恢复 较弱 很强
监控审计 通常自己实现 内置元数据
处理模型 逻辑自由 标准化 Job/Step
适合场景 简单定时动作 ETL、导入导出、归档、结算

11.4 不是谁替代谁

正确理解是:

  • @Scheduled 是“什么时候执行”的问题
  • Spring Batch 是“如何可靠执行”的问题

这两个维度并不冲突,很多生产系统会把二者结合起来:

  • @Scheduled 负责定时触发 Job
  • Spring Batch 负责真正执行批任务

十二、真实落地设计:如何组织你的批处理工程

做批处理项目,最怕两个问题:

  • 所有逻辑堆在一个类里
  • 一个 Job 配置越来越大,后面没人敢改

下面给出一些工程化建议。

12.1 一个 Job 对应一个清晰业务目标

例如:

  • importStudentJob:导入学生数据
  • archiveOrderJob:归档订单数据
  • generateDailyReportJob:生成日报

不要把多个完全不同的业务塞进一个 Job。

12.2 Step 颗粒度要合理

Step 不宜太大,也不宜太碎。建议遵循“一个清晰动作一个 Step”原则:

  • 读取文件
  • 校验数据
  • 入库
  • 生成结果文件
  • 清理临时资源

12.3 Reader / Processor / Writer 分层

建议严格分离:

  • Reader 只负责读
  • Processor 只负责处理
  • Writer 只负责写

不要在 Reader 里做复杂校验,也不要在 Writer 里拼大量业务规则。

12.4 统一错误输出

批任务中的错误不要只靠抛异常。更好的做法是:

  • 记录失败数据行
  • 记录失败原因
  • 记录任务参数
  • 记录失败时间

这样后续排查会轻松很多。

12.5 批任务要支持幂等

幂等是批任务非常重要的概念。意思是:

同一个任务重复执行多次,结果应该保持一致,或者至少不会产生不可控副作用。

实现幂等的常见手段:

  • 使用唯一业务键
  • 先查后写
  • Upsert
  • 去重表
  • 任务批次号

十三、常见坑与最佳实践

13.1 chunk 太大不一定更快

很多人会觉得 chunk size 越大越好,其实不对。

chunk 太大可能导致:

  • 内存压力变高
  • 事务时间过长
  • 回滚成本更高
  • 错误定位更难

通常要根据实际场景压测。

13.2 不要忽略 JobParameters

重复启动同一个 Job 时,JobParameters 非常重要。没有唯一参数,你可能会遇到“作业已存在”之类的问题。

13.3 不要在 Processor 里做重 IO

Processor 适合做轻量转换和校验,不适合频繁访问远程接口。否则性能会明显下降。

13.4 Writer 要尽量批量

写数据库时,应该优先使用批量写入,而不是一条一条执行。

13.5 不要忽略失败补偿

当任务失败时,应该明确:

  • 已经写入的数据是否需要回滚
  • 是否允许重跑
  • 重跑前是否要清理中间状态
  • 失败记录如何人工修复

十四、再补一个 Tasklet 实战案例:清理临时文件

为了让你对 Tasklet 有更直观的理解,这里再做一个简单案例:删除临时目录下的文件。

14.1 Tasklet 实现

package com.example.batch.config;

import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

import java.io.File;

@Configuration
public class TempCleanupJobConfig {

    @Bean
    public Step cleanupTempStep(JobRepository jobRepository,
                                PlatformTransactionManager transactionManager) {
        return new StepBuilder("cleanupTempStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    // 中文注释:这里演示删除临时目录文件
                    File tempDir = new File("./temp");
                    if (tempDir.exists() && tempDir.isDirectory()) {
                        File[] files = tempDir.listFiles();
                        if (files != null) {
                            for (File file : files) {
                                file.delete();
                            }
                        }
                    }
                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }

    @Bean
    public org.springframework.batch.core.Job cleanupTempJob(JobRepository jobRepository,
                                                             Step cleanupTempStep) {
        return new JobBuilder("cleanupTempJob", jobRepository)
                .start(cleanupTempStep)
                .build();
    }
}

14.2 为什么这个适合 Tasklet?

因为它不是“批量读写数据”,只是一个一次性动作:

  • 检查目录
  • 删除文件
  • 结束

这就是 Tasklet 的用武之地。

十五、Spring Batch 5.x 相比旧版本需要注意什么?

在 Spring Boot 3.x 时代,Spring Batch 5.x 有几个明显特点:

  • 与 Jakarta EE 体系兼容
  • 对 Spring 6 / Boot 3 更友好
  • API 和配置方式更现代化
  • 更强调显式的 JobRepository、TransactionManager 配置

对于老项目迁移而言,最重要的不是“语法是不是完全一样”,而是要理解:

  • Spring Batch 的元数据管理更重要了
  • 事务边界更清晰了
  • 组件职责更明确了

这意味着你在设计批任务时,最好从一开始就按“Job / Step / Chunk / Tasklet”的标准模型来组织,而不是写成一个巨型业务方法。

十六、从架构角度看 Spring Batch

如果把一个批任务系统看成一套架构,通常可以分成四层:

相关示意图绘制如下,仅供参考:

16.1 触发层

负责启动任务,可以是:

  • REST API
  • @Scheduled
  • 消息触发
  • 命令行启动

16.2 编排层

负责任务流程控制:

  • 先做什么
  • 后做什么
  • 失败如何处理
  • 是否并行
  • 是否条件分支

16.3 处理层

负责实际业务逻辑:

  • 读取
  • 校验
  • 转换
  • 写入

16.4 资源层

真正的输入输出来源:

  • 数据库
  • 文件
  • 第三方 API
  • 对象存储
  • 消息队列

这种分层思路,会让你的批处理系统更可维护,也更容易横向扩展。

十七、一个更贴近生产的思考:批处理任务平台化

当你的批处理任务逐渐增多,你会发现一个现实问题:

不是“能不能写一个 Job”,而是“能不能管理几十个 Job”。

这时候就会出现批任务平台化的需求:

  • 统一触发入口
  • 统一参数校验
  • 统一日志与监控
  • 统一重试与告警
  • 统一失败恢复
  • 统一权限控制

Spring Batch 本身更偏框架层,平台层通常还需要你自己做一些封装:

  • Job 注册中心
  • 任务配置中心
  • 运行历史查询页面
  • 手工重跑按钮
  • 失败记录导出

如果你的系统已经进入这个阶段,Spring Batch 往往会成为非常合适的底座。

十八、本文小结

到这里,你应该已经对 Spring Batch 5.x 的作业模型有了比较完整的认识。

我们从最基础的概念讲起,逐步串起了:

  • Job 是什么
  • Step 是什么
  • Chunk 和 Tasklet 的区别
  • 批处理适合解决什么问题
  • 与普通定时任务的差别
  • 如何做失败恢复
  • 如何做监控
  • 如何写出可运行的案例代码

最重要的是,你应该形成这样一个思维:

Spring Batch 不是“把一段代码批量执行”这么简单,而是一套围绕“可靠批处理”而设计的完整模型。

当你真正理解这一点后,再去写导入导出、归档清理、报表生成、数据同步这类任务时,思路会清晰很多。

十九、进一步扩展学习

如果你要继续精进学习,接下来可以顺着这些方向继续补充拓展:

  • 多 Step Job 的条件流转
  • JobExecutionDecider 的使用
  • 分区处理(Partitioning)
  • 并行 Step / 并发执行
  • Job 参数校验器
  • 自定义 Listener
  • 远程分片 / 分布式批处理
  • 批任务中的事务传播策略
  • 批处理与 Quartz 的组合
  • 批处理结果落库与可视化看板

这些内容相关知识点,你一旦补齐,本期内容将会把你从“入门级”成长为“可用于实战”的入门开发者。

二十、结语

Spring Batch 的价值,不只在于“能跑批任务”,更在于它把批处理从“经验活”变成了“工程化能力”。

当你开始重视:

  • 流程拆分
  • 事务边界
  • 失败恢复
  • 任务审计
  • 可观测性

你就已经从“写脚本的人”,迈向“设计批处理系统的人”了。

ok,同学们,本节课就上到这儿,下课~

🧧 学习福利 · 限时开放 🧧

当然,无论你是计算机专业在读学生,还是对编程充满兴趣的入门者,都强烈建议系统学习SpringBoot全体系专栏:👉 「滚雪球学 Spring Boot」;涵盖SpringBoot所有教学内容。

该专栏以“循序渐进 + 实战驱动”为核心理念,从基础到进阶到就业到架构师逐层展开,帮助你快速建立完整的 Spring Boot 技术体系,带你玩转SpringBoot框架。

📌 学习承诺:
通过该专栏,你将能够:

  • 快速掌握 Spring Boot 核心开发能力
  • 构建完整的后端项目认知体系
  • 实现从“入门”到“独立开发”的跃迁

就像“滚雪球”一样,知识不断积累、能力持续放大,实现指数级成长 🚀

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注技术号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们直接对照学习 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌,一名深耕 Java 后端领域数十年的一线研发老兵,曾担任独角兽企业后端技术经理、研发架构师等职位,长期专注于 Java 后端、分布式架构、微服务治理、高并发系统、工程效能与研发管理等方向。

目前活跃于多个主流技术社区,包括:

CSDN稀土掘金InfoQ51CTO华为云开发者社区阿里云开发者社区腾讯云开发者社区开源中国博客园墨天轮 等平台。

曾获得:

  • CSDN 博客之星 Top30
  • 华为云多年度十佳博主 & 卓越贡献奖
  • 掘金多年度人气作者 Top40
  • CSDN、掘金、InfoQ、51CTO 等平台签约作者 / 优质作者

截至目前,全网技术内容累计影响读者众多,全网粉丝已超过 30w+

如果你也关注 Java 后端、架构设计、技术成长、职场进阶与研发管理,欢迎关注我的技术内容合集入口:👉 点击查看 👈️

硬核技术号 「猿圈奇妙屋」 期待你的加入。

这里不仅分享技术干货,也记录一线研发人的成长、踩坑、思考与进阶路径。

愿我们一起打怪升级,在技术路上持续进阶。

- End -

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐