JobLauncher 详解
本章定位:深入掌握 JobLauncher——作业的启动器,理解同步/异步执行、自定义配置以及多种触发方式。
定义与作用
JobLauncher 是 Spring Batch 中负责启动 Job 的核心接口。接收一个 Job 和一组 JobParameters,创建并启动 JobExecution。它是外部世界(命令行、Web 请求、定时任务)与批处理引擎之间的唯一入口。
飞翔科技架构师白歌说:
"JobLauncher 是 Spring Batch 的'启动按钮'。不管你是从命令行、REST API 还是定时任务触发,最终都是调用
jobLauncher.run(job, params)。它是所有触发方式的统一入口。"
核心原理
JobLauncher 执行流程
同步 vs 异步启动
| 模式 | 行为 | 适用场景 |
|---|---|---|
| 同步(默认) | run() 阻塞直到 Job 完成,返回最终的 JobExecution | 命令行、简单 REST API |
| 异步 | run() 立即返回,Job 在后台线程执行 | Web 应用不阻塞 HTTP 请求、多个 Job 并行启动 |
// 同步启动(默认)
JobExecution execution = jobLauncher.run(job, params);
// 这行代码会等到 Job 完全结束才返回
System.out.println("Job finished: " + execution.getStatus());
// 异步启动
@Bean
public JobLauncher asyncJobLauncher(JobRepository jobRepository) throws Exception {
SimpleJobLauncher launcher = new SimpleJobLauncher();
launcher.setJobRepository(jobRepository);
launcher.setTaskExecutor(new SimpleAsyncTaskExecutor());
launcher.afterPropertiesSet();
return launcher;
}
完整示例
场景一:飞翔科技——REST API 触发订单处理 Job
背景:运营部杨英希望通过一个管理后台页面手动触发订单处理 Job,而不是等定时任务。小崔用 REST API + JobLauncher 实现。
@RestController
@RequestMapping("/api/batch")
public class BatchJobController {
private final JobLauncher jobLauncher;
private final Job orderProcessingJob;
public BatchJobController(JobLauncher jobLauncher,
@Qualifier("orderProcessingJob") Job job) {
this.jobLauncher = jobLauncher;
this.orderProcessingJob = job;
}
@PostMapping("/orders/process")
public ResponseEntity<Map<String, Object>> processOrders(
@RequestParam String date) {
try {
JobParameters params = new JobParametersBuilder()
.addString("orderDate", date)
.addLong("triggeredAt", System.currentTimeMillis())
.toJobParameters();
JobExecution execution = jobLauncher.run(orderProcessingJob, params);
Map<String, Object> response = new HashMap<>();
response.put("jobId", execution.getJobId());
response.put("jobInstanceId", execution.getJobInstance().getId());
response.put("status", execution.getStatus().name());
response.put("startTime", execution.getStartTime());
return ResponseEntity.ok(response);
} catch (JobInstanceAlreadyCompleteException e) {
return ResponseEntity.status(409)
.body(Map.of("error", "该日期的订单已处理完成"));
} catch (Exception e) {
return ResponseEntity.status(500)
.body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/orders/status/{executionId}")
public ResponseEntity<Map<String, Object>> getStatus(
@PathVariable Long executionId,
JobExplorer jobExplorer) {
JobExecution execution = jobExplorer.getJobExecution(executionId);
if (execution == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(Map.of(
"executionId", execution.getId(),
"status", execution.getStatus().name(),
"startTime", execution.getStartTime(),
"endTime", execution.getEndTime(),
"exitCode", execution.getExitStatus().getExitCode()
));
}
}
运行结果:
POST /api/batch/orders/process?date=2026-06-13
→ 200 OK
{
"jobId": 42,
"jobInstanceId": 15,
"status": "STARTED",
"startTime": "2026-06-13T02:00:00"
}
// 等待 30 秒后查询状态
GET /api/batch/orders/status/42
→ 200 OK
{
"executionId": 42,
"status": "COMPLETED",
"startTime": "2026-06-13T02:00:00",
"endTime": "2026-06-13T02:00:25",
"exitCode": "COMPLETED"
}
场景二:定时任务触发——@Scheduled 集成
@Component
public class ScheduledBatchRunner {
private final JobLauncher jobLauncher;
private final Job nightlyReportJob;
public ScheduledBatchRunner(JobLauncher jobLauncher,
@Qualifier("nightlyReportJob") Job job) {
this.jobLauncher = jobLauncher;
this.nightlyReportJob = job;
}
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void runNightlyReport() throws Exception {
JobParameters params = new JobParametersBuilder()
.addString("reportDate", LocalDate.now().toString())
.addLong("run.id", System.currentTimeMillis())
.toJobParameters();
JobExecution execution = jobLauncher.run(nightlyReportJob, params);
System.out.println("Nightly report completed: " + execution.getStatus());
}
}
易错场景与避坑
反例一:同步模式下 Web 请求超时
// ❌ 问题:同步 JobLauncher 在 Web 请求中阻塞 → 请求超时
@PostMapping("/jobs/long-running")
public String runLongJob() throws Exception {
// 这个 Job 要跑 10 分钟,HTTP 请求 30 秒就超时了
JobExecution execution = jobLauncher.run(longJob, params);
return "done"; // 永远到不了这里
}
解决方案:使用异步 JobLauncher 或异步 Controller:
// ✅ 方案一:异步 JobLauncher
@Bean
public JobLauncher asyncJobLauncher(JobRepository jobRepository) {
SimpleJobLauncher launcher = new SimpleJobLauncher();
launcher.setJobRepository(jobRepository);
launcher.setTaskExecutor(new SimpleAsyncTaskExecutor());
return launcher;
}
// ✅ 方案二:Controller 用 @Async
@Async
@PostMapping("/jobs/long-running")
public CompletableFuture<String> runLongJob() throws Exception {
JobExecution execution = jobLauncher.run(longJob, params);
return CompletableFuture.completedFuture(
"Job " + execution.getId() + ": " + execution.getStatus());
}
反例二:异步模式下忘记检查线程安全
// ❌ 异步模式 + 非线程安全的 Reader → 数据错乱
@Bean
public FlatFileItemReader<Order> reader() {
return new FlatFileItemReaderBuilder<Order>()
.name("reader")
.resource(new FileSystemResource("orders.csv"))
.saveState(false) // ❌ 关闭状态保存 + 异步 → 并发问题
.build();
}
正确做法:异步模式下确保 Reader/Writer 是线程安全的,或使用 @StepScope 为每个线程创建独立的 Bean 实例。
面试高频考点
Q1:SimpleJobLauncher 的同步和异步模式有什么区别?
同步模式
run()阻塞直到 Job 完成,适用于命令行和简单场景。异步模式通过设置TaskExecutor使run()立即返回,Job 在后台线程执行。异步模式需要在 Spring Boot 配置中设置spring.batch.job.enabled=false防止启动时自动执行。
Q2:多个 JobLauncher 可以共存吗?
可以。通过在
@Bean方法上使用@Qualifier区分。常见场景:一个同步 JobLauncher 用于命令行,一个异步 JobLauncher 用于 Web 请求。
Q3:JobLauncher 和 JobOperator 有什么区别?
JobLauncher 只负责启动 Job。JobOperator 是一个更高级的接口,聚合了 JobLauncher(启动)+ JobExplorer(查询)+ JobRepository(状态管理),提供 start/stop/restart/abandon 等完整的作业生命周期控制。
上一章:JobParameters 详解下一章:JobRepository 详解