ApplicationRunnerを活用した軽量バッチアプリケーションの構築

ApplicationRunnerを活用した軽量バッチアプリケーションの構築

はじめに

こんにちは。技術本部ECプラットフォーム部マイグレーションブロックの小原です。

本記事では、Spring BootのApplicationRunnerインタフェースを活用したバッチアプリケーション(CLIアプリケーション)の構築方法について解説します。
バッチ処理の実装において、SpringフレームワークはSpring Batchという強力なツールを提供しています。しかし、比較的単純なバッチ処理の場合、Spring Batchの使用はオーバーエンジニアリングとなる可能性があります。
そこで、軽量なアプローチとしてApplicationRunnerを利用した実装方法を説明します。この方法は、シンプルなバッチ処理に適しており、Spring Bootの機能を活用しつつ、必要最小限の実装で効率的なバッチアプリケーションを構築できます。

なお、本記事は下記の環境にて検証しました。

  • Java 21 (Eclipse Temurin)
  • Spring Boot 3.3.1

目次

ApplicationRunnerを選択した背景

今回のバッチアプリケーションにてSpring BatchではなくApplicationRunnerを選択した主な理由は以下の通りです。

  1. マイクロサービスアーキテクチャの採用: バッチの処理対象となるドメインではマイクロサービスアーキテクチャを採用しており、データ操作のためのAPIが既に存在していました。
  2. インフラ構成のシンプル化: データベース操作はマイクロサービスAPIの責務とし、バッチアプリケーション自体はデータベースを直接操作しない設計としました。これにより、バッチアプリケーションのインフラ構成をシンプルに保つことができました。
  3. Spring Batchのオーバースペック: 上記の理由から、バッチアプリケーションはデータベースを直接操作することがありません。そのため、Spring Batchは学習曲線が高く、機能的にもオーバースペックであり、導入するメリットが薄いと判断しました。
  4. 軽量性: ApplicationRunnerを使用することで、必要最小限の機能を持つ軽量なバッチアプリケーションを構築できます。

これらの背景を踏まえ、ApplicationRunnerを活用した軽量バッチアプリケーションの構築方法を以下で詳しく解説します。

ApplicationRunnerCommandLineRunnerの比較

Spring Bootには、アプリケーション起動時に処理を実行するためのインタフェースとしてApplicationRunnerCommandLineRunnerが用意されています。以下に詳細な比較を示します。

特徴 ApplicationRunner CommandLineRunner
メソッド run(ApplicationArguments args) run(String... args)
引数の処理 Spring Bootが解析済みの引数を提供 独自に引数を解析する必要がある
オプション引数の扱い --key=value形式を簡単に扱える 独自でパースが必要
非オプション引数の扱い getNonOptionArgs()で取得可能 配列の要素として直接アクセス
実行順序の制御 @Orderアノテーションで制御可能 @Orderアノテーションで制御可能

ここで、オプション引数とは--key=valueの形式で指定される引数を指し、非オプション引数とはそれ以外の単純な値として渡される引数を指します。
例えば、java -jar app.jar --input=data file1 file2というコマンドでは、以下の通りとなります。

  • オプション引数: --input=data
  • 非オプション引数: file1file2

本記事では、引数の扱いやすさからApplicationRunnerを選択しています。

ApplicationRunnerおよびCommandLineRunnerの詳細は、以下のJavadocを参照してください。

ApplicationRunnerを利用したバッチアプリケーションの実装

以下に、ApplicationRunnerを利用したバッチアプリケーションの実装例を示します。

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class BatchApplicationRunner implements ApplicationRunner {

    private final RestClient restClient;

    public BatchApplicationRunner(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.baseUrl("http://microservice/api").build();
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // コマンドライン引数から特定のキーの値を取得
        String inputData = args.getOptionValues("input").get(0);
        System.out.println("Input data: " + inputData);
        
        // マイクロサービスのAPIを呼び出してデータ操作を行う
        String response = restClient.get()
                                    .uri("/data?input=" + inputData)
                                    .retrieve()
                                    .body(String.class);
        System.out.println("API Response: " + response);
        
        // バッチ処理のロジックを実装
        performBatchProcessing(response);
    }

    private void performBatchProcessing(String data) {
        // バッチ処理の実装
        System.out.println("Processing data: " + data);
    }
}

エントリポイントのコード

バッチアプリケーションのエントリポイントとなるクラスを、依存関係にspring-boot-starter-webを含まない場合と含む場合で分けて示します。

依存関係にspring-boot-starter-webを含まない場合

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

依存関係にspring-boot-starter-webを含む場合

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(BatchApplication.class)
            .web(WebApplicationType.NONE)
            .run(args);
    }
}

上記のコードでは、SpringApplicationBuilderを使用して明示的にWebアプリケーションタイプをNONEに設定しています。これは、spring-boot-starter-webが依存関係に含まれていても、Webサーバーを起動させないようにするためです。

この方法により、RestClientRestTemplateなどのWeb関連のユーティリティを使用しつつ、Webサーバーを起動せずにバッチ処理を実行できます。

バッチを起動するコマンド

バッチアプリケーションを起動する際のコマンドは以下のようになります。

java -jar app.jar --input=somedata

このコマンドを実行すると、BatchApplicationRunnerrunメソッドが呼び出され、指定したデータを使ってバッチ処理が実行されます。

ExitCodeGeneratorを利用した終了コードの制御

バッチアプリケーションが任意の終了コードを返すために、ExitCodeGeneratorインタフェースを実行できます。以下に例を示します。

import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        System.exit(SpringApplication.exit(SpringApplication.run(BatchApplication.class, args)));
    }

    @Bean
    public ExitCodeGenerator exitCodeGenerator() {
        return () -> {
            // ここで適切な終了コードを返す
            // 例: 0は成功、1は一般的なエラー、2は特定のエラーなど
            return 0;
        };
    }
}

この例では、ExitCodeGeneratorを実装したBeanを定義しています。exitCodeGeneratorメソッド内で、バッチ処理の結果に応じて適切な終了コードを返すロジックを実装できます。

テスト実装

ApplicationRunnerを利用したバッチアプリケーションのテストには、主に2つのアプローチがあります。それぞれの特徴と使用方法を説明します。

@SpringBootTestを利用する方法

@SpringBootTestアノテーションを使用すると、@SpringBootApplicationアノテーションが付与されたmainメソッドのエントリポイントが実行されます。

@SpringBootTestを利用して、mainに引数を渡す場合のサンプルコードは次のとおりです。なお、テストコードはSpockを利用していますが、JUnitにおいても同様です。

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification

@SpringBootTest(args = ["--input=somedata"])
class BatchApplicationSpec extends Specification {

    @Autowired
    ApplicationArguments applicationArguments

    def "バッチ処理のテスト"() {
        expect:
        // 期待される結果を検証
        applicationArguments.getOptionValues("input") == ["somedata"]
    }
}

このアプローチの主なポイントは以下の通りです。

  • @SpringBootTestアノテーションのargsパラメータを使用して、コマンドライン引数をテストに渡すことができます。上記の例では、--input=somedataという引数を指定しています。
  • ApplicationArgumentsインタフェースを@Autowiredでインジェクションすることで、テストメソッド内で渡されたコマンドライン引数を取得し検証できます。
  • applicationArguments.getOptionValues("input")を使用して、特定のオプション引数の値を取得できます。この方法は、ApplicationRunnerの実装でコマンドライン引数を処理する方法と同じです。
  • アプリケーション全体のコンテキストが起動するため、実際の動作環境に近い状態でテストできます。
  • ただし、テスト実行時にバッチ処理が自動的に起動してしまうため、この挙動が問題となる場合は@ContextConfigurationを利用することで解消できます。

@ContextConfigurationを利用する方法

@SpringBootTestを使用すると、@SpringBootApplicationのエントリポイントが実行されてバッチ処理が自動的に起動してしまいます。
@ContextConfigurationアノテーションと適切なコンポーネントスキャンによりバッチ処理の自動起動を防ぎ、より細かいテスト制御が可能になります。

import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer
import org.springframework.test.context.ContextConfiguration
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.ComponentScan
import spock.lang.Specification

@ContextConfiguration(
    classes = [TestConfig],
    initializers = [ConfigDataApplicationContextInitializer]
)
class BatchApplicationSpec extends Specification {

    @TestConfiguration
    @ComponentScan(basePackages = ["com.example.batch"], 
                   excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, 
                                    classes = [BatchApplication]))
    static class TestConfig {
        // 必要であればテストに必要な設定を追加する
    }

    def "バッチ処理のテスト"() {
        expect:
        // 期待される結果を検証
    }
}

このアプローチの主なポイントは以下の通りです。

  • TestConfigクラスで@ComponentScanアノテーションを使用し、バッチアプリケーションのコンポーネントをスキャンします。ただし、バッチのエントリーポイント(BatchApplication)を除外します。
  • @ContextConfigurationアノテーションにおいて、TestConfigConfigDataApplicationContextInitializerを指定します。
    • Spring BootのApplicationContextをカスタム設定で初期化し、バッチ処理の自動起動を防ぎます。
  • ConfigDataApplicationContextInitializerを使用することで、application.propertiesapplication.ymlの設定を読み込めます。
    • @SpringBootTestを利用した場合と同様の挙動です。

まとめ

ApplicationRunnerを利用することで、Spring Batchを使用せずに軽量で柔軟なバッチアプリケーションを構築できました。この方法は以下のような場合に適しています。

  • シンプルなバッチ処理
  • マイクロサービスアーキテクチャとの統合
  • データベースを直接参照しない処理

しかしながら、上記の状況に当てはまらないケースにおいてはSpring Batchの導入を検討することも視野に入れてください。プロダクトの要件に応じて適切なアプローチを選択しましょう。

さいごに

ZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com

カテゴリー