まずは実践、Spring Boot Batchの動かし方

# ※注意（2023/05/13追記）
Spring Boot 3.0がリリースされSpring Batchも5.0にアップデートされました。
最新の情報は公式ドキュメントを参照するようにしてください。

# はじめに
[Spring Batch](https://docs.spring.io/spring-batch/docs/current/reference/html/index.html)とは、バッチアプリケーションをSpring上に構築できるフレームワークです。Spring Batchを勉強していくと登場人物が多く、概念が少し複雑です。これらを学ぶのはもちろん大事ですが、まずは動かしてみたい・・！そこで、本記事はSpring BatchをSpring Bootで動かすためにすべきことに焦点を当てます。
概念の解説は最小限とし、理論より実践、Spring Batchをどのように動かすかまずは雰囲気を感じ取ることを目標にしたいと思います。

# 本記事のゴール

- Spring BootでSpring Batchをどのように書けばいいか分かる
- Spring Batchの全体像がなんとなく分かる
- 簡単なJob, Stepを記述できる（今回はTaskletモデルを紹介）
- 必要な最低限のライブラリ、Java Configの記述が分かる

# Spring Batchの概念
最低限頭に入れておくべき概念を紹介します。
詳細は[公式リファレンス](https://docs.spring.io/spring-batch/docs/current/reference/html/index.html)を参照してください。

## 全体像
Spring Batchの全体アーキテクチャです。これだけでも頭に入れておくと良いです。
ここでは以下のワードについて解説します。

- JobLauncher
- Job
- JobParameter
- Step
- JobRepository

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89007/1e238fe4-46f5-445e-8a8c-9778074be370.png)


## JobLaucher
Jobを実行するエントリーポイントとなるインタフェースです。Jobの実行はrunメソッドを呼び出すことで行われます。runメソッドは実行するJobとJobParameterを受け取ります。

```java
public interface JobLauncher {

public JobExecution run(Job job, JobParameters jobParameters)
            throws JobExecutionAlreadyRunningException, JobRestartException,
                   JobInstanceAlreadyCompleteException, JobParametersInvalidException;
}
```

## Job
Spring Batchで一番大本にある要素でバッチ処理全体を表します。1Job = 1バッチ処理と考えて問題ないと思います。Jobは複数のStepから構成され、1つのJobを実行するとそれを構成する複数のStepが実行されます。

## JobParameter
Job実行時に渡すパラメータ。ここで渡したパラメータをJobやStepの変数として利用できます。

## Step
バッチ処理を構成する最小単位の要素。Stepは大きく2種類のモデルに分類されます。

- Chunkモデル
    - 読み込み(ItemReader)、加工(ItemProcessor)、書き込み(ItemWriter)によって構成されるステップモデル。読み込み、加工、書き込みの順でStepが構成され、必ずそれぞれの処理が実行されます（例えば読み込み、加工だけを行うようなStepにはできません）。実装者はそれぞれの処理内容を記述する必要があります。予めSpring Batch側でインタフェースが提供されているので、実装者はそれを実装してそれぞれの処理内容を書いていきます。
- Taskletモデル
    - Chunkモデルとは違い、特に処理の流れが決まっておらず、自由に処理を記述できるステップモデル。何か単一処理を行う場合はこちらを利用します。こちらもSpring Batch側でインタフェースが提供されているので、実装者はインタフェースを実装して処理内容を書いていきます。

## JobRepository
JobやStepの実行状況や実行結果を保存しておくところ。一般的にRDBなどのストレージで永続化させます。

# 本記事で作成するバッチアプリケーションの内容
今回はSpring Bootを起動をすると「Hello, World!」を表示するという単純なバッチアプリケーションを作成します。
ソースコードは[GitHub](https://github.com/kawakawaryuryu/sample-spring-batch)をご覧ください。

# 前提
今回紹介するコードは以下のバージョンで動作確認済みです。

|ライブラリ|バージョン|
|:-:|:-:|
|Spring Boot|2.2.5.RELEASE|
|Java|11|
|maven|3.5.3|

また、Spring Batchにおける設定はXMLではなくJava Configで記載します。

# 実際に動かしてみる
ここからはSpring BootでSpring Batchを動かすためにすべきことを記載していきます。

## ライブラリの導入
最低限以下のライブラリを導入します。

|ライブラリ|内容|
|:-:|:-:|
|spring-boot-starter-batch|Spring BatchをSpring Bootで利用するためのstarterライブラリ|
|h2|組み込みDB<br>JobRepositoryを格納するために必要|

```xml:pom.xml
...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        ...
    </dependencies>
...
```

## Taskletの実装
まずStepの実処理となるTaskletを実装していきます。（今回は書き込み処理がないため、Taskletモデルを採用しています）
以下は「Hello, World!」を表示するTaskletです。
Taskletインタフェースを実装してexecuteメソッドを記述します。
executeメソッドは返り値RepeatStatus型を返す必要があります。
RepeatStatusはEnum型であり、`RepeatStatus.FINISHED`と`RepeatStatus.CONTINUABLE`の2値を持っています。
返す値によって挙動が異なるので注意してください。

|RepeatStatus|返り値にした場合の挙動|
|:-:|:-:|
|FINISHED|それ以上処理が続かず終了する。<br>nullを返した場合もFINISHEDと同様の挙動になる。|
|CONTINUABLE|Taskletが継続して呼ばれる。<br>FINISHEDを返さない限りは処理が継続するので注意が必要。|

```java:HelloWorldTasklet.java
@Component
@StepScope
public class HelloWorldTasklet implements Tasklet {
    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        System.out.println("Hello, World!");
        return RepeatStatus.FINISHED;
    }
}
```

## JobやStepの定義
Java Configでは主にJobやStepの設定をBean定義として記述していきます。
本来はJobRepositoryやJobLauncherなどのBean定義も必要ですが、Spring Batchには`@EnableBatchProcessing`というアノテーションがあり、これを付与すると明示的に定義せずとも自動で設定してくれます。

```java:BatchConfig.java
@EnableBatchProcessing
@Configuration
public class BatchConfig {

    private final JobBuilderFactory jobBuilderFactory;

    private final StepBuilderFactory stepBuilderFactory;

    private final HelloWorldTasklet helloWorldTasklet;

    public BatchConfig(JobBuilderFactory jobBuilderFactory,
                       StepBuilderFactory stepBuilderFactory,
                       HelloWorldTasklet helloWorldTasklet) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.helloWorldTasklet = helloWorldTasklet;
    }

    @Bean
    public Job helloWorldJob(Step helloWorldStep) {
        return jobBuilderFactory.get("helloWorldJob") //Job名を指定
                .flow(helloWorldStep) //実行するStepを指定
                .end()
                .build();
    }

    @Bean
    public Step helloWorldStep() {
        return stepBuilderFactory.get("helloWorldStep") //Step名を指定
                .tasklet(helloWorldTasklet) //実行するTaskletを指定
                .build();
    }
}
```

上記コードを少し解説します。
まず、Jobについてです。`jobBuilderFactory.get()`メソッドの引数にはJob名を指定しています。ここでは「helloWorldJob」という名前を指定しています。次に`.flow()`メソッドでこのJobで実行するStepを指定しています。

次にStepですが、Jobと構成は似ており、まず`stepBuilderFactory.get()`メソッドの引数にStep名を指定しています。ここでは「helloWorldStep」という名前を指定しています。そして`.tasklet()`メソッドで実行するTaskletを指定しています。

## DataSourceの設定
今回はJobRepositoryの保存先としてH2を使用するため、そのためのDataSource設定を記述します。

```yml:application.yml
spring:
  datasource:
    url: "jdbc:h2:mem:test"
    username: sa
    password:
    driver-class-name: org.h2.Driver
```

## SpringBootApplicationの実装
これは一般的なSpring Bootの起動クラスとなんら変わらないです。

```java:SampleSpringBatchApplication.java
@SpringBootApplication
public class SampleSpringBatchApplication {

    public static void main(String[] args) {
        SpringApplication.run(SampleSpringBatchApplication.class, args);
    }
}
```

設定は以上！

## 起動
あとはビルドして起動すればバッチが実行されます。
Spring BootではBean定義された**全ジョブがデフォルトで実行される**仕組みになっているため、特に設定せずとも起動時にジョブが実行されます。
以下は起動ログですが、定義したhelloWorldJobが実行されていることがわかります。

```
$ ./mvnw spring-boot:run

...(省略)...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.5.RELEASE)

...(省略)...

2020-03-24 23:12:36.494  INFO 76528 --- [           main] c.k.s.SampleSpringBatchApplication       : Started SampleSpringBatchApplication in 2.261 seconds (JVM running for 2.83)
2020-03-24 23:12:36.496  INFO 76528 --- [           main] o.s.b.a.b.JobLauncherCommandLineRunner   : Running default command line with: []
2020-03-24 23:12:36.568  INFO 76528 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=helloWorldJob]] launched with the following parameters: [{}]
2020-03-24 23:12:36.638  INFO 76528 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [helloWorldStep]
Hello, World!
2020-03-24 23:12:36.672  INFO 76528 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [helloWorldStep] executed in 34ms
2020-03-24 23:12:36.678  INFO 76528 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=helloWorldJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 63ms

...(省略)...
```

# まとめ
Spring BootでSpring Batchを実行するために最低限行うことを解説しました。最後に今回行った必要なことを記載しておきます。

- spring-boot-starter-batchの追加
- DBの用意、DataSourceの設定
- JobやStepなどの設定（Java Config）
- Step処理の記述（今回はTaskletの記述）

今回はその他の概念や詳しい実行の仕組みなどの説明は割とすっ飛ばしたのでどこかでまた記事を書こうと思います。

# 補足
上記では触れなかったことに説明をいくつか補足で記載しておきます。

## `@StepScope`について
今回実装したHelloWorldTaskletクラスには`@StepScope`というアノテーションを付与しています。
このアノテーションはBeanの生成スコープを定義するもので、Step Scopeは同じStepでのみ同じインスタンスが使われるようにするスコープです。Stepが変わればインスタンスは再生成されます。デフォルトはシングルトンスコープなので、Taskletで何か状態を持っていると別のStepに引き継がれてしまうリスクがあります。それで問題なければ大丈夫ですが、問題ある場合はStep Scopeにしておくのが良いかと思います。
Step ScopeはSpring Batchでのみ利用可能です。

## StepをChunkモデルで構成する場合のStepの定義方法
どこかで別記事に記載する予定です。

## 複数Jobを定義した場合に起動時に実行するJobを制限する方法
Spring BootでSpring Batchを起動するとデフォルトで定義した全Jobが実行されるため、実行したいJobを制限したい場合があります。
その場合は`spring.batch.job.names`にJob名を指定することで実行するJobを制限することが可能です。
複数のJobを指定したい場合はカンマ区切りで指定します。

```yml:application.yml
spring:
  batch:
    job:
      names: helloWorldJob
```

そもそも起動時に全Jobの実行をオフにしたい場合は`spring.batch.job.enabled`をfalseにすることで実現できます。`@SpringBootTest`によるテスト時にJobの実行をさせたくない場合などに有効かと思います。[サンプル](https://github.com/kawakawaryuryu/sample-spring-batch)のテストでは`spring.batch.job.enabled`をfalseに設定しています。

```yml:application.yml
spring:
  batch:
    job:
      enabled: false
```

## JobLauncherは記載していなかったけどどこで呼んでいるのか？
[概念の説明](https://qiita.com/items/4b3f5cc7574b7bd6b625#joblaucher)のところでJobはJobLauncher#runメソッドの呼び出しによって実行されると記載しましたが、サンプルアプリケーションではJobLauncherの記載は特にしませんでした。ではどこで呼び出しているのでしょうか。

その答えはSpring Bootの[JobLauncherCommandLineRunner](https://github.com/spring-projects/spring-boot/blob/v2.2.5.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java)クラスにあります。このクラスはCommandLineRunnerインタフェースを実装しているため、Spring Boot起動時にrunメソッドが呼ばれます。そのrunメソッドをたどってみると、[executeメソッド](https://github.com/spring-projects/spring-boot/blob/v2.2.5.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java#L192)でJobLauncher#runメソッドを呼び出していることが分かります。

```java:JobLauncherCommandLineRunner.java
public class JobLauncherCommandLineRunner implements CommandLineRunner, Ordered, ApplicationEventPublisherAware {

    @Override
    public void run(String... args) throws JobExecutionException {
        logger.info("Running default command line with: " + Arrays.asList(args));
        launchJobFromProperties(StringUtils.splitArrayElementsIntoProperties(args, "="));
    }

    protected void launchJobFromProperties(Properties properties) throws JobExecutionException {
        JobParameters jobParameters = this.converter.getJobParameters(properties);
        executeLocalJobs(jobParameters);
        executeRegisteredJobs(jobParameters);
    }

    //(略)

    protected void execute(Job job, JobParameters jobParameters)
            throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
        JobParametersInvalidException, JobParametersNotFoundException {
        JobParameters parameters = getNextJobParameters(job, jobParameters);
        JobExecution execution = this.jobLauncher.run(job, parameters); //Jobの実行を呼び出している
        if (this.publisher != null) {
            this.publisher.publishEvent(new JobExecutionEvent(execution));
        }
    }
```

ではこのJobLauncher#runメソッドの引数に与えたJobはどこから取得しているのか。さかのぼってみると[setJobsメソッド](https://github.com/spring-projects/spring-boot/blob/v2.2.5.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java#L142)でJobのCollectionがセッターインジェクションされていることが分かります。つまり、Bean定義したJobをDIすることでJobを取得しています。

ちなみにapplication.ymlで`spring.batch.job.names`を指定している場合はどうやってJobを取得しているのか探ってみると、[BatchAutoConfigurationクラスのBeanメソッドであるjobLauncherCommandLineRunner](https://github.com/spring-projects/spring-boot/blob/v2.2.5.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java#L76-L78)でJob名の取得が行われて、それをJobLauncherCommandLineRunnerクラスのフィールドにセットし、最終的に[JobLauncherCommandLineRunnerのexecuteRegisteredJobsメソッド](https://github.com/spring-projects/spring-boot/blob/v2.2.5.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java#L170-L185)にてJob名からJobの取得を行っています。

```java:JobLauncherCommandLineRunner.java
	private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException {
		if (this.jobRegistry != null && StringUtils.hasText(this.jobNames)) {
			String[] jobsToRun = this.jobNames.split(",");
			for (String jobName : jobsToRun) {
				try {
					Job job = this.jobRegistry.getJob(jobName); //Job名からJobを取得
					if (this.jobs.contains(job)) {
						continue;
					}
					execute(job, jobParameters);
				}
				catch (NoSuchJobException ex) {
					logger.debug(LogMessage.format("No job found in registry for job name: %s", jobName));
				}
			}
		}
	}
```

## JobLauncherやJobRepositoryをカスタマイズする方法
JobLauncherやJobRepositoryの設定をカスタマイズしたい場合は`BatchConfigurer`インタフェースを実装してカスタマイズし、そのクラスをBean定義すればOKです。
ここでは詳しく解説しないので、[公式リファレンス](https://docs.spring.io/spring-batch/docs/current/reference/html/job.html#javaConfig)参照してください。

# 参考
- [Spring Batch - Reference Documentation](https://docs.spring.io/spring-batch/docs/current/reference/html/index.html)
- https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-batch-applications
- [Overview (Spring Batch 4.2.0.RELEASE API)](https://docs.spring.io/spring-batch/docs/current/api/)