
はじめに
Webアプリケーションから時間のかかるバッチ処理をキックしたい、という要求は非常によくあります。
しかし、この一見単純に見える要求には、Webアプリケーションの応答性を損なったり、サーバーリソースを枯渇させたりする危険な落とし穴が潜んでいます。
安易に同期実行で実装してしまうと、バッチ処理が終わるまでユーザーは画面の前で待たされ、最悪の場合、HTTPセッションがタイムアウトしてしまいます。
かといって、単純な非同期実行では、ジョブがいつ終わるのか、成功したのか失敗したのかを知る術がありません。
このハンズオンでは、単純な同期実行から始め、それが引き起こす問題を体験し、最終的に堅牢でスケーラブルな 「非同期ポーリング」アーキテクチャを実装するまでの道のりを追体験します。
Spring Batchが提供するJobLauncherとTaskExecutorの仕組みを理解し、Webとバッチを連携させる上でのベストプラクティスを学びましょう。
Spring Batchのパラメータ駆動とジョブの起動・停止の詳細については、以下の記事で詳細に解説していますので、是非ご覧ください。
目次
- 1. アプリケーションの仕様
- 1-1. 目的
- 1-2. 概要
- 1-3. 主要な仕様
- JobLauncherとAPIエンドポイント
- 非同期ポーリングAPI
- 1-4. フォルダ構成
- 2. Part 1: 基本を学ぶ – 同期実行と非同期実行
- 2-1. Step 1: 長時間かかるジョブの準備
- 2-2. Step 2: 3種類の
TaskExecutorとJobLauncherを定義 - 2-3. 実行方法
- Web UIからの実行
- APIでの直接実行
- 2-4. 実行と結果解説
- 3. Part 2: 同期処理の落とし穴 – 不適切な利用が招く問題
- 3-1. タイムアウト設定の誤解と限界
- 3-2. 同期処理の根本的な問題点
- 3-3. 長時間処理には非同期処理が適している理由
- 3-4. 処理方式の適切な使い分けの重要性
- 4. Part 3: ベストプラクティスへの道 – 非同期ポーリングの実装
- 4-1. アーキテクチャの概要
- 4-2. Step 1: サーバーサイドAPIの実装
- 4-3. Step 2: クライアントサイドの実装
- 4-4. 実行方法
- Web UIからの実行
- APIでの直接実行
- 4-5. 実行と結果解説
- 5. まとめ
対象読者
- WebアプリケーションからSpring Batchジョブを安全かつ効率的に実行したいJava開発者。
- 同期処理と非同期処理の使い分けや、それぞれのメリット・デメリットを理解したい方。
- 長時間実行されるバッチ処理をWeb連携する際のベストプラクティス(非同期ポーリング)を学びたい方。
- Spring Batchの
JobLauncherやTaskExecutorの挙動について深く理解したい方。
1. アプリケーションの仕様
1-1. 目的
本ハンズオンを通じて、以下のスキルを習得することを目的とします。
JobLauncherに異なるTaskExecutorを設定することで、ジョブの同期実行と非同期実行を切り替えられることを理解する。- 同期実行がWebアプリケーションに与える影響(ブロッキング)と、非同期実行の必要性を体感する。
- 長時間実行されるジョブをWebから安全に起動するための、非同期ポーリングアーキテクチャを実装できる。
JobExplorerを使用して、実行中のジョブの状態を取得する方法を学ぶ。
1-2. 概要
このハンズオンでは、意図的に5秒間待機する長時間実行ジョブ (launcherDemoJob) を作成します。
このジョブを、特性の異なる複数のJobLauncher(同期実行、非同期実行)を使ってWeb API経由で起動し、それぞれの挙動の違いを比較します。
最終的には、このジョブを非同期で起動し、クライアント側から実行ステータスを定期的に問い合わせる「非同期ポーリング」の仕組みを実装します。
1-3. 主要な仕様
JobLauncherとAPIエンドポイント
本ハンズオンでは、以下のJobLauncherと、それに対応するAPIエンドポイントを実装します。
| JobLauncher Bean | 利用するTaskExecutor | 実行方式 | APIエンドポイント |
|---|---|---|---|
jobLauncher (デフォルト) | SyncTaskExecutor | 同期 | POST /launch-job/default-sync-demo |
syncJobLauncher | SyncTaskExecutor | 同期 | POST /launch-job/sync-executor-demo |
simpleAsyncJobLauncher | SimpleAsyncTaskExecutor | 非同期 | POST /launch-job/simple-async-demo |
threadPoolAsyncJobLauncher | ThreadPoolTaskExecutor | 非同期 | POST /launch-job/thread-pool-async-demo |
非同期ポーリングAPI
| HTTPメソッド & パス | 概要 |
|---|---|
POST /launch-job/async-start | launcherDemoJobを非同期で起動し、追跡用のjobExecutionIdを即座に返します。 |
GET /launch-job/status/{executionId} | 指定されたjobExecutionIdのジョブの現在のステータス(STARTED, COMPLETEDなど)を返します。 |
1-4. フォルダ構成
今回の実装で追加・修正された主要なファイルは以下の通りです。
src/main/java/com/example/springbatchh2crud/
├── config/
│ └── JobLauncherDemoConfig.java # 今回のデモ用Job, Step, Launcherを定義
├── controller/
│ └── JobLaunchController.java # APIエンドポイントを定義 (修正)
└── tasklet/
└── LongRunningTasklet.java # 長時間実行を模倣するTasklet
src/main/resources/
└── static/
└── index.html # 実行ボタンを配置するUI (修正)Spring Batchのサンプルコードのベースを、以下の記事で提供していますので、本記事の実装をする際に、是非ご活用ください。
2. Part 1: 基本を学ぶ – 同期実行と非同期実行
まずは、長時間かかるジョブをWeb API経由で起動する基本的な仕組みを実装し、同期実行と非同期実行の違いを学びます。
2-1. Step 1: 長時間かかるジョブの準備
非同期処理の必要性を体感するため、意図的に5秒間待機するシンプルなTaskletを作成します。
src/main/java/com/example/springbatchh2crud/tasklet/LongRunningTasklet.java
package com.example.springbatchh2crud.tasklet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import java.util.Map;
/**
* Spring BatchのTaskletインターフェースを実装したクラスです。
* このタスクレットは、長時間かかる処理(例: 外部システム連携、大量データ処理)をシミュレートするために、
* 指定された時間だけスリープします。
* 初心者の方は、Spring Batchの各ステップで実行される最小単位の処理だと理解してください。
*/
public class LongRunningTasklet implements Tasklet {
private static final Logger log = LoggerFactory.getLogger(LongRunningTasklet.class);
private final long duration;
public LongRunningTasklet(long duration) {
this.duration = duration;
}
/**
* タスクレットのメイン処理です。
* ここでビジネスロジックが実行されます。
* この例では、指定されたミリ秒数だけスリープすることで、長時間処理を模倣しています。
*
* @param contribution 現在のステップの実行状況を管理するオブジェクト
* @param chunkContext 現在のチャンクのコンテキスト情報
* @return 処理の継続状態(ここでは完了を示すFINISHED)
* @throws Exception 処理中に発生した例外
*/
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
String threadName = Thread.currentThread().getName();
Map<String, Object> jobParameters = chunkContext.getStepContext().getJobParameters();
log.info("--- LongRunningTasklet START ---");
log.info("Thread: {}", threadName);
log.info("JobParameters: {}", jobParameters);
log.info("Sleeping for {} milliseconds...", duration);
Thread.sleep(duration);
log.info("Awake after {} milliseconds.", duration);
log.info("--- LongRunningTasklet FINISH ---");
return RepeatStatus.FINISHED;
}
}2-2. Step 2: 3種類のTaskExecutorとJobLauncherを定義
Spring Batchのジョブを非同期で実行するには、JobLauncherにTaskExecutorを設定します。
ここでは、同期・非同期の挙動を比較するため、以下の3種類のTaskExecutorと、それに対応するJobLauncherをBeanとして定義します。
src/main/java/com/example/springbatchh2crud/config/JobLauncherDemoConfig.java
package com.example.springbatchh2crud.config;
import com.example.springbatchh2crud.tasklet.LongRunningTasklet;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.TaskExecutorJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;
/**
* Spring Batchのジョブ、ステップ、タスクレット、JobLauncher、TaskExecutorを定義する設定クラスです。
* 初心者の方は、Spring Batchの処理の流れを構成する部品(ジョブ、ステップ、タスクレット)と、
* それらを実行するための設定(JobLauncher、TaskExecutor)がここで定義されていると理解してください。
*/
@Configuration
public class JobLauncherDemoConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
public JobLauncherDemoConfig(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
this.jobRepository = jobRepository;
this.transactionManager = transactionManager;
}
/**
* 長時間実行されるタスクレットのBean定義です。
* このタスクレットは、Spring Batchのステップ内で実行される具体的な処理をカプセル化します。
* @return LongRunningTaskletのインスタンス
*/
@Bean
public Tasklet longRunningTasklet() {
return new LongRunningTasklet(5000); // 5 seconds
}
/**
* 単一のタスクレットを実行するステップのBean定義です。
* ステップはジョブの構成要素であり、タスクレットやチャンク指向処理を実行します。
* @return Stepのインスタンス
*/
@Bean
public Step launcherDemoStep() {
return new StepBuilder("launcherDemoStep", jobRepository)
.tasklet(longRunningTasklet(), transactionManager)
.build();
}
/**
* 定義したステップを含むジョブのBean定義です。
* ジョブはSpring Batchの実行単位であり、一つ以上のステップで構成されます。
* @return Jobのインスタンス
*/
@Bean
public Job launcherDemoJob() {
return new JobBuilder("launcherDemoJob", jobRepository)
.start(launcherDemoStep())
.build();
}
// --- TaskExecutors ---
@Bean
@Qualifier("syncTaskExecutor")
public TaskExecutor syncTaskExecutor() {
return new SyncTaskExecutor();
}
@Bean
@Qualifier("simpleAsyncTaskExecutor")
public TaskExecutor simpleAsyncTaskExecutor() {
return new SimpleAsyncTaskExecutor("simpleAsyncTask-");
}
@Bean
@Qualifier("threadPoolTaskExecutor")
public TaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("threadPoolTaskExecutor-");
executor.initialize();
return executor;
}
// --- JobLaunchers ---
@Bean
@Qualifier("syncJobLauncher")
public JobLauncher syncJobLauncher(@Qualifier("syncTaskExecutor") TaskExecutor taskExecutor) throws Exception {
TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.setTaskExecutor(taskExecutor);
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
@Bean
@Qualifier("simpleAsyncJobLauncher")
public JobLauncher simpleAsyncJobLauncher(@Qualifier("simpleAsyncTaskExecutor") TaskExecutor taskExecutor) throws Exception {
TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.setTaskExecutor(taskExecutor);
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
@Bean
@Qualifier("threadPoolAsyncJobLauncher")
public JobLauncher threadPoolAsyncJobLauncher(@Qualifier("threadPoolTaskExecutor") TaskExecutor taskExecutor) throws Exception {
TaskExecutorJobLauncher jobLauncher = new TaskExecutorJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.setTaskExecutor(taskExecutor);
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
}ポイント:
SyncTaskExecutor: 名前に反して非同期実行は行わず、呼び出し元のスレッドでタスクを同期的に実行します。SimpleAsyncTaskExecutor: タスクを実行するたびに新しいスレッドを生成します。リクエストが頻発すると危険です。ThreadPoolTaskExecutor: スレッドプールを管理し、既存のスレッドを再利用してタスクを実行します。本番環境で推奨される実装です。
2-3. 実行方法
Web UIからの実行
アプリケーションを起動後、ブラウザで http://localhost:8080 を開きます。
「JobLauncher Demo」セクションに4つのボタンがあります。それぞれのボタンが、異なる設定のJobLauncherを呼び出します。
- Default Synchronous JobLauncher:
- Spring Bootが自動設定するデフォルトの同期
JobLauncherを使用します。
- Spring Bootが自動設定するデフォルトの同期
- JobLauncher with SyncTaskExecutor:
SyncTaskExecutorを明示的に設定した同期JobLauncherを使用します。
- JobLauncher with SimpleAsyncTaskExecutor:
SimpleAsyncTaskExecutorを設定した非同期JobLauncherを使用します。
- JobLauncher with ThreadPoolTaskExecutor:
ThreadPoolTaskExecutorを設定した非同期JobLauncherを使用します。
APIでの直接実行
curlコマンドを使って各JobLauncherの動作を直接確認できます。
# 同期実行 (デフォルト): ターミナルは5秒間応答を待つ
curl -X POST http://localhost:8080/launch-job/default-sync-demo
#=> 'Default-Sync-JobLauncher' を使用してジョブ 'launcherDemoJob' の起動をリクエストしました。
# 同期実行 (SyncTaskExecutor): 同様に5秒間待つ
curl -X POST http://localhost:8080/launch-job/sync-executor-demo
#=> 'SyncTaskExecutor-JobLauncher' を使用してジョブ 'launcherDemoJob' の起動をリクエストしました。
# 非同期実行 (SimpleAsyncTaskExecutor): ターミナルは即座に応答を返す
curl -X POST http://localhost:8080/launch-job/simple-async-demo
#=> 'SimpleAsyncTaskExecutor-JobLauncher' を使用してジョブ 'launcherDemoJob' の起動をリクエストしました。
# 非同期実行 (ThreadPoolTaskExecutor): 同様に即座に応答を返す
curl -X POST http://localhost:8080/launch-job/thread-pool-async-demo
#=> 'ThreadPoolTaskExecutor-JobLauncher' を使用してジョブ 'launcherDemoJob' の起動をリクエストしました。2-4. 実行と結果解説
各JobLauncherでジョブを実行し、コンソールログのThread名に注目してください。
同期実行時のログ
2025-11-20T10:29:38.306+09:00 INFO 30727 --- [nio-8080-exec-1] c.e.s.tasklet.LongRunningTasklet : --- LongRunningTasklet START ---
2025-11-20T10:29:38.306+09:00 INFO 30727 --- [nio-8080-exec-1] c.e.s.tasklet.LongRunningTasklet : Thread: http-nio-8080-exec-1
2025-11-20T10:29:38.306+09:00 INFO 30727 --- [nio-8080-exec-1] c.e.s.tasklet.LongRunningTasklet : JobParameters: {executionTime=1763554178295}
2025-11-20T10:29:38.306+09:00 INFO 30727 --- [nio-8080-exec-1] c.e.s.tasklet.LongRunningTasklet : Sleeping for 5000 milliseconds...
2025-11-20T10:29:43.309+09:00 INFO 30727 --- [nio-8080-exec-1] c.e.s.tasklet.LongRunningTasklet : Awake after 5000 milliseconds.Webリクエストを処理したスレッド (http-nio-8080-exec-1) が、そのままTaskletの処理を実行していることがわかります。この間、このスレッドは他のリクエストを処理できず、ブロックされます。
非同期実行時のログ (ThreadPoolTaskExecutorの例)
2025-11-20T10:30:44.561+09:00 INFO 30727 --- [nio-8080-exec-4] c.e.s.controller.JobLaunchController : 'ThreadPoolTaskExecutor-JobLauncher' を使用してジョブ 'launcherDemoJob' の起動をリクエストしました。
2025-11-20T10:30:44.564+09:00 INFO 30727 --- [lTaskExecutor-1] c.e.s.tasklet.LongRunningTasklet : --- LongRunningTasklet START ---
2025-11-20T10:30:44.564+09:00 INFO 30727 --- [lTaskExecutor-1] c.e.s.tasklet.LongRunningTasklet : Thread: threadPoolTaskExecutor-1
2025-11-20T10:30:44.564+09:00 INFO 30727 --- [lTaskExecutor-1] c.e.s.tasklet.LongRunningTasklet : JobParameters: {executionTime=1763554244553}Webリクエストを処理したスレッド (nio-8080-exec-4) はリクエスト受付後すぐに処理を終え、実際のジョブはバックグラウンドのスレッド (threadPoolTaskExecutor-1) で実行されています。これにより、アプリケーションの応答性が保たれます。
3. Part 2: 同期処理の落とし穴 – 不適切な利用が招く問題
同期処理は、リクエストに対して即座に結果が返されることを期待する処理に適しています。しかし、長時間にわたる処理を同期的に実行しようとすると、様々な「落とし穴」に直面します。
3-1. タイムアウト設定の誤解と限界
「長時間待たされる処理をタイムアウトで強制的にキャンセルすれば良い」と考えるかもしれません。しかし、Webアプリケーションフレームワークが提供する一般的なタイムアウト設定は、意図したような処理の強制終了には繋がりません。
server.tomcat.connection-timeoutの例:- この設定は、リクエスト処理そのものの実行時間ではなく、コネクションの確立やアイドル状態に関するものです。
- リクエスト処理中のタイムアウト制御:
- リクエスト処理中のタイムアウトを適切に制御するには、より複雑な実装が必要となります。
3-2. 同期処理の根本的な問題点
技術的な課題以上に重要なのは、「そもそも長時間処理を同期で待つべきではない」 という設計思想です。長時間処理を同期で行うことには、以下のような問題があります。
- クライアント側のタイムアウト発生:
- クライアントは処理完了まで応答を待ち続けるため、クライアント側でタイムアウトが発生しやすくなります。
- リソースの占有:
- サーバー側のリソース(スレッドなど)が長時間占有され、他のリクエストの処理が滞る可能性があります。
- ユーザー体験の悪化:
- ユーザーは処理完了まで待たされるため、アプリケーションの応答性が悪く感じられます。
- 複雑なキャンセル機構:
- サーバー側で同期処理を強制的にキャンセルする仕組みを導入することは、同期処理の本来の目的から逸脱しており、システムの複雑性を不必要に高めます。
3-3. 長時間処理には非同期処理が適している理由
長時間かかる処理には、非同期処理が本質的に適しています。
- バックグラウンド実行:
- ジョブの開始を通知した後、クライアントはバックグラウンドで実行されるジョブの状態を後からポーリングすることで結果を取得できます。
- クライアントの待機不要:
- クライアントは長時間待機する必要がなくなり、他の処理を進めることができます。
- 効率的なリソース利用:
- サーバーリソースも効率的に利用でき、システム全体のパフォーマンスが向上します。
3-4. 処理方式の適切な使い分けの重要性
アーキテクチャとして重要なのは、処理の性質と要件に応じて、同期と非同期の処理方式を適切に使い分けることです。
不適切な処理方式の選択は、単なる技術的な問題に留まらず、システム全体の設計品質や運用効率に悪影響を及ぼします。
そこで今回のハンズオンでは、Webアプリケーションと長時間バッチ処理を連携させる上での「真のベストプラクティス」である非同期ポーリングアーキテクチャを実装してみましょう。
4. Part 3: ベストプラクティスへの道 – 非同期ポーリングの実装
非同期ポーリングアーキテクチャの概要と実装を行っていきます。
4-1. アーキテクチャの概要
- 起動リクエスト:
- クライアントがジョブ起動をリクエスト。
- 即時応答:
- サーバーはジョブを非同期で起動し、即座に追跡用のジョブ実行IDを返す (HTTP 202 Accepted)。
- ステータス確認 (ポーリング):
- クライアントは受け取ったIDを使い、数秒おきにジョブの状態をサーバーに問い合わせる。
- 結果表示:
- ジョブが完了したら、ポーリングを停止し、最終結果を画面に表示する。
4-2. Step 1: サーバーサイドAPIの実装
JobLaunchControllerに2つのエンドポイントを追加します。
POST /launch-job/async-start:- ジョブを非同期で起動し、
jobExecutionIdを返します。
- ジョブを非同期で起動し、
GET /launch-job/status/{executionId}:JobExplorerを使い、指定されたIDのジョブ状態を返します。
src/main/java/com/example/springbatchh2crud/controller/JobLaunchController.java (抜粋)
/**
* Spring Batchジョブの非同期開始をトリガーするAPIエンドポイントです。
* このエンドポイントを呼び出すと、ジョブはバックグラウンドで実行され、
* すぐにジョブ実行IDとステータスを返します。
* 初心者の方は、Web画面からバッチ処理を開始する「入り口」だと理解してください。
* @return ジョブ実行IDと現在のステータスを含むレスポンス
*/
@PostMapping("/async-start")
public ResponseEntity<Map<String, Object>> asyncStart() {
// ...
JobExecution jobExecution = threadPoolAsyncJobLauncher.run(launcherDemoJob, jobParameters);
// ...
response.put("jobExecutionId", jobExecution.getId());
response.put("status", jobExecution.getStatus());
return ResponseEntity.accepted().body(response);
}
/**
* 指定されたジョブ実行IDの現在のステータスを取得するためのAPIエンドポイントです。
* 非同期で開始されたジョブの進捗状況を、クライアント側から定期的に問い合わせる(ポーリングする)際に使用します。
* 初心者の方は、実行中のバッチ処理が今どういう状態か(完了したか、エラーになったかなど)を確認する「窓口」だと理解してください。
* @param executionId ステータスを取得したいジョブの実行ID
* @return ジョブの現在のステータスを含むレスポンス
*/
@GetMapping("/status/{executionId}")
public ResponseEntity<Map<String, Object>> getJobStatus(@PathVariable Long executionId) {
JobExecution jobExecution = jobExplorer.getJobExecution(executionId);
// ...
return ResponseEntity.ok(response);
}4-3. Step 2: クライアントサイドの実装
index.htmlに、ポーリング処理を行うJavaScriptを実装します。
src/main/resources/static/index.html (抜粋)
// --- Polling Job Logic ---
const startPollingBtn = document.getElementById("startPollingJob");
const pollingResultDiv = document.getElementById("pollingResult");
let pollingInterval;
startPollingBtn.addEventListener("click", async () => {
// 1. Start the job
const startResponse = await fetch("/launch-job/async-start", { method: "POST" });
const startData = await startResponse.json();
const executionId = startData.jobExecutionId;
// 2. Poll for status
pollingInterval = setInterval(async () => {
const statusResponse = await fetch(`/launch-job/status/${executionId}`);
const statusData = await statusResponse.json();
// 3. Stop polling if finished
if (statusData.status === "COMPLETED" || statusData.status === "FAILED") {
clearInterval(pollingInterval);
}
}, 2000); // Poll every 2 seconds
});4-4. 実行方法
Web UIからの実行
ブラウザで http://localhost:8080 を開き、「Asynchronous Job with Polling」セクションの「Start Asynchronous Job」ボタンをクリックします。
ボタンの下の領域に、まずジョブが開始された旨とjobExecutionIdが表示され、2秒ごとにステータスが更新され、最終的に完了結果が表示されることが確認できます。
APIでの直接実行
curlとjq(JSONパーサー)を使って、APIレベルでのポーリングをシミュレートできます。
1. 非同期ジョブの起動とjobExecutionIdの取得
# -s (silent) オプションで進捗表示を消し、jqでjobExecutionIdを抽出して変数に格納
EXECUTION_ID=$(curl -s -X POST http://localhost:8080/launch-job/async-start | jq '.jobExecutionId')
echo "Job started with Execution ID: $EXECUTION_ID"
#=> Job started with Execution ID: 52. ステータスのポーリング
# 取得したIDを使ってステータスを確認(ジョブ完了まで数回実行する)
curl http://localhost:8080/launch-job/status/$EXECUTION_ID | jq4-5. 実行と結果解説
/async-startを呼び出すと、サーバーは即座にHTTP 202 Acceptedを返し、以下のようなJSONレスポンスを送信します。
{
"message": "Job launch request accepted.",
"jobExecutionId": 5,
"status": "STARTING"
}この時、サーバーログではバックグラウンドスレッドでジョブが開始されていることが確認できます。
2025-11-20T10:31:31.567+09:00 INFO 30727 --- [lTaskExecutor-2] o.s.b.c.l.s.TaskExecutorJobLauncher : Job: [SimpleJob: [name=launcherDemoJob]] launched with the following parameters: ...
2025-11-20T10:31:31.570+09:00 INFO 30727 --- [lTaskExecutor-2] c.e.s.tasklet.LongRunningTasklet : --- LongRunningTasklet START ---
2025-11-20T10:31:31.570+09:00 INFO 30727 --- [lTaskExecutor-2] c.e.s.tasklet.LongRunningTasklet : Thread: threadPoolTaskExecutor-2
2025-11-20T10:31:31.570+09:00 INFO 30727 --- [lTaskExecutor-2] c.e.s.tasklet.LongRunningTasklet : JobParameters: {executionTime=1763554291562}その後、curl http://localhost:8080/launch-job/status/5 | jq を実行すると、最初は"status": "STARTED"が返り、約5秒後には以下のように"status": "COMPLETED"が返ってくることが確認できます。
{
"jobExecutionId": 5,
"startTime": "2025-11-20T10:31:31.567907",
"endTime": "2025-11-20T10:31:36.585384",
"exitStatus": "COMPLETED",
"status": "COMPLETED"
}これにより、クライアントはサーバーをブロックすることなく、自身のペースでジョブの完了を知ることができます。
5. まとめ
このハンズオンを通じて、私たちは以下の重要な教訓を学びました。
- 同期処理の危険性:
- 長時間かかる処理を同期で実行すると、ユーザー体験を損ない、サーバーリソースを浪費します。
- プロパティの正確な理解:
- フレームワークが提供する設定プロパティは、その意味を正確に理解して使わなければ、意図しない結果を招きます。
- エラーは学びの宝庫:
- 開発中に発生するエラーは、フレームワークの内部的な仕組みや、より良い実装方法を学ぶ絶好の機会となります。
- 非同期ポーリーングの有効性:
- Webとバッチ処理を連携させる場合、非同期でジョブを起動し、クライアントから状態をポーリングするアーキテクチャが、堅牢でスケーラブルなベストプラクティスです。
安易な実装は避け、それぞれの技術の特性を理解した上で、適切なアーキテクチャを選択することが、信頼性の高いシステムを構築する鍵となります。
免責事項
本記事の内容は、執筆時点での情報に基づいています。技術情報は常に変化するため、記載された情報が常に最新かつ正確であることを保証するものではありません。本記事の情報を利用したことにより生じるいかなる損害についても、筆者は一切の責任を負いません。ご自身の判断と責任においてご利用ください。
