LoginSignup
54
67

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-01

久しぶりに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?どっち使えば良いの?

とりあえず、Tasklet1つで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実行回数が多いなら、Job1回でまとめて処理できるようにする(つもり)。

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

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

54
67
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
54
67