1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Spring Batch で Tasklet を使用してジョブがリスタートする挙動を確認する

Last updated at Posted at 2021-08-15

概要

  • 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

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?