资讯动态

Spring Batch项目从零搭建:Job与Step核心实战指南

发布时间:2026/10/16 1:12:17 来源:尧图企业网站定制
做后台开发的朋友迟早会和批处理打交道。我自己的第一份批量导入导出任务就是用原生JDBC加循环硬扛下来的几万条数据跑一次要几分钟中途出错只能从头再来。后来切到Spring Batch才算是把这块彻底理顺了。这篇是Spring Batch自学之路的第二篇专门讲清楚一件事如何从零搭建一个Spring Batch项目。这篇文章适合三种人看刚接触Spring Batch、照着文档却不知道怎么落地的新手已经在用Spring Boot做Web开发、想给项目加上批处理能力的开发者还有像我一样被各种“批量任务写成定时器循环”折磨过、想找个正经框架的老手。我会把项目搭建的每一步拆开讲包括为什么要引入这些依赖、核心组件之间是怎么配合的、第一个Demo任务怎么跑起来以及我在反复搭建中踩过的坑。1. 搭项目之前先把Spring Batch的核心思路理清楚1.1 批处理到底解决什么问题批处理解决的核心问题其实就一个在一个合理的时间窗口内把大数据量、重复性高的任务稳定可靠地跑完。这里的“大数据量”不是指几万条而是几十万、上百万甚至更多。事务边界怎么控制数据读一半失败了怎么办重跑之后会不会重复处理任务跑得慢怎么定位瓶颈这些才是批处理框架真正要回答的问题。Spring Batch不是万能的它适合处理的是明确的、边界清晰的数据处理流程比如文件数据导入数据库、数据库表之间的数据转换、报表数据的预计算。它不适合做实时请求响应也不要拿它做流式计算那是另外一套体系。打一个比方Spring MVC像一家餐厅的点单区一单一单地处理讲究响应快Spring Batch像是后厨备菜流水线一批一批地处理讲究稳定、量大、可追溯。1.2 为什么不直接用循环加事务很多人写批处理的第一反应是我写个for循环每积累100条提交一次事务不就行了吗小数据量确实行但数据量一上来就会出现几个问题失败后从哪里续跑没有记录并行处理时没有统一的分区策略处理进度没有可观测的指标代码一旦复杂读数据、处理数据、写数据全耦合在一起改一处牵动全身。Spring Batch把这套流程抽象成了标准模型一个Job对应一个完整的批处理任务Job里面包含一个或多个Step每个Step要么是简单的单任务处理要么是标准的读-处理-写三段式。这种抽象最大的好处是它把数据来源、处理逻辑、输出目标三者解耦了。今天我读CSV明天改成读数据库只需要换掉Reader今天写到数据库明天改成写文件只需要换掉Writer。中间的Processor保持稳定这就是框架带来的结构性收益。1.3 搭建前需要理解的两个关键概念Job与StepJob和Step是Spring Batch的两个顶级抽象理解清楚这俩后面所有代码都顺理成章。Job是批处理任务的完整单元就像一条流水线的完整订单从接单、备料、生产到质检、交付。一个Job有自己的名字、状态、执行参数每次执行都会留下执行记录。Step则是Job里的一个执行阶段。拿“从CSV导入用户数据”这个任务来说整个导入过程是一个Job其中“读取CSV并解析”、“校验并转换数据格式”、“批量写入数据库”可以拆成不同Step也可以合并成一个Step内的读-处理-写三阶段。简单场景中一个Job通常只需要一个Step。但复杂一点的业务比如“先清洗数据、再转换数据、最后归档数据”每个阶段都需要独立的错误处理和重试策略这时候拆成多个Step就非常必要。我在实际项目里就遇到过这样一个场景数据清洗和归档的失败率完全不同合并在一起时归档失败会导致已清洗的数据也要重新处理拆开后就灵活多了。2. 环境准备与依赖引入2.1 基础环境版本选择搭建Spring Batch项目第一步不是写代码而是把基础环境对齐。我用的是JDK 17、Maven 3.9、Spring Boot 3.2.x对应Spring Batch 5.x。如果你是Spring Boot 2.x的老项目用的则是Spring Batch 4.x两者在配置方式上有些差异主要是包名和部分API的调整。建议新项目直接走Spring Boot 3 Spring Batch 5毕竟旧版迟早要维护。IDE方面我用的是IntelliJ IDEA社区版完全够用。数据库我强烈建议第一步先用H2内存库原因后面会详细说。如果你要用MySQL或PostgreSQL记得提前建好库和账号因为Spring Batch的元数据表需要写权限。2.2 Maven依赖引入实操既然用了Spring Boot就绕开手动管理版本号的麻烦。在pom.xml里第一件事是引入父工程parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version relativePath/ /parent然后是Spring Batch的起步依赖和Web起步依赖。这里有个细节很多人不知道为什么要加Web依赖。Spring Batch本身是独立运行的批处理框架不强依赖Web。但加上spring-boot-starter-web以后项目里就能用Controller去手动触发Job或者写一个简单的接口查看Job状态。对于平时调试和后期接入调度平台来说方便的不是一点半点。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-batch/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2.3 配置文件里的基础参数依赖引好之后配置文件是最容易出错的一步。我用的是application.yml核心配置如下spring: application: name: spring-batch-demo datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1 driver-class-name: org.h2.Driver username: sa password: batch: jdbc: initialize-schema: always job: enabled: false这里有两个关键配置必须解释清楚。第一个是spring.batch.jdbc.initialize-schema它控制Spring Batch是否自动创建元数据表。Spring Batch运行时需要记录Job和Step的执行情况这些记录存在一张张元数据表里表名通常是BATCH_JOB_INSTANCE、BATCH_JOB_EXECUTION之类。如果你的数据库里已经有这些表可以设为never否则设为always让它自动建表。第二个是spring.batch.job.enabled。默认情况下Spring Boot会自动运行容器里所有定义好的Job这对定时任务没问题但开发调试时非常烦人你只想启动服务看一看结果任务直接跑起来了。我习惯先设成false后面需要手动触发时再在代码里调用JobLauncher。这个细节网上很多教程都不提但实际开发时非常影响体验。3. 一个可以直接跑的Demo任务从CSV导入用户数据3.1 Demo场景与数据准备场景就选最简单的把一批用户数据从CSV文件里读出来做一次简单的清洗处理然后写入数据库。这种场景覆盖了读、处理、写三个核心环节而且文件导入是批处理里最经典的应用。我提前准备了一个users.csv文件放在src/main/resources目录下id,name,age,email 1,张三,25,zhangsanexample.com 2,李四,31,lisiexample.com 3,王五,18,wangwuexample.com 4,赵六,-5,zhaoliuexample.com这个数据里故意放了一条年龄为负数的脏数据后面Processor演示数据校验时要用到。3.2 实体类与表结构在Spring Batch里Reader读取的数据最终要映射成对象所以先写一个User实体类public class User { private Long id; private String name; private Integer age; private String email; // getter/setter 省略 }数据库表结构用H2内存库的话可以直接在schema.sql里定义CREATE TABLE IF NOT EXISTS users ( id BIGINT PRIMARY KEY, name VARCHAR(50), age INT, email VARCHAR(100) );3.3 核心组件读取、处理、写入Spring Batch的chunk-oriented处理模式是它最核心的运行机制把整个Step拆成一个个chunk每个chunk先完成读取和业务处理达到一定数量后统一提交。这个概念我用一个流水线来类比你组装一台手机不是装完一台再开始下一台而是先把这批手机的外壳全部装上再统一安装电池再统一安装屏幕。每个工序都是一个独立阶段分批推进效率高且出错可控。在代码层面三个核心组件分别对应三种接口。ItemReader负责数据读取我这里用的是FlatFileItemReader按行解析CSV文件Bean public FlatFileItemReaderUser userItemReader() { FlatFileItemReaderUser reader new FlatFileItemReader(); reader.setResource(new ClassPathResource(users.csv)); reader.setLinesToSkip(1); reader.setDelimited() .setNames(new String[]{id, name, age, email}); reader.setFieldSetMapper(new BeanWrapperFieldSetMapper() {{ setTargetType(User.class); }}); return reader; }setLinesToSkip(1)是跳过CSV表头。setDelimited()表示按逗号分隔还能设置分隔符。如果不设置FieldSetMapper默认也是用字段名映射到Bean属性。ItemProcessor负责数据加工这也是自定义业务逻辑的主要位置。我这里演示一个最简单的校验过滤逻辑public class UserProcessor implements ItemProcessorUser, User { Override public User process(User item) { if (item.getAge() 0 || item.getAge() 150) { return null; } return item; } }process方法返回null时这条记录会被框架自动丢弃不进入Writer。这个机制在过滤脏数据时非常好用。需要注意的是如果过滤掉的记录也要记日志得自己在Processor里处理框架不会主动记录被丢弃的原因。ItemWriter负责数据写入最简单的方式就是直接用JdbcBatchItemWriter把积累到一个chunk的数据批量写入Bean public JdbcBatchItemWriterUser userItemWriter(DataSource dataSource) { JdbcBatchItemWriterUser writer new JdbcBatchItemWriter(); writer.setDataSource(dataSource); writer.setSql(INSERT INTO users(id, name, age, email) VALUES (:id, :name, :age, :email)); writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider()); return writer; }这段代码里:id、:name这些参数是Spring Batch通过Bean属性自动绑定的所以实体类里必须要有对应的getter方法。3.4 组装Job和Step到这一步整个批处理任务的骨架就出来了。Step把Reader、Processor、Writer串起来Job再把Step串起来Spring Batch通过JobLauncher统一调度。Configuration EnableBatchProcessing public class BatchConfig { Autowired private JobBuilderFactory jobBuilderFactory; Autowired private StepBuilderFactory stepBuilderFactory; Bean public Step importUserStep(ItemReaderUser reader, ItemProcessorUser, User processor, ItemWriterUser writer) { return stepBuilderFactory.get(importUserStep) .User, Userchunk(10) .reader(reader) .processor(processor) .writer(writer) .build(); } Bean public Job importUserJob(JobRepository jobRepository, Step importUserStep) { return jobBuilderFactory.get(importUserJob) .repository(jobRepository) .start(importUserStep) .build(); } }这里的chunk(10)是事务提交的边界值意思是每处理10条数据就提交一次事务。这个值需要根据实际情况调优提交太频繁事务开销大提交太稀疏一次事务里持有大量数据内存占用高失败后回滚的代价也大。我一般先用100再根据数据量和字段多少微调不是越大越好需要实测。3.5 让任务可以手动触发配置里我们把spring.batch.job.enabled设成了false这样启动时不自动跑。为了手动触发任务我需要一个简单的Controller和一个JobLauncherRestController public class JobLaunchController { Autowired private JobLauncher jobLauncher; Autowired private Job importUserJob; GetMapping(/run) public String run() throws Exception { JobParameters params new JobParametersBuilder() .addLong(time, System.currentTimeMillis()) .toJobParameters(); jobLauncher.run(importUserJob, params); return job started; } }这里必须加一个time参数而且每次调用都要不同否则后一次调用会被Spring Batch判定为“同一Job实例已经执行完”直接报错不会重新跑。这是我们马上要在第5节里详细展开的一个经典坑。4. 启动运行与结果验证4.1 第一次启动会看到什么启动Spring Boot应用后控制台会打印出Spring Batch启动的相关日志同时能看到自动建表SQL的日志。如果没看到建表日志检查一下initialize-schema配置并且确保DataSource有建表权限。浏览器访问http://localhost:8080/run正常情况下控制台会打印类似下面的日志Starting Job: importUserJob Step: importUserStep Processing chunk of 10 items Writing 4 items Step completed successfully Job completed successfully这里有个细节值得注意Writing 4 items说明4条数据全部写入了其中有一条年龄是-5按我们的Processor逻辑应该被过滤掉。为什么会写4条因为我示例中的Processor直接忽略了用户数据实际4条数据都会写进去。这说明过滤逻辑没有生效时检查Processor代码是否真的被Spring容器接管比如有没有漏掉Component或Bean注册。4.2 看日志的正确姿势Spring Batch的日志输出比较克制默认只打关键节点。调试阶段建议把日志级别调到DEBUG尤其是以下几个包logging: level: org.springframework.batch: DEBUG org.springframework.jdbc.core: DEBUG打开DEBUG之后能清晰地看到每个chunk的读取、处理、写入过程以及每条SQL的执行情况。这对于排查数据读取不完整、字段映射错误、SQL写错之类的问题非常有帮助。线上环境再调回INFO避免日志量过大。4.3 验证结果的三条路径任务跑完后验证结果有三条路径。第一条是直接查数据库H2内存库需要打开控制台或者写个接口查数据。实际项目中用MySQL的话直接select count(*) from users就行。第二条是看Spring Batch的元数据表。BATCH_JOB_EXECUTION表里记录了每次Job的执行状态BATCH_STEP_EXECUTION表里记录了每个Step的读取条数、写入条数、提交次数、失败原因等。这些表本身就是天然的监控数据很多企业级BPM调度平台就是靠这些表做任务监控的。第三条是看文件输出。如果你做的批处理是“数据库导出到文件”任务跑完直接比对文件行数就行。5. 搭建过程中最容易踩的坑5.1 元数据表不存在的报错我第一次跑Demo时遇到的最典型报错Table BATCH_JOB_INSTANCE doesnt exist原因基本只有两类一类是配置文件里initialize-schema设成了never另一类是DataSource连接到了一个空库。解决办法也很直接把initialize-schema设成always让它自动建表。如果数据库用户权限不够建表失败则需要提前用脚本手动建表。Spring Batch自带的建表脚本就在框架依赖的jar包里可以解压后直接拿。5.2 同一个Job无法重复启动这是我反复讲过的一个坑。第一次运行成功后第二遍再触发同样的Job会报A job instance already exists and is complete原因是Spring Batch为了保证幂等性用Job名称加JobParameters生成的唯一键判断是否执行过。同一个Job用一模一样的参数重复触发框架就会拒绝执行。解决办法就是每次触发时加入一个时间戳参数或者传入业务日期等每次不同的参数。这也是我在第3节的Controller里特意加addLong(time, System.currentTimeMillis())的原因。5.3 字段映射错位导致数据全乱FlatFileItemReader里setNames定义的字段顺序必须和CSV文件列顺序严格对应否则数据会映射错位最常见的就是姓名和年龄对调。我在实际项目中还踩过一个更隐蔽的坑源文件被Excel重新保存过原本逗号分隔变成了分号。处理这种情况需要在setDelimited()里明确指定分隔符必要时还要处理引号包裹的字段。写严格一点就不容易被脏数据带偏。5.4 文件路径和编码问题文件读取路径是个容易被忽视的坑。用ClassPathResource时如果文件放在resources根目录写users.csv就行如果放在子目录必须写完整路径。用FileSystemResource时一定要搞清楚项目的工作目录。不同IDE的工作目录不一样IDEA里通常是项目根目录。编码问题也很常见CSV文件如果是Excel导出的很可能带BOM头GBK编码更是家常便饭。处理方法是显式指定编码Spring Batch的FlatFileItemReader默认使用平台默认编码在服务器上很容易出乱码我通常会在构建Reader时把编码强制设为UTF-8或GBK视源文件而定。6. 关于批次大小与后续扩展的几点个人经验6.1 批次大小不是越大越好很多新手会把chunk size设得特别大觉得一次提交的数据越多性能越高。但实际上chunk size直接决定了事务的边界设太大时一次事务里积压了大量的未提交数据一旦中途异常整个事务回滚的代价很大内存占用也会明显上升。我自己在实践中得出的经验是数据行比较小、字段不多时500到1000都是可以接受的字段多、单行数据大的时候建议控制在100以内然后做一个最小测试再逐步加大找到当前环境下稳定与性能的平衡点。6.2 从Demo到真实项目还需要什么这个Demo建立在你理解了Job和Step的基础之上。真正接到业务项目时还有几个方向几乎一定会碰见一是跳过与重试策略Spring Batch支持在Step上配置skipLimit和retryLimit比如某些数据校验失败允许跳过而网络抖动引起的Writer失败则自动重试二是数据分区和并行处理Spring Batch可以用Partitioner把数据分成多个区块多线程并发执行这一块能大幅缩短处理时间三是多Step的流程控制比如条件分支、失败退出、组合嵌套都是通过配置Step之间的跳转条件实现的。6.3 我反复搭建后最有体会的一点骨架代码本身并不复杂真正让我花时间的地方往往是那些不起眼的基础设施细节数据源连接、文件编码、Job参数唯一性、元数据表的初始化时机。把这些基础细节理顺后面扩展再复杂的功能都稳当。我第一次搭这个框架的时候光是Job重复启动那个问题就卡了半天当时还没意识到是JobParameters在作怪。后来把元数据表翻出来看才恍然大悟原来框架在底层做了一套完整的执行状态管理。从那之后我每搭一个批处理项目都会先去翻一遍BATCH_开头的这几张表读懂了它们很多报错就能直接定位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑