この記事では Spring Batch の基本的な使用方法をサンプルコードを交えて説明します。
Spring Boot に関しては別記事で例外処理の動きをまとめたりしていますが、Spring Batch も同じく記事は色々あるのですが、結局どうすればよいのかがいまいち分からず色々試行錯誤しました。
同じような方の助けになれば幸いです。
今回は Tasklet を使ったパターンで、以下までやります。
- シンプルな Spring Batch 実行
- ジョブ / ステップの前後に処理を挟む
- コマンドラインからの実行
なお、そもそも Spring Batchとは?については、公式含め多くの記事があるのでここでは割愛します。
環境
- Spring Boot 2.7.5
- Java 17
準備
[準備]環境
毎度のこと、Spring Initializr を利用します。
Spring Batch が利用する DB は今回は簡易のため h2 にしています。
[準備]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 を実行する例を作ります。必要なのは以下です。
- Job の定義
- Step の定義
- 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」をパラメータとして実行します。
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 に関して比較的わかりやすく説明された書籍です。
コメント