概要
- Spring Batch で Tasklet を使用してジョブが再実行 (リスタート) する挙動を確認する
- 1度目は必ず失敗し、2度目以降は必ず成功するステップを用意する
- 失敗したステップからジョブが再実行 (リスタート) されることを確認する
動作確認環境
- Java 11 (AdoptOpenJDK 11.0.11)
- Spring Batch 4.3.3
- Spring Boot 2.5.3
- Spring Framework 5.3.9
- Gradle 7.1.1
- macOS Big Sur 11.4
ソースコード
ファイル一覧
├── build.gradle
└── src
└── main
├── java
│ └── com
│ └── example
│ └── steps
│ ├── MyApplication.java
│ ├── MyConfig.java
│ ├── MyController.java
│ └── MyGate.java
└── resources
└── application.properties
build.gradle
plugins {
id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
// Spring Batch
implementation 'org.springframework.boot:spring-boot-starter-batch'
// Spring Web MVC
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Batch のメタデータを入れるデータベース
runtimeOnly 'com.h2database:h2:1.4.200'
}
MyApplication.java
package com.example.steps;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// Spring Boot アプリケーションクラス
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
MyConfig.java
package com.example.steps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.step.tasklet.MethodInvokingTaskletAdapter;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// バッチ構成クラス
@Configuration // Bean 定義クラスであることを示すアノテーション
@EnableBatchProcessing // Spring Batch を有効にする
public class MyConfig {
private static final Logger log = LoggerFactory.getLogger(MyConfig.class);
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private MyGate firstGate = new MyGate("1番目の門");
private MyGate secondGate = new MyGate("2番目の門");
public MyConfig(JobBuilderFactory jobBuilderFactory,
StepBuilderFactory stepBuilderFactory) {
this.jobBuilderFactory = jobBuilderFactory;
this.stepBuilderFactory = stepBuilderFactory;
}
// ジョブ
@Bean
public Job myJob(@Qualifier("firstStep") Step firstStep, @Qualifier("secondStep") Step secondStep) {
log.info("myJob メソッドを実行");
return jobBuilderFactory.get("myJob") // 一意となる任意のジョブ名を指定
.start(firstStep) // 最初に実行するステップを指定
.next(secondStep) // 次に実行するステップを指定
.build();
}
// ひとつめに実行するステップ
@Bean("firstStep")
public Step firstStep(@Qualifier("firstTasklet") Tasklet firstTasklet) {
log.info("firstStep メソッドを実行");
return stepBuilderFactory.get("firstStep") // 任意のステップ名を指定
.tasklet(firstTasklet) // 実行するタスクレットを指定
.build();
}
// ひとつめのステップで実行するタスクレット
@Bean("firstTasklet")
@StepScope // Step の実行ごとに新たなインスタンスを生成する
public Tasklet firstTasklet(@Value("#{jobParameters['challenger']}") String challenger) {
log.info("firstTasklet メソッドを実行");
MethodInvokingTaskletAdapter tasklet = new MethodInvokingTaskletAdapter();
tasklet.setTargetObject(firstGate); // 実行対象のオブジェクト
tasklet.setTargetMethod("challenge"); // 実行するメソッド名
tasklet.setArguments(new Object[]{challenger}); // 実行するメソッドに渡すパラメータ
return tasklet;
}
// ふたつめに実行するステップ
@Bean("secondStep")
public Step secondStep(@Qualifier("secondTasklet") Tasklet secondTasklet) {
log.info("secondStep メソッドを実行");
return stepBuilderFactory.get("secondStep") // 任意のステップ名を指定
.tasklet(secondTasklet) // 実行するタスクレットを指定
.build();
}
// ふたつめのステップで実行するタスクレット
@Bean("secondTasklet")
@StepScope // Step の実行ごとに新たなインスタンスを生成する
public Tasklet secondTasklet(@Value("#{jobParameters['challenger']}") String challenger) {
log.info("secondTasklet メソッドを実行");
MethodInvokingTaskletAdapter tasklet = new MethodInvokingTaskletAdapter();
tasklet.setTargetObject(secondGate); // 実行対象のオブジェクト
tasklet.setTargetMethod("challenge"); // 実行するメソッド名
tasklet.setArguments(new Object[]{challenger}); // 実行するメソッドに渡すパラメータ
return tasklet;
}
}
MyController.java
package com.example.steps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionException;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* ジョブを実行するための WebAPI を提供する。
*/
@RestController
public class MyController {
private static final Logger log = LoggerFactory.getLogger(MyController.class);
private final JobLauncher jobLauncher;
private final Job myJob;
public MyController(JobLauncher jobLauncher, Job myJob) {
this.jobLauncher = jobLauncher;
this.myJob = myJob;
}
@GetMapping("/execute/{challenger}")
public Map execute(@PathVariable("challenger") String challenger) {
try {
log.info("===== ジョブを実行 =====");
jobLauncher.run(myJob, new JobParametersBuilder()
.addString("challenger", challenger)
.toJobParameters());
return Map.of("execute", "ok");
} catch (JobExecutionException e) {
log.info(e.toString());
return Map.of("execute", "error");
}
}
}
MyGate.java
package com.example.steps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashSet;
import java.util.Set;
/**
* 挑戦者が通る門。
*/
public class MyGate {
private static final Logger log = LoggerFactory.getLogger(MyGate.class);
private final String gateName;
private final Set<String> recordBook = new HashSet<>();
/**
* 挑戦者が通る門を生成する。
* @param name 門の名前
*/
public MyGate(String name) {
this.gateName = name;
}
/**
* 一度目は必ず失敗する。二度目以降は必ず成功する。
* @param challenger 挑戦者
*/
public void challenge(String challenger) {
if (recordBook.contains(challenger)) { // 挑戦者はすでに来ていたか
log.info(String.format("成功: %s は %s を突破した", challenger, gateName));
} else {
log.info(String.format("失敗: %s は %s に阻まれた", challenger, gateName));
recordBook.add(challenger); // 挑戦者の名を記録する
throw new IllegalStateException("失敗");
}
}
}
application.properties
# 起動時にジョブを実行しない設定
spring.batch.job.enabled=false
# ログ出力設定
logging.level.root=INFO
logging.level.org.springframework.batch.core.step.AbstractStep=OFF
logging.pattern.console=%-20.20logger{0} %message%n
挙動を確認する
Spring Boot を起動する
$ gradle bootrun
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.3)
(中略)
MyConfig firstStep メソッドを実行
MyConfig secondStep メソッドを実行
MyConfig myJob メソッドを実行
ジョブの実行と出力ログの確認
curl コマンドでジョブ実行のためのエンドポイントをコールし、Spring Boot + Spring Batch のログを確認する。
1度目の実行。
$ curl http://localhost:8080/execute/Alice
{"execute":"ok"}
ログを確認。ジョブが実行されて、1つめのステップが失敗する。
MyController ===== ジョブを実行 =====
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{challenger=Alice}]
SimpleStepHandler Executing step: [firstStep]
MyConfig firstTasklet メソッドを実行
MyGate 失敗: Alice は 1番目の門 に阻まれた
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{challenger=Alice}] and the following status: [FAILED] in 80ms
2度目の実行。同じパラメータを指定してジョブを実行することで、失敗した箇所からジョブを再実行 (リスタート) することができる。
$ curl http://localhost:8080/execute/Alice
{"execute":"ok"}
ログを確認。ジョブが実行されて、1つめのステップが成功し、2つめのステップが失敗する。
MyController ===== ジョブを実行 =====
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{challenger=Alice}]
SimpleStepHandler Executing step: [firstStep]
MyConfig firstTasklet メソッドを実行
MyGate 成功: Alice は 1番目の門 を突破した
SimpleStepHandler Executing step: [secondStep]
MyConfig secondTasklet メソッドを実行
MyGate 失敗: Alice は 2番目の門 に阻まれた
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{challenger=Alice}] and the following status: [FAILED] in 34ms
3度目の実行。
$ curl http://localhost:8080/execute/Alice
{"execute":"ok"}
ログを確認。1つめのステップはすでに成功しているので何もしない。2つめのステップが成功する。ジョブが正常に完了する。
MyController ===== ジョブを実行 =====
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{challenger=Alice}]
SimpleStepHandler Step already complete or not restartable, so no action to execute: StepExecution: id=2, version=3, name=firstStep, status=COMPLETED, exitStatus=COMPLETED, readCount=0, filterCount=0, writeCount=0 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=1, rollbackCount=0, exitDescription=
SimpleStepHandler Executing step: [secondStep]
MyConfig secondTasklet メソッドを実行
MyGate 成功: Alice は 2番目の門 を突破した
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{challenger=Alice}] and the following status: [COMPLETED] in 14ms
4度目の実行。
$ curl http://localhost:8080/execute/Alice
{"execute":"error"}
ログを確認。すでに正常に完了したジョブを再実行しようとして JobInstanceAlreadyCompleteException 例外が発生している。
MyController ===== ジョブを実行 =====
MyController org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={challenger=Alice}. If you want to run this job again, change the parameters.
パラメータを変更すれば別のジョブとして実行できる。
$ curl http://localhost:8080/execute/Bob
{"execute":"ok"}
MyController ===== ジョブを実行 =====
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{challenger=Bob}]
SimpleStepHandler Executing step: [firstStep]
MyConfig firstTasklet メソッドを実行
MyGate 失敗: Bob は 1番目の門 に阻まれた
SimpleJobLauncher Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{challenger=Bob}] and the following status: [FAILED] in 13ms