三层架构
本章定位:掌握 Spring Batch 的分层架构设计——理解 Application、Batch Core、Batch Infrastructure 三层的职责划分和协作方式。
定义与作用
Spring Batch 采用严格的分层架构(Layered Architecture),将业务逻辑、运行时控制和基础设施解耦为三个独立层次。
飞翔科技架构师白歌在代码评审时常说:
"如果你在 ItemReader 里写了事务控制的代码,或者在 Job 定义里直接管理数据库连接,那说明你把三层职责混在一起了。Spring Batch 的每一层都有自己的边界。"
三层架构解决的问题:
| 问题 | 分层解决的方案 |
|---|---|
| 业务逻辑和基础设施耦合 | Application 层只写业务,Infrastructure 层提供通用实现 |
| 状态管理和业务处理混杂 | Core 层统一管理所有状态,Application 层无需关心元数据表 |
| 不同场景切换困难 | 换个 Reader 实现即可从 CSV 切换到数据库,上层代码零改动 |
核心原理
各层职责详解
| 层 | 职责 | 核心组件 | 开发者需要做什么 |
|---|---|---|---|
| Application(应用层) | 定义业务逻辑——数据从哪来、怎么处理、写到哪去 | Job、Step、ItemReader、ItemProcessor、ItemWriter | 编写具体的 Reader/Processor/Writer 实现,用 Builder API 组装 Job 和 Step |
| Batch Core(核心层) | 管理运行时状态——谁启动了 Job、执行到哪一步、事务怎么控制 | JobLauncher、JobRepository、JobExecution、StepExecution | 不需要写代码,Spring Batch 自动管理 |
| Batch Infrastructure(基础设施层) | 提供通用能力——CSV 读取、JDBC 批量写入、重试策略 | FlatFileItemReader、JdbcBatchItemWriter、RetryPolicy | 直接使用内置实现或基于接口扩展自定义实现 |
分层带来的数据流
关键洞察:Application 层在循环中处理数据时,完全不知道 Core 层在后台默默更新元数据表。这种"无感"正是分层架构的价值——业务的归业务,状态的归状态。
完整示例
场景一:飞翔科技——从 CSV 导入学生成绩
背景:产品经理孔蓝要求将教务系统导出的学生成绩 CSV 文件导入数据库。小崔需要实现一个完整的批量导入作业。
操作前:小崔最初把所有逻辑写在一个类里
// ❌ 操作前:三层混杂——Reader、状态管理、事务控制全部耦合
public class GradeImporter {
private Connection conn;
private int lineNumber = 0;
private int successCount = 0;
public void importGrades(String csvPath) {
try (BufferedReader br = new BufferedReader(new FileReader(csvPath))) {
conn = DriverManager.getConnection("jdbc:mysql://...");
conn.setAutoCommit(false);
String line;
while ((line = br.readLine()) != null) {
lineNumber++;
String[] fields = line.split(",");
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO grades VALUES (?, ?, ?)");
// ... 参数设置
ps.executeUpdate();
successCount++;
if (successCount % 50 == 0) {
conn.commit(); // 手动事务管理
saveProgress(lineNumber); // 手动状态持久化
}
}
conn.commit();
} catch (Exception e) {
conn.rollback(); // 手动回滚
throw new RuntimeException("Failed at line " + lineNumber);
}
}
}
操作后:按三层架构拆分
Application 层——Job 和 Step 定义:
@Configuration
@EnableBatchProcessing
public class GradeImportConfig {
@Bean
public Job gradeImportJob(JobRepository jobRepository,
Step gradeImportStep) {
return new JobBuilder("gradeImportJob", jobRepository)
.start(gradeImportStep)
.build();
}
@Bean
public Step gradeImportStep(JobRepository jobRepository,
PlatformTransactionManager tx,
FlatFileItemReader<GradeDTO> reader,
ItemProcessor<GradeDTO, GradeEntity> processor,
JdbcBatchItemWriter<GradeEntity> writer) {
return new StepBuilder("gradeImportStep", jobRepository)
.<GradeDTO, GradeEntity>chunk(50, tx)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
}
Application 层——Processor(业务逻辑):
@Component
public class GradeProcessor implements ItemProcessor<GradeDTO, GradeEntity> {
@Override
public GradeEntity process(GradeDTO dto) {
// 业务逻辑:转换 + GPA 计算
GradeEntity entity = new GradeEntity();
entity.setStudentId(dto.getStudentId());
entity.setCourseName(dto.getCourseName());
entity.setScore(dto.getScore());
entity.setGpa(calculateGpa(dto.getScore())); // 业务规则
return entity;
}
private double calculateGpa(double score) {
if (score >= 90) return 4.0;
if (score >= 85) return 3.7;
if (score >= 80) return 3.3;
if (score >= 75) return 3.0;
if (score >= 70) return 2.7;
if (score >= 60) return 2.0;
return 0.0;
}
}
操作前后对比:
| 维度 | 操作前(混杂版) | 操作后(三层架构版) |
|---|---|---|
| 代码行数 | 约 80 行(全部混在一起) | Application 约 50 行 + Infrastructure 0 行(直接复用) |
| 事务管理 | 手动 commit/rollback | 声明式:chunk(50, tx) |
| 状态持久化 | 手动 saveProgress() | Core 层自动写入 BATCH_STEP_EXECUTION |
| 可测试性 | 需要真实数据库和 CSV 文件 | Processor 可独立单元测试 |
| 复用性 | Reader/Writer 逻辑不可复用 | FlatFileItemReader 换文件路径即可复用 |
场景二:数据源切换——从 CSV 改为数据库读取
背景:运营部高英反映学生数据已经从 CSV 文件迁移到了 MySQL 数据库,要求修改导入作业的数据源。
在分层架构下,只需替换 Infrastructure 层的实现,Application 层代码不改一行:
// 原来:FlatFileItemReader(CSV 读取)
@Bean
public FlatFileItemReader<GradeDTO> reader() {
return new FlatFileItemReaderBuilder<GradeDTO>()
.name("csvReader")
.resource(new FileSystemResource("grades.csv"))
.delimited().delimiter(",")
.names("studentId", "courseName", "score")
.targetType(GradeDTO.class)
.build();
}
// 改为:JdbcCursorItemReader(数据库读取)—— Step 和 Processor 完全不变
@Bean
public JdbcCursorItemReader<GradeDTO> reader(DataSource dataSource) {
return new JdbcCursorItemReaderBuilder<GradeDTO>()
.name("dbReader")
.dataSource(dataSource)
.sql("SELECT student_id, course_name, score FROM grades WHERE status = 'PENDING'")
.rowMapper(new BeanPropertyRowMapper<>(GradeDTO.class))
.build();
}
关键点:Step 定义中的 .reader(reader) 依赖的是 ItemReader<GradeDTO> 接口,不关心具体实现。这正是分层架构中"面向接口编程"的优势。
易错场景与避坑
反例:在 Processor 中直接操作数据库
小崔最早写的 Processor 不仅做数据转换,还直接查数据库做关联:
// ❌ Processor 跨越三层:既做业务逻辑(Application),又做数据访问(Infrastructure)
@Component
public class BadProcessor implements ItemProcessor<GradeDTO, GradeEntity> {
@Autowired
private JdbcTemplate jdbcTemplate; // ❌ Processor 不应该直接操作数据库
@Override
public GradeEntity process(GradeDTO dto) {
// 在 Processor 中查数据库做关联——破坏了层次隔离
String studentName = jdbcTemplate.queryForObject(
"SELECT name FROM students WHERE id = ?",
String.class, dto.getStudentId());
// ...
}
}
问题:
- Processor 每次处理都执行一次 SQL 查询,10 万条数据就是 10 万次查询——性能灾难
- Processor 的事务中混入了只读查询,语义不清
- 单元测试需要 mock JdbcTemplate,测试复杂度上升
解决方案:将关联逻辑前置到 Reader 中(用 JOIN 查询)或后置到 Step 间的数据传递中(用 ExecutionContext 缓存)。
面试高频考点
Q1:Spring Batch 的三层架构分别对应什么职责?
Application 层(开发者编写):Job/Step 定义 + Reader/Processor/Writer 实现。Core 层(框架管理):JobLauncher/JobRepository/JobExecution/StepExecution 状态管理。Infrastructure 层(框架提供):FlatFileItemReader/JdbcBatchItemWriter/RetryPolicy 等通用实现。
Q2:三层架构中,如果我想替换 Reader 实现(比如从 CSV 换成数据库),需要改哪些代码?
只需修改 Reader Bean 的定义(Infrastructure 层的选择),Step 和 Job 定义中的
.reader(reader)依赖的是ItemReader<T>接口,无需修改。这是分层架构的核心价值——上层依赖接口,下层提供实现。
Q3:为什么 Chunk 的事务管理属于 Core 层而不是 Application 层?
事务边界(每个 Chunk 一个事务)是框架层面的通用机制,与具体业务逻辑无关。无论处理学生数据还是订单数据,事务管理模式完全一致。如果放在 Application 层,每个 Job 都要自己管理事务,造成大量重复代码。