
はじめに
こんにちは。技術本部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では一緒にサービスを作り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。