はじめに
こんにちは。技術本部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
を選択した主な理由は以下の通りです。
- マイクロサービスアーキテクチャの採用: バッチの処理対象となるドメインではマイクロサービスアーキテクチャを採用しており、データ操作のためのAPIが既に存在していました。
- インフラ構成のシンプル化: データベース操作はマイクロサービスAPIの責務とし、バッチアプリケーション自体はデータベースを直接操作しない設計としました。これにより、バッチアプリケーションのインフラ構成をシンプルに保つことができました。
- Spring Batchのオーバースペック: 上記の理由から、バッチアプリケーションはデータベースを直接操作することがありません。そのため、Spring Batchは学習曲線が高く、機能的にもオーバースペックであり、導入するメリットが薄いと判断しました。
- 軽量性:
ApplicationRunner
を使用することで、必要最小限の機能を持つ軽量なバッチアプリケーションを構築できます。
これらの背景を踏まえ、ApplicationRunner
を活用した軽量バッチアプリケーションの構築方法を以下で詳しく解説します。
ApplicationRunner
とCommandLineRunner
の比較
Spring Bootには、アプリケーション起動時に処理を実行するためのインタフェースとしてApplicationRunner
とCommandLineRunner
が用意されています。以下に詳細な比較を示します。
特徴 | 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
- 非オプション引数:
file1
とfile2
本記事では、引数の扱いやすさから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サーバーを起動させないようにするためです。
この方法により、RestClient
やRestTemplate
などのWeb関連のユーティリティを使用しつつ、Webサーバーを起動せずにバッチ処理を実行できます。
バッチを起動するコマンド
バッチアプリケーションを起動する際のコマンドは以下のようになります。
java -jar app.jar --input=somedata
このコマンドを実行すると、BatchApplicationRunner
のrun
メソッドが呼び出され、指定したデータを使ってバッチ処理が実行されます。
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
アノテーションにおいて、TestConfig
とConfigDataApplicationContextInitializer
を指定します。- Spring Bootの
ApplicationContext
をカスタム設定で初期化し、バッチ処理の自動起動を防ぎます。
- Spring Bootの
ConfigDataApplicationContextInitializer
を使用することで、application.properties
やapplication.yml
の設定を読み込めます。@SpringBootTest
を利用した場合と同様の挙動です。
まとめ
ApplicationRunner
を利用することで、Spring Batchを使用せずに軽量で柔軟なバッチアプリケーションを構築できました。この方法は以下のような場合に適しています。
- シンプルなバッチ処理
- マイクロサービスアーキテクチャとの統合
- データベースを直接参照しない処理
しかしながら、上記の状況に当てはまらないケースにおいてはSpring Batchの導入を検討することも視野に入れてください。プロダクトの要件に応じて適切なアプローチを選択しましょう。
さいごに
ZOZOでは一緒にサービスを作り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。