如何在 Spring Boot 中整合 Spring Batch 实现批处理作业?

来源:Linux教程作者:沙月恵奈‌头衔:网络博主
导读:本期聚焦于沙月恵奈‌创作的《如何在 Spring Boot 中整合 Spring Batch 实现批处理作业?》,敬请观看详情。批处理任务到底该不该写定时脚本?当单次处理的数据从几千条增长到百万级,纯手工遍历和逐条落库会带来内存溢出、事务不一致和重启丢失进度等问题。Spring Batch 作为 Java 生态成熟的批处理框架,提供了一套可重入、可监控的作业执行模型。本文以 Spring Boot 整合 Spring Batch 为主线,从 Job 与 Step 的拆分讲起,演示如何通过 FlatFileItemReader 读取 CSV、自定义 Processor 清洗字段,并用 JdbcBatchItemWriter 批量写回数据库。同时会覆盖作业参数传递、失败重试与跳过策略,以及 Windows 环境下路径配置和常见乱码问题。读完本文你可以直接搭建一个可运行的批处理示例,理解批处理作业在数据导入、报表生成、数据清洗等场景中的落地方式。

Spring Batch 并不是一个简单的定时任务工具,它解决的是一类更复杂的问题:当数据量达到数十万条,任务需要分批读取、逐条处理、批量写入,并且要在失败后能够从断点继续执行时,普通定时任务就很难维护了。Spring Boot 对 Spring Batch 做了自动装配,开发者可以省去大量 XML 配置,把精力集中在作业本身的逻辑上。本文会从一个 CSV 文件导入数据库的典型场景出发,拆解 Spring Batch 的核心组件,并给出可在 Windows 环境下直接运行的代码示例。

如何在 Spring Boot 中整合 Spring Batch 实现批处理作业?

一、Spring Batch 的核心模型:Job 与 Step 如何分工

Spring Batch 把一次批处理任务抽象为 Job,一个 Job 由一个或多个 Step 组成。Step 是作业执行的最小单元,每个 Step 内部遵循读取、处理、写入三个阶段的链式结构。这种设计让数据处理逻辑变得非常清晰:读取阶段只负责从文件、数据库或消息队列中拿到数据,处理阶段做业务计算或清洗,写入阶段负责批量落库或输出到目标位置。

除了 Job 和 Step,还有几个必须理解的对象。JobRepository 负责保存作业执行状态、参数和统计信息,它默认使用数据库表来持久化元数据。JobLauncher 用来启动 Job,可以同步或异步执行。ItemReader、ItemProcessor、ItemWriter 分别对应读取、处理、写入三个阶段。Spring Batch 提供了一大批开箱即用的实现,比如读取 CSV 文件的 FlatFileItemReader、读取数据库的 JdbcCursorItemReader、写数据库的 JdbcBatchItemWriter 等。实际开发中通常只需组合这些组件,再补上少量自定义逻辑。

一个典型的作业从启动到结束会经过这样的流程:JobLauncher 触发 Job,Job 按顺序执行 Step,Step 通过 chunk 机制分批处理数据。例如 chunk 大小设置为 100,表示每读取 100 条数据就执行一次写入,这样既不会一次性把所有数据加载进内存,也能利用数据库批量提交提升性能。事务边界也由 chunk 控制,单批失败可以整体回滚,配合重试和跳过策略,能够实现相当健壮的容错能力。

二、搭建 Spring Boot 批处理项目与 Windows 路径准备

在 Spring Boot 项目中引入批处理能力,只需要添加 spring-boot-starter-batch 依赖。为了演示方便,数据库先使用 H2 内存数据库,实际生产环境可以替换成 MySQL、PostgreSQL 或 SQL Server。Spring Batch 的作业元数据表需要提前创建,可以通过配置 spring.batch.jdbc.initialize-schema 让框架自动生成。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

在 Windows 环境下处理文件时,路径建议使用完整的绝对路径,并且保持反斜杠分隔符。例如输入文件放在 C:\batch\input\users.csv,输出目录使用 C:\batch\output。配置文件中的路径值用单引号包裹,可以避免 YAML 对反斜杠做转义处理。

spring:
  batch:
    job:
      enabled: false
    jdbc:
      initialize-schema: always
  datasource:
    url: jdbc:h2:mem:batchdb;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
file:
  input: 'C:\batch\input\users.csv'
  output: 'C:\batch\output\users.csv'

这里把 spring.batch.job.enabled 设置为 false,是因为 Spring Boot 默认会在应用启动时自动运行所有 Job。对于需要手动触发或者通过接口触发的场景,关闭自动运行可以避免启动阶段意外执行。作业元数据表会在应用启动时自动创建,第一次运行后可以通过 H2 控制台查看 BATCH_JOB_INSTANCEBATCH_STEP_EXECUTION 等表的内容。

三、实现 CSV 导入到数据库的完整作业

假设 CSV 文件的第一行是列名,后续每一行包含用户 id、姓名、邮箱和年龄。先定义一个简单的 User 实体类,字段与 CSV 列一一对应,并生成对应的 getter 和 setter。为了简化代码,这里省略了构造方法和部分样板代码,只展示核心字段。

public class User {
    private Long id;
    private String name;
    private String email;
    private int age;

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

读取器使用 FlatFileItemReader,它负责按行读取 CSV 文件。通过 DelimitedLineTokenizer 指定列名,再使用 BeanWrapperFieldSetMapper 把每一行数据自动绑定到 User 对象。文件路径从配置项 file.input 注入,在 Windows 下写成 C:\batch\input\users.csv 这种形式即可。

@Configuration
public class BatchConfig {

    @Bean
    public FlatFileItemReader<User> reader(@Value("${file.input}") String inputPath) {
        FlatFileItemReader<User> reader = new FlatFileItemReader<>();
        reader.setResource(new FileSystemResource(inputPath));
        reader.setLinesToSkip(1);
        DefaultLineMapper<User> lineMapper = new DefaultLineMapper<>();
        DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
        tokenizer.setNames("id", "name", "email", "age");
        lineMapper.setLineTokenizer(tokenizer);
        BeanWrapperFieldSetMapper<User> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setTargetType(User.class);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        reader.setLineMapper(lineMapper);
        return reader;
    }
}

处理器负责对每一条记录进行业务处理,比如去掉姓名首尾空格、把邮箱统一转成小写、过滤掉年龄不合法的数据。处理阶段返回 null 表示该条记录被过滤,不会写入目标位置。这样可以在写入之前完成数据清洗,避免脏数据进入数据库。

public class UserItemProcessor implements ItemProcessor<User, User> {

    @Override
    public User process(User item) {
        String name = item.getName() == null ? "" : item.getName().trim();
        String email = item.getEmail() == null ? "" : item.getEmail().trim().toLowerCase();
        item.setName(name);
        item.setEmail(email);
        if (item.getAge() < 0) {
            return null;
        }
        return item;
    }
}

写入器使用 JdbcBatchItemWriter,它会把处理器返回的对象批量写入数据库。SQL 语句采用命名参数形式,参数由 BeanPropertySqlParameterSourceProvider 根据 User 对象的属性自动填充。写入器需要在数据库中先建好 users 表,可以通过 schema.sql 自动执行。

CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100),
    age INT
);
@Bean
public JdbcBatchItemWriter<User> writer(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<User>()
            .itemSqlParameterSourceProvider(new BeanPropertySqlParameterSourceProvider<>())
            .sql("INSERT INTO users (id, name, email, age) VALUES (:id, :name, :email, :age)")
            .dataSource(dataSource)
            .build();
}

把读取器、处理器、写入器组合成一个 Step,再由 Job 启动。Step 使用 chunk 机制,每次读取 100 条数据后批量写入。Job 可以配置 RunIdIncrementer,这样每次运行都会生成新的作业参数,避免相同参数导致作业实例重复执行的问题。

@Bean
public Step importUserStep(JobRepository jobRepository,
                           PlatformTransactionManager transactionManager,
                           FlatFileItemReader<User> reader,
                           ItemProcessor<User, User> processor,
                           JdbcBatchItemWriter<User> writer) {
    return new StepBuilder("importUserStep", jobRepository)
            .<User, User>chunk(100, transactionManager)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
}

@Bean
public Job importUserJob(JobRepository jobRepository, Step importUserStep) {
    return new JobBuilder("importUserJob", jobRepository)
            .incrementer(new RunIdIncrementer())
            .start(importUserStep)
            .build();
}

四、作业参数传递、重试跳过与监听器

批处理任务经常需要根据运行时的参数决定读取哪个文件、日期范围是什么。Spring Batch 通过 JobParameters 传递这类参数,作业启动时可以从参数中读取值。使用 StepScope 可以让读取器、处理器或写入器在每一步执行时动态获取参数。

@Bean
@StepScope
public FlatFileItemReader<User> reader(@Value("#{jobParameters['input.file']}") String inputPath) {
    FlatFileItemReader<User> reader = new FlatFileItemReader<>();
    reader.setResource(new FileSystemResource(inputPath));
    reader.setLinesToSkip(1);
    DefaultLineMapper<User> lineMapper = new DefaultLineMapper<>();
    DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer();
    tokenizer.setNames("id", "name", "email", "age");
    lineMapper.setLineTokenizer(tokenizer);
    BeanWrapperFieldSetMapper<User> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
    fieldSetMapper.setTargetType(User.class);
    lineMapper.setFieldSetMapper(fieldSetMapper);
    reader.setLineMapper(lineMapper);
    return reader;
}

如果 CSV 中偶尔出现脏数据,比如某一行年龄字段不是数字,整个 Step 默认会失败。为了提高健壮性,可以在 Step 上开启容错机制,允许跳过一定数量的错误记录,并对临时性异常进行重试。跳过和重试策略必须谨慎设置,否则可能掩盖真正的数据问题。

return new StepBuilder("importUserStep", jobRepository)
        .<User, User>chunk(100, transactionManager)
        .reader(reader)
        .processor(processor)
        .writer(writer)
        .faultTolerant()
        .skip(Exception.class)
        .skipLimit(10)
        .retry(Exception.class)
        .retryLimit(3)
        .build();

除了容错,Spring Batch 还支持执行监听器,用来记录作业开始和结束时间、统计处理条数、发送告警通知等。实现 JobExecutionListenerStepExecutionListener 后,在 Step 或 Job 构建时通过 listener 方法注册即可。监听器只是作业生命周期的一种扩展,不会影响主流程逻辑。

五、Windows 环境常见坑与生产建议

在 Windows 上运行 Spring Batch 时,路径问题是最高频的坑。文件路径必须使用反斜杠,例如 C:\batch\input\users.csv,不能写成 C:/batch/input/users.csv。如果路径来自配置文件,建议用单引号包裹,并在代码中使用 FileSystemResource 进行读取。路径中如果包含空格,需要确认配置文件不会被工具做额外转义。

另一个常见问题是中文乱码。CSV 文件使用 UTF-8 编码时,如果 FlatFileItemReader 没有指定编码,可能默认使用系统编码 GBK 导致乱码。可以通过 setEncoding("UTF-8") 明确指定编码。输出文件同样需要指定编码,避免写入后的文件在其他工具中打开出现乱码。

生产环境不建议使用 H2 内存数据库保存作业元数据,因为应用重启后作业执行历史会丢失,也无法支持多实例并发。应切换到 MySQL、PostgreSQL 或 SQL Server,并让多个服务共享同一个作业仓库。同时建议关闭 Spring Boot 的自动运行,改为通过调度平台或 REST 接口触发任务,便于控制执行时机和传递运行参数。

最后要注意内存和事务边界。批处理任务不要一次性加载所有数据,chunk 大小通常保持在 50 到 500 之间,具体数值取决于单条记录的大小和数据库压力。如果处理逻辑中存在调用外部接口或写本地文件的操作,应该保证这些操作具备幂等性,否则重试时可能产生重复数据。Spring Batch 的事务边界默认围绕 chunk 展开,只有理解这一点,才能在调试失败作业时快速定位是读取、处理还是写入阶段出了问题。

Spring BootSpring Batch批处理作业修改时间:2026-08-27 07:54:14

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。