久しぶりにSpringBatchに触り、自分なりに使い方を整理したかったので、書いた。
※あくまで私の一意見であり、どこかで実践編を書きたいと思う。
前提
- SpringBatchを使おうとして、挫折しそうor挫折済みの人向け
- この後出てくるSpringBatchの用語が分からない人は、まず本家のイントロダクションと用語を説明したページを読んでからの方が良い
- 上記を雑に読んだあと、実際に使ってみて感じた結果をまとめているので、本家で説明していることと違うことがあるかもしれない。
なぜSpringBatchを使おうとしたのか
非同期処理 をしたいから
例えば、Web経由でリクエストを受け付けて、必要なロジックは裏でやるとか。
わりと、思いつきでもやりたくなっちゃうことがあると思う。(反省)
SpringBatchなら、
このBeanを定義して、
@Bean
public JobLauncher jobLauncher(JobRepository jobRepository) {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
// 非同期処理を定義、並列数は3
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
taskExecutor.setConcurrencyLimit(3);
jobLauncher.setTaskExecutor(taskExecutor);
return jobLauncher;
}
実行する。
/** 非同期処理するようにした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を使っておらず、システム通じてプロセス1人だけなら、H2DBのような組み込みでも良い。
Chunk?Tasklet?どっち使えば良いの?
とりあえず、Tasklet
1つでJob
をつくればよい。
感覚的には、アドホックに、たまに動く非同期処理を想定している。
Chunkとか、StepやFlowでの分岐、Retry/Skipは全部忘れてよし。
DB1レコードにつきJob
を1つ動かすイメージで、シンプルにJob
を作ろう。
引数なしの関数を呼び出す
xxxService#execute()
の処理を実行するだけ。簡単でしょ。
@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;
}
}
引数ありの関数を呼び出す
引数ありなら、こうなる。
@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を更新することなどに使える。
@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
で実行した処理が例外を出力して異常終了した場合でも、その間に更新したレコードをロールバックしてくる。
しかし逆にいえば、1つのTasklet
中で行われるDBの更新は、そのTasklet
が終了するまでコミットされない。
だから、1つのTasklet
中で行われるDBの更新をちゃんとコミットしたいなら、コミットしたい単位でTasklet
を分割すればよい。
@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
1回でまとめて処理できるようにする(つもり)。
例えば、
Tasklet
を使った場合は、DB1レコードにつきJob
を1つ動かすイメージで作っていたのを、
Chunk
を使った場合は、DB1レコードにつきChunk
を1つ作るイメージで、
かつDBレコードを複数束ねた1リクエストにつきJob
を1つ動かす。
ただ私の場合、使わなかったため、予想になっている。
使うとしたらどうなるかは、別途まとめたいと思う。