- Spring
Spring Batch入門|Chunk・TaskletをBoot 4で書く
Spring Batch は、大量のデータを「読み込む → 加工する → 書き込む」処理を、決まった件数(チャンク)ごとにまとめてコミットしながら進めるためのフレームワークです。 途中で止まっても、どこまで終わったかを記録しているので、同じパラメーターで再実行すると続きから動きます。
検索で見つかる入門記事の多くは Spring Boot 2〜3(Spring Batch 4〜5)の書き方です。Spring Boot 4 に対応する Spring Batch 6 では、スターターが「DB あり/なし」の2種類に分かれ、chunk() の書き方とパッケージ名が変わりました。 この記事では、仕組みを表と図で整理したうえで、CSV を DB に取り込むジョブを Spring Batch 6 の書き方で全文載せ、5系との違いを対応表にしています。
確認環境:Spring Boot 4.1.1、Spring Batch 6.0.5 の公式リファレンス・移行ガイドで確認(確認日 2026年10月10日)。
全体像:Job・Step・Chunk・Tasklet
Job(memberImportJob)
├─ Step 1:cleanupStep(Tasklet) ……… 前回のエラー記録を消す(1回で終わる処理)
└─ Step 2:importStep(Chunk 100件)… CSV を読む → 加工・検証 → DB に書く
├─ ItemReader 1件ずつ読む ×100回
├─ ItemProcessor 1件ずつ加工 ×100回(null を返すとその行は捨てる)
└─ ItemWriter 100件まとめて書く → コミット → 次の100件へ
| 用語 | 意味 | 例 |
|---|---|---|
| Job | バッチ全体。1つ以上の Step を順に(または条件で分岐して)動かす | 会員データの取り込み |
| Step | Job の中の1工程 | 前処理、取り込み、集計 |
| Chunk 指向の Step | 読み込み・加工・書き込みを N 件ずつ繰り返し、N 件ごとにコミットする | CSV 10万行を100件ずつ DB へ |
| Tasklet の Step | 1つの処理を実行して終わる(トランザクションで包まれる) | 一時テーブルの削除、ファイルの移動 |
| JobParameters | 実行ごとに渡すパラメーター。同じ値なら「同じ実行(JobInstance)」とみなされる | input.file=...、target.date=2026-10-10 |
| JobRepository | 実行の記録(どこまで終わったか)を持つ | DB のメタデータテーブル |
Chunk と Tasklet の使い分け
| Chunk | Tasklet | |
|---|---|---|
| 向いている処理 | 件数の多い「読む→加工→書く」 | 1回で終わる処理(削除、ファイル操作、ストアド呼び出し) |
| コミットの単位 | N 件ごと | Tasklet の1回の実行ごと |
| 途中で止まったとき | コミット済みのチャンクの続きから再開できる | Step の最初からやり直し |
| スキップ・リトライ | 件単位で設定できる | 自分で書く |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring Batch入門|Chunk・TaskletをBoot 4で書く」
Spring Boot 4 での依存関係
Spring Boot 4 では、Spring Batch のスターターが2つに分かれました。
| スターター | 実行の記録 | 向いている場面 |
|---|---|---|
spring-boot-starter-batch-jdbc |
DB のメタデータテーブルに保存 | 本番のバッチ(再実行・続きからの再開が必要) |
spring-boot-starter-batch |
メモリだけ(DB を使わない) | 使い捨ての処理、試し書き |
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Spring Boot 3 から上げて spring-boot-starter-batch のままだと、実行の記録が DB に保存されなくなります(Spring Boot 4.0 の移行ガイドにも、DB を使い続けるには spring-boot-starter-batch-jdbc に変える必要があると書かれています)。再実行や続きからの再開を使っているジョブは、JDBC 版のスターターに変えます。
# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/app
spring.datasource.username=app
spring.datasource.password=secret
# メタデータのテーブル(BATCH_JOB_INSTANCE など)を作る。本番は DBA が作るなら never
spring.batch.jdbc.initialize-schema=always
# Job が複数あるときは、起動時に動かす Job を名前で指定する
spring.batch.job.name=memberImportJob
Spring Boot では @EnableBatchProcessing を付けません。 付けると Spring Boot の自動構成が止まり、起動時のジョブの実行やテーブルの作成が行われなくなります。
完成コード:CSV を DB に取り込むジョブ
入力の CSV と取り込み先のテーブル
id,name,email,joined_on
1001,佐藤 花子,hanako@example.com,2026-04-01
1002,鈴木 一郎,ichiro(at)example.com,2026-04-01
1003, 高橋 健 ,KEN@EXAMPLE.COM,2026-05-10
CREATE TABLE member (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL,
joined_on DATE NOT NULL
);
CREATE TABLE member_import_error (
id BIGSERIAL PRIMARY KEY,
message VARCHAR(500) NOT NULL
);
1002 はメールアドレスの形が正しくないので取り込まずに飛ばし(スキップ)、1003 は前後の空白を除き、メールアドレスを小文字にして取り込みます。
データの型
package com.example.batch;
/** CSV の1行(文字列のまま受け取る) */
public record MemberCsv(String id, String name, String email, String joinedOn) {}
package com.example.batch;
import java.time.LocalDate;
/** DB に書き込む形 */
public record Member(long id, String name, String email, LocalDate joinedOn) {}
package com.example.batch;
/** 取り込めない行を表す例外(スキップの対象にする) */
public class InvalidMemberException extends RuntimeException {
public InvalidMemberException(String message) {
super(message);
}
}
Job の設定(Spring Batch 6 の書き方)
package com.example.batch;
import java.time.LocalDate;
import java.util.Set;
import javax.sql.DataSource;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.skip.LimitCheckingExceptionHierarchySkipPolicy;
import org.springframework.batch.infrastructure.item.ItemProcessor;
import org.springframework.batch.infrastructure.item.database.JdbcBatchItemWriter;
import org.springframework.batch.infrastructure.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.infrastructure.item.file.FlatFileItemReader;
import org.springframework.batch.infrastructure.item.file.FlatFileParseException;
import org.springframework.batch.infrastructure.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.infrastructure.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class MemberImportJobConfig {
@Bean
public Job memberImportJob(JobRepository jobRepository, Step cleanupStep, Step importStep) {
return new JobBuilder("memberImportJob", jobRepository)
.start(cleanupStep)
.next(importStep)
.build();
}
/** Step 1:前回のエラー記録を消す(Tasklet) */
@Bean
public Step cleanupStep(JobRepository jobRepository, PlatformTransactionManager transactionManager,
JdbcTemplate jdbc) {
return new StepBuilder("cleanupStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
jdbc.update("DELETE FROM member_import_error");
return RepeatStatus.FINISHED; // 1回で終わる
}, transactionManager)
.build();
}
/** Step 2:CSV を100件ずつ読み、加工して DB に書く(Chunk) */
@Bean
public Step importStep(JobRepository jobRepository, PlatformTransactionManager transactionManager,
FlatFileItemReader<MemberCsv> memberReader,
ItemProcessor<MemberCsv, Member> memberProcessor,
JdbcBatchItemWriter<Member> memberWriter) {
var skippable = Set.<Class<? extends Throwable>>of(
InvalidMemberException.class, FlatFileParseException.class);
return new StepBuilder("importStep", jobRepository)
.<MemberCsv, Member>chunk(100)
.transactionManager(transactionManager)
.reader(memberReader)
.processor(memberProcessor)
.writer(memberWriter)
.faultTolerant()
.skipPolicy(new LimitCheckingExceptionHierarchySkipPolicy(skippable, 10)) // 10件まで飛ばす
.build();
}
/** 読み込み:ファイル名は JobParameters の input.file で受け取る */
@Bean
@StepScope
public FlatFileItemReader<MemberCsv> memberReader(
@Value("#{jobParameters['input.file']}") String inputFile) {
return new FlatFileItemReaderBuilder<MemberCsv>()
.name("memberReader")
.resource(new FileSystemResource(inputFile))
.encoding("UTF-8")
.linesToSkip(1) // 見出し行
.delimited()
.names("id", "name", "email", "joinedOn")
.targetType(MemberCsv.class) // record にそのまま詰める
.build();
}
/** 加工:検証と整形。例外を投げた行はスキップ、null を返した行は捨てる */
@Bean
public ItemProcessor<MemberCsv, Member> memberProcessor() {
return csv -> {
String email = csv.email().strip().toLowerCase();
if (!email.matches("[^@\\s]+@[^@\\s]+\\.[^@\\s]+")) {
throw new InvalidMemberException("メールアドレスの形が正しくありません: id=" + csv.id());
}
return new Member(
Long.parseLong(csv.id().strip()),
csv.name().strip(),
email,
LocalDate.parse(csv.joinedOn().strip()));
};
}
/** 書き込み:100件をまとめて1回の JDBC バッチで INSERT */
@Bean
public JdbcBatchItemWriter<Member> memberWriter(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<Member>()
.dataSource(dataSource)
.sql("INSERT INTO member (id, name, email, joined_on) "
+ "VALUES (:id, :name, :email, :joinedOn)")
.itemSqlParameterSourceProvider(m -> new MapSqlParameterSource()
.addValue("id", m.id())
.addValue("name", m.name())
.addValue("email", m.email())
.addValue("joinedOn", m.joinedOn()))
.build();
}
}
| 部品 | ポイント |
|---|---|
chunk(100) + .transactionManager(...) |
Spring Batch 6 の書き方。5系の chunk(100, transactionManager) は6系では非推奨 |
@StepScope + #{jobParameters['input.file']} |
Step の実行時に Bean を作るので、実行ごとのパラメーターを受け取れる |
targetType(MemberCsv.class) |
CSV の列を record の項目に名前で詰める |
ItemProcessor が null を返す |
その行は書き込まずに捨てる(フィルター)。件数は filter_count に記録される |
ItemProcessor が例外を投げる |
スキップの対象なら飛ばす(process_skip_count)。対象外なら Step が失敗する |
JdbcBatchItemWriter |
チャンクの件数ぶんを JDBC のバッチ更新で1回に送る |
実行する
Spring Boot のアプリとして起動すると、Job が自動で実行されます。ジョブのパラメーターは -- を付けずに渡します(-- 付きは Spring の設定値として扱われ、ジョブには渡りません)。
java -jar member-batch.jar input.file=/data/members.csv target.date=2026-10-10
# 設定値とジョブのパラメーターを一緒に渡す例(ジョブに渡るのは input.file だけ)
java -jar member-batch.jar --spring.profiles.active=prod input.file=/data/members.csv
| やりたいこと | 方法 |
|---|---|
| 起動時に自動で動かさない(Web アプリから起動するなど) | spring.batch.job.enabled=false |
| Job が複数ある | spring.batch.job.name=memberImportJob で1つ指定 |
| コードから起動する | JobOperator を注入して start(job, jobParameters)(6系では JobLauncher の役割も JobOperator が持つ) |
再実行と JobParameters:同じパラメーターでは2回動かない
Spring Batch は、名前とパラメーターが同じ実行を「同じ JobInstance」とみなします。
| 前回の結果 | 同じパラメーターで実行すると |
|---|---|
| COMPLETED(成功) | 実行されない(JobInstanceAlreadyCompleteException)。もう一度動かすならパラメーターを変える |
| FAILED(失敗) | 続きから再開する。Chunk の Step はコミット済みのチャンクの次から読む |
| STARTED のまま(プロセスが強制終了した) | そのままでは再実行できない。状態を確認して FAILED にしてから再実行する(6系では JobOperator に失敗した実行を回復する recover が追加された) |
毎日動かすジョブは、target.date=2026-10-10 のように処理対象を表すパラメーターを付けると、「その日の分は1回だけ」「FAILED の日だけやり直す」が自然に表せます。
RunIdIncrementer で毎回 run.id を増やす方法もありますが、Spring Batch 6 では、インクリメンターを付けたジョブに利用者が渡した追加のパラメーターは警告付きで無視されます(移行ガイドより)。ファイル名などをパラメーターで渡すジョブでは、インクリメンターに頼らず、識別できるパラメーターを自分で渡します。
再実行するときは、識別に使うパラメーターも、識別に使わないパラメーターも、すべて前回と同じものを指定し直します(前回の値は引き継がれません)。
メタデータのテーブル:どこまで終わったかを見る
JDBC 版では、次のテーブルに実行の記録が残ります。
| テーブル | 中身 |
|---|---|
BATCH_JOB_INSTANCE |
Job の名前とパラメーターの組み合わせ(JobInstance) |
BATCH_JOB_EXECUTION |
実行1回ごとの開始・終了時刻、状態(COMPLETED・FAILED など) |
BATCH_JOB_EXECUTION_PARAMS |
実行に渡したパラメーター |
BATCH_STEP_EXECUTION |
Step ごとの件数(読み・書き・スキップ・コミット・ロールバック)と状態 |
BATCH_JOB_EXECUTION_CONTEXT/BATCH_STEP_EXECUTION_CONTEXT |
再開に使う情報(何行目まで読んだか など) |
-- 直近の実行と状態
SELECT ji.job_name, je.job_execution_id, je.status, je.exit_code, je.start_time, je.end_time
FROM batch_job_execution je
JOIN batch_job_instance ji ON ji.job_instance_id = je.job_instance_id
ORDER BY je.job_execution_id DESC
LIMIT 10;
-- Step ごとの件数(1回の実行ぶん)
SELECT step_name, status, read_count, filter_count, write_count,
read_skip_count, process_skip_count, write_skip_count,
commit_count, rollback_count
FROM batch_step_execution
WHERE job_execution_id = 42
ORDER BY step_execution_id;
上の CSV(3行)を取り込むと、importStep には「読み3件・書き2件・加工でのスキップ1件」が記録されます(スキップが起きたチャンクでは、ロールバックとやり直しの回数も記録されます)。運用では、この件数を見て「読んだ件数=書いた件数+捨てた件数+スキップ件数」になっているかを確かめます。
スキップとリトライ(Spring Batch 6)
| 設定 | 書き方 | 意味 |
|---|---|---|
| スキップ | .faultTolerant().skipPolicy(new LimitCheckingExceptionHierarchySkipPolicy(例外の集合, 上限)) |
指定した例外の行を飛ばす。上限を超えた次の例外で Step が失敗する(上限10なら11件目で失敗) |
| リトライ | .faultTolerant().retryPolicy(RetryPolicy.builder().maxRetries(3).includes(例外の集合).build()) |
一時的なエラー(デッドロックなど)の行を最大3回やり直す |
| 飛ばした行の記録 | SkipListener を実装して .listener(...) |
飛ばした行と理由をエラーテーブルやログに残す |
Spring Batch 6 のリトライは、Spring Retry ライブラリではなく Spring Framework 7 のリトライ機能(org.springframework.core.retry.RetryPolicy) を使うように変わりました。
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.includes(Set.of(DeadlockLoserDataAccessException.class))
.build();
return new StepBuilder("importStep", jobRepository)
.<MemberCsv, Member>chunk(100)
.transactionManager(transactionManager)
.reader(memberReader)
.processor(memberProcessor)
.writer(memberWriter)
.faultTolerant()
.retryPolicy(retryPolicy)
.build();
Spring Batch 5 から 6 で変わったところ
| 項目 | 5系(Spring Boot 3) | 6系(Spring Boot 4) |
|---|---|---|
| スターター | spring-boot-starter-batch(DB に記録) |
spring-boot-starter-batch-jdbc(DB に記録)/spring-boot-starter-batch(メモリのみ) |
| Chunk の書き方 | .chunk(100, transactionManager) |
.chunk(100).transactionManager(transactionManager)(旧形式は非推奨として6系の間は残る) |
Job・JobExecution などのパッケージ |
org.springframework.batch.core |
org.springframework.batch.core.job |
Step・StepExecution・StepContribution |
org.springframework.batch.core |
org.springframework.batch.core.step |
JobParameters など |
org.springframework.batch.core |
org.springframework.batch.core.job.parameters |
| リスナーのインターフェース | org.springframework.batch.core |
org.springframework.batch.core.listener |
ItemReader・FlatFileItemReader などの部品 |
org.springframework.batch.item.* |
org.springframework.batch.infrastructure.item.* |
RepeatStatus |
org.springframework.batch.repeat |
org.springframework.batch.infrastructure.repeat |
| ジョブの起動 | JobLauncher |
JobOperator(JobLauncher を継承。JobLauncher は非推奨) |
JobExplorer |
別の Bean | JobRepository が JobExplorer を継承 |
@EnableBatchProcessing の DB 設定 |
@EnableBatchProcessing(dataSourceRef = ...) |
@EnableJdbcJobRepository(dataSourceRef = ...) に分離 |
DefaultBatchConfiguration の既定 |
DB に記録 | メモリのみ(DB に記録するなら JdbcDefaultBatchConfiguration) |
| リトライ | Spring Retry | Spring Framework 7 のリトライ |
| コマンドラインの起動クラス | CommandLineJobRunner |
CommandLineJobOperator |
| 5系で開始して FAILED のジョブの再開 | ― | 6系では再開できない(パラメーターの保存形式が変わったため)。移行前に終わらせておく |
移行で特に気をつけるのは2点です。1つは、スターターを JDBC 版に替えないと実行の記録が DB に残らなくなること。もう1つは、5系で FAILED のまま残っているジョブを6系では再開できないことです。上げる前に、FAILED のジョブを完了させておきます。
よくある質問
Q. チャンクの件数はいくつにすればいいですか。 A. 数十〜数千件が目安で、100〜1000 から始めて計測して決めます。大きいほどコミットの回数が減って速くなりますが、エラーのときにやり直す件数と、1回のトランザクションで使うメモリが増えます。
Q. 処理の途中で例外が出たら、それまでの件はどうなりますか。
A. コミット済みのチャンクは DB に残り、例外が起きたチャンクはロールバックされます。同じパラメーターで再実行すると、そのチャンクから再開します。読み込み側が再開に対応した部品(FlatFileItemReader など)であることが前提です。
Q. Web アプリの中で、画面の操作からバッチを動かしたいです。
A. spring.batch.job.enabled=false で起動時の自動実行を止め、JobOperator を注入して実行します。時間のかかるジョブは、非同期に実行して、状態はメタデータのテーブルから表示します。
Q. cron で毎日動かしたいです。
A. OS の cron やジョブ管理ツールから java -jar app.jar target.date=$(date +%F) ... のように、日付をパラメーターにして起動する形が扱いやすくなります。アプリの中で @Scheduled から JobOperator を呼ぶ方法もありますが、多重起動の防止とサーバーが複数台のときの扱いを別に考える必要があります。
まとめ
- Job は Step の集まり、Step は Chunk(N件ずつ読む→加工→書く→コミット)か Tasklet(1回の処理)
- Spring Boot 4 では
spring-boot-starter-batch-jdbcを使うと、実行の記録が DB に残り、FAILED のジョブを続きから再開できる - Spring Batch 6 の Chunk は
chunk(100).transactionManager(tx)。部品のパッケージはorg.springframework.batch.infrastructure.*に移った - ジョブのパラメーターは
--を付けずに渡し、同じパラメーターで成功済みのジョブは再実行されない。日付などの識別できるパラメーターを付ける - スキップは
LimitCheckingExceptionHierarchySkipPolicy、リトライは Spring Framework 7 のRetryPolicyで設定する - 運用では
BATCH_STEP_EXECUTIONの件数(読み・書き・スキップ) で取り込みの結果を確かめる
参考・出典
確認日はいずれも 2026年10月10日です。
- Spring Batch リファレンス(6.0.5)「What's new in Spring Batch 6.0」:https://docs.spring.io/spring-batch/reference/whatsnew.html
- Spring Batch「Spring Batch 6.0 Migration Guide」:https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-6.0-Migration-Guide
- Spring Batch リファレンス(6.0.5)「Configuring a Step(Chunk-oriented Processing)」:https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/configuring.html
- Spring Batch リファレンス(6.0.5)「Configuring Skip Logic」「Configuring Retry Logic」:https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/configuring-skip.html 、https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/retry-logic.html
- Spring Batch リファレンス(6.0.5)「TaskletStep」:https://docs.spring.io/spring-batch/reference/step/tasklet.html
- Spring Batch リファレンス「Meta-Data Schema」:https://docs.spring.io/spring-batch/reference/schema-appendix.html
- Spring Boot リファレンス(4.1.1)「Batch Applications」:https://docs.spring.io/spring-boot/how-to/batch.html
- Spring Boot「Spring Boot 4.0 Migration Guide」:https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring Batch入門|Chunk・TaskletをBoot 4で書く」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。