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

一、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_INSTANCE、BATCH_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 还支持执行监听器,用来记录作业开始和结束时间、统计处理条数、发送告警通知等。实现 JobExecutionListener 或 StepExecutionListener 后,在 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