1. この記事について
Spring BatchのTaskletを使用した定期実行の実装について、調べたことをまとめました。
この記事ではTaskletで実装する際の構成や、Spring Batchで使われるメソッドなどについてどのようなものかを書いています。
2. Spring Batchの構成
Spring Batchでバッチ処理を実装する際、ChunkモデルとTaskletモデルでの実装があります。
Chunkモデルは大量のデータを一定件数ずつ分割して処理する一方、Taskletモデルは1回だけの処理を行います。
通常はChunkモデルで大量の売上データなどを処理し、Taskletモデルでメール送信をするなど組み合わせる使い方が一般的です。
SpringBatch:ChunkモデルとTaskletモデルの概要について
今回はChunkモデルは使わず、Taskletのみでの実装を紹介します。
ディレクトリ構成
ソースコード内で使用されているOrderやUserに関連するクラスについては割愛しています。
.
└── src/
└── main/
└── java/
└── com.example.spring_app/
├── batch/
│ └── BatchScheduler.java
├── config/
│ └── BatchConfig.java
├── entities/
│ └── MailTemplate.java
├── repositories/
│ └── MailTemplateRepository.java
└── services/
├── MailComposerService.java
└── MailTasklet.java
この構成では、バッチ処理の役割を以下の3つに分離しています。
1.バッチ処理の定義(BatchConfig)
-
@Configurationアノテーションを付ける -
JobとStepをBeanとして定義する - 「何をどの順番で実行するか」を組み立てる
2.処理の本体(Tasklet)
-
Taskletインターフェースを実装する -
execute()メソッドの中にやりたい処理を書く - DBアクセス、メール送信など、実際のビジネスロジックを記述する
3.起動トリガー(BatchScheduler)
-
@Scheduledで定期実行のタイミング制御する -
JobOperator.start()を呼んでJobを起動する - ただし、APIやCLI起動の場合は不要
3. BatchConfig
BatchConfigは、バッチ処理の「設計図」を定義するクラスです。
「どんな処理を(Step)」「どの順番で実行するか(Job)」をSpring Batchに登録する役割を持ちます。
@Configurationアノテーションを付けることで、SpringがアプリケーションのBean定義として認識します。
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class BatchConfig {
// Jobの定義
@Bean
public Job mailJob(JobRepository jobRepository, Step mailStep) {
return new JobBuilder("mailJob", jobRepository)
.start(mailStep)
.build();
}
// Stepの定義
@Bean
public Step mailStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
MailTasklet tasklet) {
return new StepBuilder("mailStep", jobRepository)
.tasklet(tasklet, transactionManager)
.build();
}
}
3-1. JobRepositoryとは
Spring BatchはJobやStepの実行状態(開始時刻・終了時刻・成功/失敗など)を自動的にDBに登録する仕組みを持っており、その記録係がJobRepositoryです。
開発者が明示的に操作する必要はなく、BatchConfigに渡すことでSpring Batchが内部で使用します。
3-2. Jobの役割
どのStepをどの順番で実行するかという実行フローを定義します。
.start()
-
最初に実行するStepを指定
-
Stepが複数ある場合は
.next(step2).next(step3)と連結する
Job (Spring Batch 6.0.4 API) - Javadoc
3-3. Stepの役割
何をどうやって実行するかという処理実行の単位を定義します。
.tasklet()
- このStepの実行方式としてTaskletモデルを選択し、実行する処理本体(Taskletの実装クラス)を登録する
transactionManager
- Tasklet実行をトランザクション管理する
4. Tasklet
Spring BatchのTaskletは、1つのステップで単純な単一処理(ファイルの削除やデータの初期化、メール送信など)を実行するためのインターフェースです。
Taskletを実装し、executeメソッドに実際の処理を記述することで、独自のバッチ処理を作成できます。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.StepContribution;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.infrastructure.repeat.RepeatStatus;
import org.springframework.stereotype.Component;
@Component
public class MailTasklet implements Tasklet {
private final OrderItemRepository orderItemRepository;
private final MailComposerService mailComposerService;
private final MailTemplateRepository mailTemplateRepository;
MailTasklet(
OrderItemRepository orderItemRepository,
MailComposerService mailComposerService,
MailTemplateRepository mailTemplateRepository
) {
this.orderItemRepository = orderItemRepository;
this.mailComposerService = mailComposerService;
this.mailTemplateRepository = mailTemplateRepository;
}
@Override
public @Nullable RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)
throws Exception {
// 直近1週間の購入商品検索
LocalDateTime end = LocalDate.now().atStartOfDay();
LocalDateTime start = end.minusDays(7);
List<OrderItem> weeklyItems = orderItemRepository.findPurchasedProducts(start, end);
// メール送信
MailTemplate template = mailTemplateRepository.findByTemplateName("weekly_purchase")
.orElseThrow(() -> new RuntimeException("テンプレートが見つかりません"));
mailComposerService.sendMail(template, weeklyItems);
return RepeatStatus.FINISHED;
}
}
4-1. 基本構造
Tasklet はメソッドが execute 1つだけ の 関数型インターフェースです。
Tasklet の動き
TaskletStep が execute() を呼び出す
↓
処理を実行する
↓
RepeatStatus.FINISHED を返す → 正常終了
例外をスローする → 失敗終了
-
execute()は FINISHED を返すまで繰り返し呼ばれます - 各呼び出しはトランザクションでラップされます
Spring Batchの階層との関係
Job
└── Step
└── Tasklet ← ここに実際の処理を書く
Step の実行エンジンがトランザクションを張った上で execute を呼び出してくれるので、Tasklet側はビジネスロジックに集中できる設計になっています。
Tasklet (Spring Batch 6.0.4 API)
implements Taskletを継承
Step の中でやりたい処理を1つ書くためのインターフェースです。
最初にも書きましたが、Spring Batchでステップの処理を実装する方法はTaskletモデルとChunkモデルの2つあります。
- Taskletモデル →
execute()に処理を丸ごと1つ書く。メール送信・ファイル削除など「1回やって終わり」の処理に向いている - Chunkモデル → 大量データを「読む→加工→書く」の単位で繰り返す。CSVインポートなど大量レコード処理に向いている
今回はメール送信という「1回やって終わり」の処理なので、Taskletを選択しています。
RepeatStatusについて
Spring BatchのTasklet.execute()メソッドで返されるRepeatStatusは、ステップの繰り返し(再実行)を制御するEnumです
Taskletはfunctional interfaceで、execute(StepContribution contribution, ChunkContext chunkContext) という抽象メソッドが1つ定義されています。戻り値は @Nullable RepeatStatus です。
TaskletStepによって繰り返し呼び出され、完了の場合はRepeatStatus.FINISHEDを返し、完了していない場合はRepeatStatus.CONTINUABLEを返し、失敗した場合は例外をスローします。
各呼び出しはトランザクション内でラップされます。
RepeatStatus (Spring Batch 6.0.4 API)
TaskletStep :: Spring Batch Reference
4-2. execute メソッドの引数について
| 引数 | 型 | 役割 |
|---|---|---|
contribution |
StepContribution |
現在のStep実行の状態を更新できる(例:読み書き件数など) |
chunkContext |
ChunkContext |
同じStep内の複数回呼び出し間で共有できる属性置き場 |
主なメソッド
| メソッド | 意味 |
|---|---|
incrementReadCount() |
読み込み件数を+1 |
incrementWriteCount(n) |
書き込み件数をn増やす |
incrementReadSkipCount() |
読み込みスキップ件数を+1 |
incrementWriteSkipCount() |
書き込みスキップ件数を+1 |
incrementProcessSkipCount() |
処理スキップ件数を+1 |
setExitStatus(ExitStatus) |
Stepの終了ステータスを設定 |
ChunkContext chunkContext
ChunkContextは、Spring Batchのチャンク処理において、1チャンクの処理単位ごとに生成されるコンテキストオブジェクトです。
Spring BatchはTaskletをラップする際にChunkContextを生成して渡してきます。
Taskletのexecute()は通常1回だけ呼ばれ、RepeatStatus.FINISHEDを返すと処理が終了します。
ただし以下のケースでは複数回呼ばれ、その時に同じChunkContextインスタンスが使われます。
-
RepeatStatus.CONTINUABLEを返す場合 - リトライ設定がある場合のエラー時
同じChunkContextインスタンスが使われるため、前回のexecute()で保存した情報を次回に引き継ぐことができます。
5. BatchScheduler
5-1. Spring BatchのJobを起動する
バッチの起動方法には自動と手動がありますが、今回は手動で起動のコードを実装しています。
手動ではありますが、スケジューラー(@Scheduled(cron = "0 0 9 * * *"))で定期自動実行にしています。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.JobExecutionException;
import org.springframework.batch.core.job.parameters.JobParameters;
import org.springframework.batch.core.job.parameters.JobParametersBuilder;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class BatchScheduler {
private static final Logger log = LoggerFactory.getLogger(BatchScheduler.class);
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
private final JobOperator jobOperator;
private final Job mailJob;
public BatchScheduler(JobOperator jobOperator, Job mailJob) {
this.jobOperator = jobOperator;
this.mailJob = mailJob;
}
@Scheduled(cron = "0 0 9 * * *")
public void run() {
LocalDateTime now = LocalDateTime.now();
try {
JobParameters params = new JobParametersBuilder()
.addLocalDateTime("time", now)
.toJobParameters();
jobOperator.start(mailJob, params);
log.info("メール送信ログ : {}", now.format(TIME_FORMATTER));
} catch (JobExecutionException e) {
log.error("バッチ処理失敗", e);
}
}
}
5-2. JobParametersの基本
JobParametersは、ジョブ実行時に外部から渡すパラメーターをまとめたオブジェクトです。
Spring Batchはジョブの実行状態をDBに記録しており(BATCH_JOB_INSTANCE、BATCH_JOB_EXECUTIONテーブル)、JobParametersを使ってJobInstanceを識別します。これにより「同じ仕事を何度も安全に実行できる」ことを保証しています。
再実行時の挙動は前回の実行ステータスによって異なります。
| 前回のステータス | 同じパラメーターで再実行 |
|---|---|
COMPLETED |
実行を拒否する |
FAILED |
続きから再開できる |
例えばバッチ処理中にサーバーがクラッシュした場合、FAILEDとして記録されるため、同じパラメーターで再実行すれば途中から再開できます。毎回異なるパラメーターを渡す運用では再開ができなくなるため、リトライを想定する場合はパラメーターの設計に注意が必要です。
SpringBatchのJobParametersについて - Web開発における知見共有系ページ
JobParametersBuilder()
JobParametersBuilderは、Spring BatchのJobを実行する際に必要な「パラメータのセット(JobParameters)」を組み立てるためのビルダークラスです。
具体的には、.addString()や.addLong()などのメソッドをチェーンで呼び出しながらパラメータを追加していき、最後に.toJobParameters()を呼ぶことでJobParametersオブジェクトを生成できます。
JobParametersBuilder (Spring Batch 6.0.4 API) - Javadoc
.addLocalDateTime("time", now)
Spring Batchには「同じJobParametersで同じJobは2回実行できない」というルールがあります。
1回目: params = { time: 2025-01-01T10:00:00 } → 実行OK
2回目: params = { time: 2025-01-01T10:00:00 } → 実行NG(同一パラメータ扱い)
2回目: params = { time: 2025-01-01T10:00:01 } → 実行OK(別パラメータ扱い)
今回のLocalDateTime.now() はビジネスロジックに使うためではなく、毎回異なるパラメーターを作って重複実行エラーを防ぐためのタイムスタンプの役割をしています。
jobOperator.start(mailJob, params)
完成したparams を使ってmailJob を起動します。
JobOperator はJobをプログラム的に操作するSpring Batchのインターフェースで、Jobの起動・停止・再起動などができます。
5-3. JobLauncher と JobOperator
Spring Batch 5.x以前は、ジョブを操作するためのインターフェースが2つに分かれていました。
-
JobLauncher
ジョブを起動することに特化しており、Job BeanをDIで受け取ってrun()で実行します。 -
JobOperator
管理運用に特化しており、ジョブ名(文字列)で操作できるため、REST APIのパスパラメーターからそのまま利用できます。停止・再起動・放棄など一通りの操作が揃っています。
しかし、Spring Batch 6.0からはJobOperatorがJobLauncherを継承し、両者は統合されました。
これによりJobLauncherは非推奨となり、6.2以降での削除が予告されています。
What's new in Spring Batch 6 - JobOperator now extends JobLauncher
6.0以降はJobOperatorだけを注入すれば、起動・停止・再起動のすべてを1つのBeanで扱えます。
run() と start() の使い分け
JobOperatorはJobLauncher由来のrun(Job, JobParameters)(非推奨)と、6.0で新設されたstart(Job, JobParameters)の両方を持っています。
今後を見据えるならstart()を使うのが安全です。
JobOperator (Spring Batch 6.0.4 API)
JobLauncher (Spring Batch 6.0.4 API)
6. メールの組み立て
MailComposerServiceではJobが実行されると購入商品のデータが渡され、メールを組み立て送信します。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.mail.MailSendException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import com.example.spring_app.constants.UserStatus;
import com.example.spring_app.entities.MailTemplate;
import com.example.spring_app.entities.User;
import com.example.spring_app.repositories.MailTemplateRepository;
import com.example.spring_app.repositories.UserRepository;
import com.example.spring_app.entities.OrderItem;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@RequiredArgsConstructor
@Slf4j
public class MailComposerService {
private final MailTemplateRepository mailTemplateRepository;
private final UserRepository userRepository;
private final JavaMailSender mailSender;
public void sendMail(MailTemplate template, List<OrderItem> weeklyItems) {
// 商品情報を組み立てる
StringBuilder productList = new StringBuilder();
int i = 1;
for (OrderItem weeklyItem : weeklyItems) {
String orderItemTemplate = template.getOrderItemTemplate();
orderItemTemplate = String.format(orderItemTemplate, i++);
orderItemTemplate = orderItemTemplate.replace("{productName}", weeklyItem.getProductName());
orderItemTemplate = orderItemTemplate.replace("{price}", String.valueOf(weeklyItem.getPrice()));
orderItemTemplate = orderItemTemplate.replace("{productId}", String.valueOf(weeklyItem.getProductId()));
productList.append(orderItemTemplate).append("\n");
}
// ステータスがACTIVEのユーザーだけに絞る
List<User> users = userRepository.findByStatus(UserStatus.ACTIVE);
// メールを組み立てる
List<SimpleMailMessage> messages = new ArrayList<>();
for (User user : users) {
SimpleMailMessage message = new SimpleMailMessage();
String body = template.getBody();
body = body.replace("{userName}", user.getUserName());
body = body.replace("{productList}", productList.toString());
message.setSubject(template.getSubject());
message.setText(body);
message.setTo(user.getEmail());
message.setFrom(template.getFromMail());
messages.add(message);
}
// メール送信
try {
mailSender.send(messages.toArray(new SimpleMailMessage[0]));
} catch (MailSendException e) {
Map<Object, Exception> failedMessages = e.getFailedMessages();
for (Map.Entry<Object, Exception> failedMessage : failedMessages.entrySet()) {
log.error("送信失敗:", failedMessage.getKey(), failedMessage.getValue());
}
}
}
}
6-1. mail_templatesテーブル
| カラム | 型 | 役割 |
|---|---|---|
id |
INT |
主キー |
template_name |
VARCHAR |
テンプレート名(例:weekly_purchase) |
subject |
VARCHAR |
メールの件名(例:今週のおすすめ商品のご紹介) |
order_item_template |
TEXT |
購入商品名や価格などを表示するためのテンプレート |
body |
TEXT |
メール本文 |
from_mail |
VARCHAR |
送信元メールアドレス |
created_at |
DATETIME |
テンプレート作成日時 |
updated_at |
DATETIME |
テンプレート更新日時 |
6-2. 使用インターフェースやメソッドの解説
StringBuilder
複数の商品情報をfor文で1件ずつ取り出して、StringBuilderを使用して「商品情報テンプレート」に文字列を連結します。
まずStringクラスはImmutable(不変)なクラスです。
そのため+演算子で文字列連結するたびにメモリ上の領域が広がってStringインスタンスが大きくなる訳ではありません。
仕組みとしては文字列を追加したり、変更するたびに
新しいStringクラスを生成されています。
つまり内部で何度もインスタンス化をしているのでJVMに負荷がかかり処理が遅くなります。
「+」を用いた文字列連結は、メモリを無駄に消費している。
この問題を解決し、文字列連結を滞りなく行えるクラスがStringBuilderクラスやStringBufferクラスです。
Java StringBuilder バージョン21 API仕様
SimpleMailMessage
送信元、宛先、CC、件名、本文などのフィールドを含む、シンプルなメールメッセージを組み立てるために使用します。
Mavenに以下を追加します。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
SMTPサーバーはMailpitを使用しています。
SMTPサーバーへの接続情報を追加します。
# メール送信設定
server.port=8080
spring.mail.host=localhost
spring.mail.port=1025
spring.mail.username=
spring.mail.password=
spring.mail.properties.mail.smtp.auth=false
spring.mail.properties.mail.smtp.starttls.enable=false
Dockerを使用している場合は以下も追加します。
mailpit:
image: axllent/mailpit
container_name: mailpit
restart: always
ports:
- "1025:1025"
- "8025:8025"
SpringBootでメール送信 - Qiita
ローカルテスト用SMTPメールサーバーはMailpitがオススメ - Qiita
Mailpit - email & SMTP testing tool
SimpleMailMessage (Spring Framework 7.0.8 API)
JavaMailSender
MailSenderインターフェースを拡張したもので、SimpleMailMessageだけでなく添付ファイルやHTML本文などを含むMIMEメール送信できるインターフェースです。
.send()メソッドを使用してメールを送信します。
JavaMailSender (Spring Framework 7.0.8 API)
6-3. エラーキャッチ
try-catchでメールを送信することで、一部の宛先でメール送信が失敗した場合に、送信成功した分の情報も含めて呼び出し元にエラーが返りアプリが落ちてしまうのを防いでいます。
mailSender.send(messages.toArray(new SimpleMailMessage[0]));
-
.toArray(new SimpleMailMessage[0])とすることで、組み立てたメールのリストをSimpleMailMessage型の配列に変換しています。 -
.send()の引数に配列を渡すことで、複数のメールをまとめて送信することができます。
Map<Object, Exception> failedMessages = e.getFailedMessages();
-
MailSenderExceptionは「どのメッセージが失敗したか」を保持するgetFailedMessage()というメソッドを持っています。 -
Map<Object, Exception>にはmessages.toArray(...)で複数のメールをまとめて送信した際に成功した分はそのまま送信され、失敗した分の例外だけ詰められます。 - キー(Obuject):送信に失敗した
SimpleMailMessageのインスタンスが入ります。 - 値(Exception):
JavaMailで発生した例外オブジェクトが入ります。.getMessage()で例外の詳細メッセージを取得できます。
Map.Entry .entrySet()
for (Map.Entry<Object, Exception> failedMessage : failedMessages.entrySet()) {
log.error("送信失敗:", failedMessage.getKey(), failedMessage.getValue());
}
mapに含まれるキーと値のペアをMap.Entryでひとまとめにし、Setとして取得します。
-
.getKey()→ そのペアのキーを取り出す -
.getValue()→ そのペアの値を取り出す
MailSendException (Spring Framework 7.0.8 API)
7. entityとrepository
7-1. MailTemplate
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "mail_templates")
@Getter
@Setter
@EntityListeners(AuditingEntityListener.class)
public class MailTemplate {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "template_name")
private String templateName;
private String subject;
@Column(name = "order_item_template")
private String orderItemTemplate;
@Column(columnDefinition = "TEXT")
private String body;
@Column(name = "from_mail")
private String fromMail;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
@EntityListeners(AuditingEntityListener.class)
Spring Data JPAのAuditing機能を有効にするためのアノテーションです。エンティティに付けることで、作成日時・更新日時などを自動的に埋め込んでくれます。
具体的には以下のようなアノテーションが付いたフィールドに対して自動入力されます。
@CreatedDate
private LocalDateTime createdAt; // レコード作成時に自動セット
@LastModifiedDate
private LocalDateTime updatedAt; // レコード更新時に自動更新
@CreatedBy
private String createdBy; // 作成者を自動セット
@LastModifiedBy
private String lastModifiedBy; // 最終更新者を自動セット
- この機能を動作させるには、メインクラスに
@EnableJpaAuditingを付与する必要があります -
@CreatedBy/@LastModifiedByを使う場合は、さらにAuditorAwareの実装Bean(今ログイン中のユーザーを返す処理)を登録する必要があります
7-2. MailTemplateRepository
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.spring_app.entities.MailTemplate;
@Repository
public interface MailTemplateRepository extends JpaRepository<MailTemplate, Integer> {
Optional<MailTemplate> findByTemplateName(String templateName);
}
8. まとめ
今回はSpring BatchのTaskletモデルを使い、「直近1週間の購入商品をまとめてメール送信する」バッチ処理を実装しました。
Taskletモデルは、Chunkモデルのような「読む→加工→書く」の繰り返し処理とは異なり、execute()に処理を1つ書くだけで完結するシンプルさが魅力です。今回のような「1回やって終わり」の定期処理には、Chunkモデルで無理に構成するよりもコードの見通しが良くなると感じました。
一方で、実装を通じて以下の点は特に押さえておくべきだと感じました。
-
Job/Step/Taskletの3層構造:役割ごとにクラスを分離することで、「何を」「どの順番で」「どう実行するか」を見通し良く整理できる -
JobParametersによる重複実行防止:同じパラメータでは再実行できない仕様を理解した上で、タイムスタンプなどを使い分ける
また、今回の実装はSpring Batch 6.0を前提にしており、JobOperatorがJobLauncherを吸収するなど、5.x系からいくつかAPIが変わっている点にも触れました。もし5.x系で読んでいる方がいれば、パッケージ構成やJobOperatorまわりの違いに注意して読み替えていただければと思います。
公式ドキュメントや関連する技術記事を参考に今回の記事を書きましたが、間違いやもっと良い実装方法などありましたら教えていただけますと幸いです。