Spring Batch の基本的な使い方とCommandLineRunner による CLI 実行

Java
JavaSpringバックエンド

この記事では Spring Batch の基本的な使用方法をサンプルコードを交えて説明します。

Spring Boot に関しては別記事で例外処理の動きをまとめたりしていますが、Spring Batch も同じく記事は色々あるのですが、結局どうすればよいのかがいまいち分からず色々試行錯誤しました。
同じような方の助けになれば幸いです。

今回は Tasklet を使ったパターンで、以下までやります。

  1. シンプルな Spring Batch 実行
  2. ジョブ / ステップの前後に処理を挟む
  3. コマンドラインからの実行


なお、そもそも Spring Batchとは?については、公式含め多くの記事があるのでここでは割愛します。

環境

  • Spring Boot 2.7.5
  • Java 17

準備

[準備]環境

毎度のこと、Spring Initializr を利用します。
Spring Batch が利用する DB は今回は簡易のため h2 にしています。

https://start.spring.io/
https://start.spring.io/

[準備]DBの設定 – application.properties

h2 を利用する場合の設定になります。
jdbc:h2:file:./target/h2sampledb は、プロジェクト直下からのパスになり、実行すると h2sampledb.mv.db というファイルが出来上がります。

Spring Batch は実行管理のために複数のテーブルが必要になりますが、自動で生成してくれます。テーブルのDDLも Spring Batch が用意してくれていてこちらにあります。
application.properties は以下としました。

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./target/h2sampledb
spring.datasource.username=<user>
spring.datasource.password=<password>

# spring.batch.jdbc.schema=classpath:/org/springframework/batch/core/schema-h2.sql
spring.batch.jdbc.initialize-schema=always

シンプルな Spring Batch 実行

Job / Step の作成

CLI から実行する前に、Spring Batch を実行する例を作ります。必要なのは以下です。

  1. Job の定義
  2. Step の定義
  3. Tasklet の定義

Job は JobBuilderFactory, Step は StepBuilderFactory で作ることができます。

// @Configuration: 内部で @Bean を利用するため
// @EnableBatchProcessing: JobBuilderFactory, StepBuilderFactory を利用するため
// @RequiredArgsConstructor: 上記を DI するため
@Configuration
@EnableBatchProcessing
@RequiredArgsConstructor
public class BatchConfig {

    private final JobBuilderFactory jobBuilderFactory;

    private final StepBuilderFactory stepBuilderFactory;

    private final Tasklet tasklet1;

    /**
     * Step を作成、helloStep1 という名前のステップで、
     * 実際の処理は tasklet1 を行う
     */
    @Bean
    public Step step1() {
        return stepBuilderFactory.get("helloStep1")
                .tasklet(tasklet1)
                .build();
    }

    /**
     * Job の作成、helloJob というジョブ
     * 実行毎に実行IDをインクリメントすることでパラメータ重複しないようにする
     * 実際の処理は step1 を行う
     */
    @Bean
    public Job job() {
        return jobBuilderFactory.get("helloJob")
                .incrementer(new RunIdIncrementer())
                .start(step1())
                .build();
    }
}

Task の作成

上記で、tasklet1 としていたタスクです。
今回は Tasklet を1つだけ作成したので、上記の実装の Tasklet の DI に以下のタスクが注入されます。

/**
 * @Component: Bean として登録するため
 * @StepScope: Stepごとにスコープを定義。
 *      StepScope インタフェースは次のアノテーションを持ちます。
 *      @Scope(value = "step", proxyMode = ScopedProxyMode.TARGET_CLASS)
 */
@Component
@StepScope
public class BatchTasklet1 implements Tasklet {

    /**
     * job の実行時に指定されたパラメータがセットされます。
     * param1=テスト1 param2=202 param3=テスト3 の場合、「テスト1」です。
     */
    @Value("#{jobParameters[param1]}")
    private String param1;

    /**
     * job の実行時に指定されたパラメータがセットされます。
     * param1=テスト1 param2=202 param3=テスト3 の場合、「202」です。
     */
    @Value("#{jobParameters[param2]}")
    private int param2;

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext context) {
        System.out.println("Batch Task1 is called !");
        System.out.println("param1: " + param1);
        System.out.println("param2: " + param2);

        contribution.setExitStatus(new ExitStatus("success"));

        return RepeatStatus.FINISHED;
    }
}

実行

最低限の実装としてこれでOKです。
Intellij を利用して、「param1=テスト1 param2=202 param3=テスト3」をパラメータとして実行します。

実行結果 - シンプルな Spring Batch
実行結果 – シンプルな Spring Batch

helloJob という名前のジョブが実行され、helloStep1 というステップが実行されています。
また、作成した Tasklet で記載した内容とパラメータで渡した内容が出力できています。

ジョブ / ステップの前後に処理を挟む

JobExecutionListenerSupport / StepExecutionListenerSupport

ジョブの前後に処理を挟む場合は JobExecutionListenerSupport を利用します。
ステップの場合は、StepExecutionListenerSupport を利用します。

とても簡単ですね。

@Component
public class BatchJobListener extends JobExecutionListenerSupport {

    @Override
    public void beforeJob(JobExecution jobExecution) {
        System.out.println("beforeJob");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        System.out.println("aftefJob");
    }
}

ステップの場合も同じです。違いが1つあり、afterStep では ExitStatus を返却する必要があるようです。

@Component
public class BatchStepListener extends StepExecutionListenerSupport {

    @Override
    public void beforeStep(StepExecution stepExecution) {
        System.out.println("beforeStep");
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        System.out.println("aftefStep");
        return stepExecution.getExitStatus();
    }
}

使い方は、Listener を実行したいジョブ、ステップに差し込むだけです。

~~
    @Bean
    public Step step1() {
        return stepBuilderFactory.get("helloStep1")
                .tasklet(tasklet1)
                .listener(stepListener)
                .build();
    }

~~
    @Bean
    public Job job() {
        return jobBuilderFactory.get("helloJob")
                .incrementer(new RunIdIncrementer())
                .start(step1())
                .listener(jobListener)
                .build();
    }

実行

ちゃんと順番にログが出てますね。

実行結果 - ジョブ / ステップの前後に処理を挟む
実行結果 – ジョブ / ステップの前後に処理を挟む

コマンドラインからの実行

CommandLineRunner

コマンドラインから実行させる場合、CommandLineRunner を使用します。
似たクラスとして ApplicationRunner もありますが、違いは引数のとり方くらいとのことでした。

CommandLineJobRunner というクラスがあり、こちらを利用する方法もあります。
が、これは実行時にパラメータとして jobPath を指定するのですが、xml ベースでの定義を指定する必要があるようです。
java ファイルで完結させたかったので、今回は CommandLIneRunner にしています。

run メソッドの中ではパラメータを = 区切りで区切ってジョブのパラメータとしています(この辺の実装は Spring Boot の JobRancherApplicationRunner に書かれている実装を参考にしました。

また、@ConditionalOnProperty でこの処理を実行するための条件を設定します。今回の例では、--batch=my-batch が指定されると実行されます。

@Component
@EnableBatchProcessing
@RequiredArgsConstructor
@ConditionalOnProperty(value = { "batch" }, havingValue = "my-batch")
public class MyBatchCommandLineRunner  implements CommandLineRunner {

    private final JobLauncher jobLauncher;

    private final Job job;

    private DefaultJobParametersConverter converter = new DefaultJobParametersConverter();

    @Override
    public void run(String... args) throws Exception {
        System.out.println("run my-batch");
        Properties properties = StringUtils.splitArrayElementsIntoProperties(args, "=");
        JobParameters jobParameters = this.converter.getJobParameters(properties);
        // パラメータ
        System.out.println(jobParameters.toString());

        // ジョブ実行
        JobExecution jobExecution = jobLauncher.run(job, jobParameters);

        // 終了ステータス
        System.out.println("my-batch status: " + jobExecution.getExitStatus().getExitCode());
    }
}

また、これまではジョブが1つだったので意識していませんでしたが、デフォルトでは起動すると登録されているすべてのジョブが実行されます。今回から、指定したジョブだけ実行されるように設定を変更します。といっても、application.properties に1行足すだけです。

spring.batch.job.enabled=false

実行

以下のパラメータで実行します。

--batch=my-batch param1=テスト1 param2=202 param3=テスト3

ジョブの実行前後に、run で出力しているログが見えています。
また、パラメータも問題なく取得できています。

実行結果 - コマンドラインからの実行
実行結果 – コマンドラインからの実行

ちなみに、--batch=my-batch を指定しない場合(例えば --batch=my-batch2 をパラメータに指定した場合)は、実行してもジョブは実行されません。

注意: 同じパラメータでの実行は1回です

ここでは --batch=my-batch param1=テスト1 param2=202 param3=テスト3 というパラメータを指定して実行しましたが、同じパラメータで実行しようとすると2回目はエラーになります。これは Spring Batch の仕様です。

ジョブの定義で、.incrementer(new RunIdIncrementer()) としている部分がありましたが、ここで run.id というパラメータがなければ 1 , 次は 2 と増えてパラメータとして渡るようになっていました。が、CommanLineRunner で jobLauncher.run をすると、run.id は自動的にパラメータとして渡されません。

以下のように、明示的にパラメータに run.id を渡すことで、次の新たな実行をしてくれるようにはできます。

//MyBatchCommandLineRunner.java - run メソッド

JobParameters jobParameters = this.converter.getJobParameters(properties);
// これを設定する
// その後、実行パラメータに run.id=1 をつけると、今度は run.id=2 で実行してくれます
jobParameters = job.getJobParametersIncrementer().getNext(jobParameters);

もっとよいやり方がありそうな気もしますが、今の理解としては、

  • 同じ引数での定期実行であれば、ジョブ定義の .incrementer(new RunIdIncrementer()) で十分
  • CLI から実行する場合は、パラメータが毎回可変になるような値を渡す(run.id を渡して上のように run.id をインクリメントさせてもよいですが、実行時の日付情報を渡すなどが多いのかなと思いました。日時実行する場合は、 yyyymmdd_n 形式でパラメータを渡すと、同じ yyyymmdd での実行は基本実行されず、もし再実行したい場合は n をインクリメントしてコマンドを実行させるような感じです。

まとめ

調べてみて、実際にはもっといろんな書き方や高度な設定ができそうです。
ただ、シンプルにあるバッチ処理を実行したい、と言ったケースにおいては、今回の範囲で十分使えそうに思います(変わるのは メイン処理の Tasklet くらいで、それ以外は定型でいけそう)。その上で構造化された形で処理がかけるのがいいですね。

あと把握しておきたい動きは以下あたりです。これらもいつかためそうと思います。

  • 任意の戻り値を返す
  • 並列処理と待ち合わせ
  • リラン / リトライ
  • ログの出力

以下は数少ない Spring Batch に関して比較的わかりやすく説明された書籍です。

コメント