SpringBatch再入門 - 自分なりに使い方を整理してみた

久しぶりにSpringBatchに触り、自分なりに使い方を整理したかったので、書いた。

※あくまで私の一意見であり、どこかで実践編を書きたいと思う。

# 前提

- SpringBatchを使おうとして、挫折しそうor挫折済みの人向け
- この後出てくるSpringBatchの用語が分からない人は、まず本家の[イントロダクション](https://docs.spring.io/spring-batch/4.0.x/reference/html/spring-batch-intro.html#spring-batch-intro)と[用語を説明したページ](https://docs.spring.io/spring-batch/4.0.x/reference/html/domain.html#domainLanguageOfBatch)を読んでからの方が良い
- 上記を雑に読んだあと、実際に使ってみて感じた結果をまとめているので、本家で説明していることと違うことがあるかもしれない。

# なぜSpringBatchを使おうとしたのか
## **非同期処理** をしたいから
例えば、Web経由でリクエストを受け付けて、必要なロジックは裏でやるとか。
わりと、思いつきでもやりたくなっちゃうことがあると思う。（反省）

SpringBatchなら、
このBeanを定義して、

```java
@Bean
public JobLauncher jobLauncher(JobRepository jobRepository) {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        // 非同期処理を定義、並列数は３
	SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
	taskExecutor.setConcurrencyLimit(3);
	jobLauncher.setTaskExecutor(taskExecutor);
        return jobLauncher;
}
```

実行する。

```java
/** 非同期処理するようにしたJobLuncher */
@Autowired
JobLauncher jobLauncher;

/** 処理を定義したジョブ */
@Autowired
Job job;

public void execute(Long orderId) throws Exception{
    // 引数を定義する
    JobParameters jobParameters = new JobParametersBuilder().addLong(
         "order.id", orderId, true).toJobParameters();
    // 非同期処理　開始！
    jobLauncher.run(job, new JobParameters());
}
```

SpringBatchで困る人は、ここで詰まることはないと思う。
辛いのは、`Job`の作り方だと思う。

## 自分で**非同期処理の仕組み**を作りたくないから
非同期処理を動かすだけなら、`Runnable`インターフェースか`Callable`インターフェースを実装し、`Exetutor`クラスたちに渡せばよい。

だが、複数人数で同じく非同期処理を作り始めたら、どうなるだろう。
リソースは枯渇し、GC連発で遅くなったり、OutOfMemoryErrorに悩まされるのではないかと思う。
これを防ぐなら、仕組みを作ることになるが、やりたくない。

SpringBatchなら、
そのような仕組みを自分で作らず、上記のような使い方を共有すれば良い。
Google先生に聞ける、ということは、とても強みだと思う。

かつ、自分でつくるよりもSpringBatchのほうが、高機能だ。
例えば、`JobParameter`などの情報を使って分かる範囲で、多重実行抑止などをやってくれる。
上記の例では、`order.id`の値をみて、一度実行完了した`Job`を実行しないようにしてくれる。
トランザクション等も利用されているため、自分で考慮はしなくてよい。
　（この辺の挙動は、SpringBatchが嫌いになりそうにもなるけど）

## 大げさにはしたくないから
非同期処理を作ることが前提なら、RabbitMQやKafkaなど、Queueプロセスを立てることもあると思う。

だが、それらを立てるとなると運用が大変になる。
Queueプロセスが死んだら？NWが死んだら？監視は？

これらを考える手間を超えるほど、非同期処理をするメリット出るのは、処理件数がよっぽど多く、
ケースとしてはわりと少ないのではないかと思う。

SpringBatchなら、
いわゆるJava(Spring)プロセス＋RDBを使った普通の構成で作れる。
RDBを使っておらず、システム通じてプロセス１人だけなら、H2DBのような組み込みでも良い。

# Chunk？Tasklet？どっち使えば良いの？
## とりあえず、`Tasklet`１つで`Job`をつくればよい。
感覚的には、アドホックに、たまに動く非同期処理を想定している。
Chunkとか、StepやFlowでの分岐、Retry/Skipは全部忘れてよし。

DB１レコードにつき`Job`を１つ動かすイメージで、シンプルに`Job`を作ろう。

### 引数なしの関数を呼び出す
`xxxService#execute()`の処理を実行するだけ。簡単でしょ。

```java

 @Configuration
 @EnableBatchProcessing
 public class BatchConfiguration {

	@Autowired
	private JobBuilderFactory jobBuilderFactory;

	@Autowired
	private StepBuilderFactory stepBuilderFactory;

	@Autowired
	private OrderService orderService;

	@Bean
	public JobLauncher jobLauncher(JobRepository jobRepository) {
		SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
		jobLauncher.setJobRepository(jobRepository);
		jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
		return jobLauncher;
	}

	@Bean
	public Job job(Step step1) throws Exception {
		return jobBuilderFactory.get("job1").incrementer(new RunIdIncrementer()).start(step1).build();
	}

	@Bean
	public Step step1(Tasklet tasklet1) {
		return stepBuilderFactory.get("step1").tasklet(tasklet1).build();
	}

	@Bean
	public Tasklet tasklet1() {
		// xxxService#execute()を実行する
		MethodInvokingTaskletAdapter tasklet = new MethodInvokingTaskletAdapter();
		tasklet.setTargetObject(orderService);
		tasklet.setTargetMethod("execute");
		return tasklet;
	}
 }
```

### 引数ありの関数を呼び出す
引数ありなら、こうなる。

```java

	@StepScope
	@Bean
	public Tasklet tasklet1(@Value("#{jobParameters['order.id']}") Long orderId) {
		// xxxService#execute()を実行する
		MethodInvokingTaskletAdapter tasklet = new MethodInvokingTaskletAdapter();
		tasklet.setTargetObject(orderService);
		tasklet.setTargetMethod("execute");
		tasklet.setArguments(new Object[] { orderId });
		return tasklet;
	}
 }
```

### 例外があれば異常終了、Jobの再実行は可能な状態で終了する
例えば、業務上入れてはいけない数値がやってきたとか、DBアクセスエラーが起きたなど、
失敗とみなして、あとからやり直しをすることになる場合、とにかく例外を発生させればよい。

そうすれば、SpringBatchは`Job`の実行に失敗した、という情報を持ち、再実行を許してくれる。
かつロールバックも発生して、DBの書き込みもなくなる。

逆に、呼び出した関数が例外を発生させなければ、正常終了する。

### 異常終了したけど、ロールバックはさせないようにする
Jobは失敗だけどロールバックさせないようにもできる。
業務上の処理に失敗した時、失敗した状態にDBを更新することなどに使える。

```java
	@Bean
	public Step step1(Tasklet tasklet1) {
		return stepBuilderFactory.get("step1").tasklet(tasklet1)
		        .exceptionHandler(exceptionHandler()).build();
	}

	private ExceptionHandler exceptionHandler() {
		return new ExceptionHandler() {
			
			@Override
			public void handleException(RepeatContext context, Throwable throwable) throws Throwable {
				// 例外を投げず、終了する
				context.setTerminateOnly();
			}
		};
	}
```

上記のように、`ExceptionHandler`を実装すれば良い。
かつその中で、失敗とみなす場合に呼び出す`RepeatContext#setTerminateOnly()`を使う。

## トランザクション境界を意識して、Tasklet(Step)を複数に分ける
SpringBatchは`Tasklet`(`Step`)の開始から終了までの間で、特に設定せずともトランザクションを有効にしてくれている。
そのため大変ありがたいことに、`Tasklet`で実行した処理が例外を出力して異常終了した場合でも、その間に更新したレコードをロールバックしてくる。

しかし逆にいえば、１つの`Tasklet`中で行われるDBの更新は、その`Tasklet`が終了するまでコミットされない。

だから、１つの`Tasklet`中で行われるDBの更新をちゃんとコミットしたいなら、コミットしたい単位で`Tasklet`を分割すればよい。

```java

	@Bean
	public Job job(Step beforeStep1, Step inProgressStep2, Step afterStep3) throws Exception {
		return jobBuilderFactory.get("job1").incrementer(new RunIdIncrementer())
			.start(beforeStep1)
			.next(inProgressStep2)
			.next(afterStep3)
			.build();
	}
```

## Job実行待ちが多いようなら、Chunkを使うかな
Job実行回数が多いなら、`Job`１回で**まとめて処理**できるようにする（つもり）。

例えば、
`Tasklet`を使った場合は、DB１レコードにつき`Job`を１つ動かすイメージで作っていたのを、
`Chunk`を使った場合は、DB１レコードにつき`Chunk`を１つ作るイメージで、
かつDBレコードを複数束ねた１リクエストにつき`Job`を１つ動かす。

ただ私の場合、使わなかったため、予想になっている。
使うとしたらどうなるかは、別途まとめたいと思う。