モダンJavaバッチ開発:Spring Batch TaskletAdapterで既存のサービスをSpring Batchに組み込むハンズオンガイド

Spring Batchを使っていると、「すでに実装済みの便利なサービスクラスがあるのに、バッチ処理のためだけに同じようなロジックをTaskletとして再実装するのは面倒だ…」と感じることはありませんか?

DRY原則(Don’t Repeat Yourself)の観点からも、テスト済みの安定したビジネスロジックを再利用するのが理想的です。

そんな悩みをスマートに解決してくれるのが TaskletAdapter です。

この記事では、TaskletAdapter を使って、バッチ用に作られていない既存クラスのメソッドを簡単にバッチ処理のステップとして組み込む方法を、ハンズオン形式で詳しく解説します。


目次

  1. TaskletAdapterとは?
    1-1. TaskletAdapterが役立つ場面
  2. 今回の実装目標
  3. 【ステップ1】:ビジネスロジックを担うサービスクラスの作成
    3-1. UserService
    3-2. SummaryService
    3-3. ExternalNotificationService
  4. 【ステップ2】:TaskletAdapterを使ったバッチジョブの定義
    補足:メソッドに引数を渡す場合
  5. 【ステップ3】:ジョブ実行用APIの作成
  6. 【ステップ4】:動作確認
  7. まとめ

対象読者

  • Spring Batchの基本的な概念を理解している方
  • 既存のJavaサービスをSpring Batchに組み込む方法を知りたい方
  • DRY原則に基づいた効率的なバッチ処理の実装に関心がある方

1. TaskletAdapterとは?

TaskletAdapter は、Spring Batchが提供するアダプタークラスの一つで、Tasklet インターフェースを実装していない既存のクラスやBeanのメソッドを、直接呼び出すことを可能にします。

通常、Spring Batchで単一の操作(ファイルの移動、テーブルの初期化など)を行うには、Tasklet インターフェースを実装したクラスを作成する必要があります。

しかし、TaskletAdapter を仲介させることで、この実装の手間を完全に省略できます。


1-1. TaskletAdapterが役立つ場面

ロジックを再利用することで、開発スピードが向上するだけでなく、コードの重複がなくなり、アプリケーション全体のメンテナンス性も向上します。

TaskletAdapterが適しているいくつかのケースを挙げてみます。

  • 複雑なビジネスロジックの再利用:
    • すでに実装・テスト済みの @Service クラスのメソッドを、そのままバッチ処理で使いたい場合。
  • DB操作の共通化:
    • 複数のアプリケーションで共通利用している @Repository やDAOの更新メソッドをバッチから呼び出したい場合。
  • 外部連携処理の活用:
    • メール送信や外部APIクライアントなど、既存の連携用コンポーネントをバッチのフローに組み込みたい場合。

Spring BatchのChunk処理の詳細については、以下の記事で詳細に解説していますので、是非ご覧ください。


2. 今回の実装目標

このハンズオンでは、以下の3つの異なる役割を持つサービスメソッドを順次呼び出す、シンプルなSpring Batchジョブ (taskletAdapterJob) を構築します。

  1. UserService: DBからユーザー情報を取得し、ステータスを一括更新する。
  2. SummaryService: DBの集計テーブルの値を更新する。
  3. ExternalNotificationService: 外部システムへの通知処理をシミュレートする。

これらの処理を TaskletAdapter を使ってステップ化し、Web API経由でジョブを実行できるようにします。

図:taskletAdapterJobの実装イメージ

3つの異なる役割を持つサービスメソッドを順次呼び出す、シンプルなSpring Batchジョブ (taskletAdapterJob)

Spring Batchのサンプルコードのベースを、以下の記事で提供していますので、本記事の実装をする際に、是非ご活用ください。


3. 【ステップ1】:ビジネスロジックを担うサービスクラスの作成

まず、TaskletAdapter から呼び出す3つのサービスクラスを作成します。これらは Tasklet インターフェースを実装しない、ごく一般的な @Service コンポーネントです。


3-1. UserService

USERS テーブルの全レコードの status'inactive' に更新するビジネスロジックです。

src/main/java/com/example/springbatchh2crud/service/UserService.java

package com.example.springbatchh2crud.service;

import com.example.springbatchh2crud.model.User;
import com.example.springbatchh2crud.repository.UserRepository;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public void updateUserStatus() {
        log.info("Starting to update user statuses to 'inactive'.");

        List<User> users = userRepository.findAll();
        if (users.isEmpty()) {
            log.info("No users found to update.");
            return;
        }

        for (User user : users) {
            user.setStatus("inactive");
        }

        userRepository.saveAll(users);

        log.info("{} users have been updated to 'inactive' status.", users.size());
    }
}

3-2. SummaryService

SUMMARIES テーブルの summaryValue100 を加算する処理です。

src/main/java/com/example/springbatchh2crud/service/SummaryService.java

package com.example.springbatchh2crud.service;

import com.example.springbatchh2crud.model.Summary;
import com.example.springbatchh2crud.repository.SummaryRepository;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class SummaryService {

    private final SummaryRepository summaryRepository;

    public SummaryService(SummaryRepository summaryRepository) {
        this.summaryRepository = summaryRepository;
    }

    @Transactional
    public void updateSummary() {
        log.info("Starting to update summary values.");

        List<Summary> summaries = summaryRepository.findAll();
        if (summaries.isEmpty()) {
            log.info("No summaries found to update.");
            return;
        }

        for (Summary summary : summaries) {
            summary.setSummaryValue(summary.getSummaryValue() + 100);
        }

        summaryRepository.saveAll(summaries);

        log.info("{} summaries have been updated.", summaries.size());
    }
}

3-3. ExternalNotificationService

外部API呼び出しなどを模倣し、ログを出力するだけのシンプルな処理です。
src/main/java/com/example/springbatchh2crud/service/ExternalNotificationService.java

package com.example.springbatchh2crud.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class ExternalNotificationService {

    public void sendNotification() {
        log.info("Simulating notification to an external system.");
        // In a real scenario, this method would contain logic to call an external API,
        // send an email, or push a message to a queue.
        log.info("External system notification process completed.");
    }
}

4. 【ステップ2】:TaskletAdapterを使ったバッチジョブの定義

BatchConfig.java に、TaskletAdapter を使って上記サービスのメソッドを呼び出す Step と、それらをまとめる Job を定義します。ここがこのハンズオンの核心部分です。

MethodInvokingTaskletAdapter の設定は非常にシンプルです。

  • setTargetObject(Object): 呼び出したいメソッドを持っているBean(インスタンス)を指定します。
  • setTargetMethod(String): 呼び出したいメソッドの名前を文字列で指定します。

これだけで、Spring Batchは指定されたBeanのメソッドを実行する Tasklet を動的に生成してくれます。

src/main/java/com/example/springbatchh2crud/config/BatchConfig.java の追記・変更箇所

// ... (imports)
import com.example.springbatchh2crud.service.ExternalNotificationService;
import com.example.springbatchh2crud.service.SummaryService;
import com.example.springbatchh2crud.service.UserService;
import org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter;

// ...

@Configuration
public class BatchConfig {

    // ... (既存のフィールド)
    private final UserService userService;
    private final SummaryService summaryService;
    private final ExternalNotificationService externalNotificationService;

    // コンストラクタにサービスを追加
    public BatchConfig(
        // ... (既存の引数)
        UserService userService,
        SummaryService summaryService,
        ExternalNotificationService externalNotificationService
    ) {
        // ... (既存の代入)
        this.userService = userService;
        this.summaryService = summaryService;
        this.externalNotificationService = externalNotificationService;
    }

    // ... (既存のBean定義)

    // --- TaskletAdapter-based Job ---

    @Bean
    public MethodInvokingTaskletAdapter updateUserStatusTasklet() {
        MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
        // どのBeanの
        adapter.setTargetObject(userService); 
        // どのメソッドを呼び出すか
        adapter.setTargetMethod("updateUserStatus"); 
        return adapter;
    }

    @Bean
    public Step updateUserStatusStep() {
        return new StepBuilder("updateUserStatusStep", jobRepository)
            .tasklet(updateUserStatusTasklet(), transactionManager)
            .build();
    }

    @Bean
    public MethodInvokingTaskletAdapter updateSummaryTasklet() {
        MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
        adapter.setTargetObject(summaryService);
        adapter.setTargetMethod("updateSummary");
        return adapter;
    }

    @Bean
    public Step updateSummaryStep() {
        return new StepBuilder("updateSummaryStep", jobRepository)
            .tasklet(updateSummaryTasklet(), transactionManager)
            .build();
    }

    @Bean
    public MethodInvokingTaskletAdapter notifyExternalSystemTasklet() {
        MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
        adapter.setTargetObject(externalNotificationService);
        adapter.setTargetMethod("sendNotification"); // "notify" から変更
        return adapter;
    }

    @Bean
    public Step notifyExternalSystemStep() {
        return new StepBuilder("notifyExternalSystemStep", jobRepository)
            .tasklet(notifyExternalSystemTasklet(), transactionManager)
            .build();
    }

    @Bean
    public Job taskletAdapterJob(
        Step updateUserStatusStep,
        Step updateSummaryStep,
        Step notifyExternalSystemStep
    ) {
        return new JobBuilder("taskletAdapterJob", jobRepository)
            .start(updateUserStatusStep)
            .next(updateSummaryStep)
            .next(notifyExternalSystemStep)
            .build();
    }
}

【補足】メソッドに引数を渡す場合

もし呼び出したいメソッドに引数が必要な場合は、setArguments(Object[]) を使って引数の配列を渡すこともできます。

// 例:void updateStatusById(Long id, String status) を呼び出す場合
adapter.setTargetMethod("updateStatusById");
adapter.setArguments(new Object[]{123L, "active"}); 

5. 【ステップ3】:ジョブ実行用APIの作成

JobLaunchController.java に、新しく作成した taskletAdapterJob を起動するためのAPIエンドポイントを追加します。

src/main/java/com/example/springbatchh2crud/controller/JobLaunchController.java の追記・変更箇所

// ... (クラス定義)
public class JobLaunchController {

    // ... (既存フィールド)
    private final Job taskletAdapterJob;

    // コンストラクタにJobを追加
    public JobLaunchController(
        // ... (既存の引数)
        @Qualifier("taskletAdapterJob") Job taskletAdapterJob
    ) {
        // ... (既存の代入)
        this.taskletAdapterJob = taskletAdapterJob;
    }

    // ... (既存のエンドポイント)

    @GetMapping("/launch-tasklet-adapter-job")
    public ResponseEntity<String> launchTaskletAdapterJob() {
        // ... (実装は前のターンで提示したものと同じ)
    }
}

@Qualifier("taskletAdapterJob")BatchConfig で定義したジョブBeanをインジェクションし、それを起動するエンドポイントを追加しています。


6. 【ステップ4】:動作確認

すべての実装が完了したので、実際にアプリケーションを動かして確認してみましょう。

  1. アプリケーションの起動: ターミナルで以下のコマンドを実行します。 ./mvnw spring-boot:run
  2. H2コンソールの準備: ブラウザで http://localhost:8080/h2-console を開き、JDBC URL jdbc:h2:mem:testdb で接続します。SELECT * FROM USERS;SELECT * FROM SUMMARIES; を実行し、ジョブ実行前の初期状態を確認しておきます。
  3. ジョブの実行: ブラウザで http://localhost:8080/launch-tasklet-adapter-job にアクセスします。「ジョブ ‘taskletAdapterJob’ は正常に起動されました。」と表示されれば成功です。
  4. 実行ログの確認: ターミナルに3つのサービスが順次実行されたログが出力されることを確認します。以下のようなログが出力されていれば成功です。(期待されるログ出力を参照)
  5. DBの変更確認: 再度H2コンソールでテーブルを検索し、USERS テーブルの全レコードの STATUS カラムが 'inactive' になっていること、SUMMARIES テーブルの SUMMARY_VALUE100 加算されていることを確認します。

期待されるログ出力:

  // ジョブの開始
  INFO ... o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=taskletAdapterJob]] launched ...

  // ステップ1: updateUserStatusStep
  INFO ... o.s.batch.core.job.SimpleStepHandler     : Executing step: [updateUserStatusStep]
  INFO ... c.e.s.service.UserService                : Starting to update user statuses to 'inactive'.
  INFO ... c.e.s.service.UserService                : ... users have been updated to 'inactive' status.

  // ステップ2: updateSummaryStep
  INFO ... o.s.batch.core.job.SimpleStepHandler     : Executing step: [updateSummaryStep]
  INFO ... c.e.s.service.SummaryService             : Starting to update summary values.
  INFO ... c.e.s.service.SummaryService             : ... summaries have been updated.

  // ステップ3: notifyExternalSystemStep
  INFO ... o.s.batch.core.job.SimpleStepHandler     : Executing step: [notifyExternalSystemStep]
  INFO ... c.e.s.s.ExternalNotificationService      : Simulating notification to an external system.
  INFO ... c.e.s.s.ExternalNotificationService      : External system notification process completed.

  // ジョブの完了
  INFO ... o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=taskletAdapterJob]] completed ... status: [COMPLETED]

まとめ

TaskletAdapter を使うことで、既存のコード資産を有効活用し、新しい Tasklet クラスを実装する手間を省きながら、迅速にバッチ処理を構築できることがお分かりいただけたかと思います。

ロジックの再利用性を高め、テスト済みのコードを活用することで、より堅牢でメンテナンス性の高いバッチアプリケーションを開発できます。ぜひ、日々の開発で TaskletAdapter を活用してみてください。


免責事項

本記事は、Spring BatchのTaskletAdapterの利用方法に関する情報提供を目的としています。
記載されているコードや設定は、特定の環境や要件に完全に合致することを保証するものではありません。
本記事の内容を実践する際は、ご自身の責任において十分なテストと検証を行ってください。
本記事の情報を利用したことにより生じた、いかなる損害についても著者は一切の責任を負いません。


SNSでもご購読できます。

コメントを残す

*