Pulog

Spring Batchのユースケース

本稿では、自分が案件で Spring Batch を利用した際に色々試行錯誤して、結果どのように対応したかをまとめたり、自分は案件では使用していないですが Spring Batch を利用する際に知っておくと良さそうなことをまとめておきます。

Spring Batch の基本的な使い方は以前ChunkモデルとTaskletモデルの2パターンをそれぞれ掲載しているので、そちらを参照してくださればと思います。

Spring BootでSpring BatchのChunkモデルを試す
Spring BootでSpring BatchのTaskletモデルを試す

複数のバッチJobを実装して、片方だけバッチを動作させたい

Q. 複数のバッチJobを実装した状態で Spring Boot を起動するとどちらのバッチJobも動作してしまう。1月単位で動作させたいバッチJobと1日単位で動作させたいバッチJobがあるけれど、片方だけ動作させる方法はないだろうか?

A. Spring Boot 起動時に spring.batch.job.names オプションで起動させたいバッチJobを指定することで可能です。

@Configuration
@EnableBatchProcessing
@Slf4j
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    /**
     * バッチ1 1月に1度動かしたいバッチ
     */
    @Bean
    public Job importUserJob() {
        return jobBuilderFactory.get("importUserJob")
                .incrementer(new RunIdIncrementer())
                .start(importUserJobStep1())
                .build();
    }

    @Bean
    public Step importUserJobStep1() {
        return stepBuilderFactory.get("step1")
                .tasklet((contribution, chunkContext) -> {
                    log.info("=== importUserJobStep1 ===");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }

    /**
     * バッチ2 1日に1度動かしたいバッチ
     */
    @Bean
    public Job testJob() {
        return jobBuilderFactory.get("testJob")
                .incrementer(new RunIdIncrementer())
                .start(testJobStep1())
                .build();
    }

    @Bean
    public Step testJobStep1() {
        return stepBuilderFactory.get("step1")
                .tasklet((contribution, chunkContext) -> {
                    log.info("=== testJobStep1 ===");
                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

上記のうような実装をそのまま起動してしまうと importUserJobtestJob 両方起動してしまうため、Spring Boot を起動時に spring.batch.job.names={Job名称[,...]} とオプションを渡すことで任意のバッチだけを実行させることができます。

kato@KatonoMacBook demo % ./gradlew bootRun -Dspring.batch.job.names=importUserJob

Welcome to Gradle 6.6.1!

~~ 中略 ~~

2020-11-20 18:14:58.600  INFO 11523 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] launched with the following parameters: [{run.id=21}]
2020-11-20 18:14:58.651  INFO 11523 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
2020-11-20 18:14:58.662  INFO 11523 --- [           main] c.e.d.b.BatchConfiguration               : === importUserJobStep1 ===
2020-11-20 18:14:58.691  INFO 11523 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 40ms
2020-11-20 18:14:58.699  INFO 11523 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] completed with the following parameters: [{run.id=21}] and the following status: [COMPLETED] in 64ms

~~ 以下略 ~~

JobBuilderFactory#get で渡した引数を spring.batch.job.names 引数の値に渡す(複数動作させたい場合はカンマ区切りで)ことで、任意のバッチJobだけを起動させることができます。

※ Javaのシステムプロパティを用いているため、先頭に -D が付いています。

サンプルGit

https://git.pu10g.com/root/springBatchExample/tree/node/37/usecase/multiJob

Spring Batch のためだけにDBを用意したくない

Q. Spring Batch の実行結果をデータベース上に保持しておく必要がないが、セキュリティーの観点で(できるだけライブラリ依存を減らしたい)H2DB も用いずに Spring Batch を利用することはできるだろうか?

A. Spring Batch の失敗した処理の再実行などの処理ができなくなるなどのデメリットはあるが、DBを用いずに Spring Batch を利用することは可能です。

ちなみに、DBの設定を行っていないと、通常以下のようなエラーが出て正常にバッチが起動しないです。

19:04:39: Executing task 'bootRun'...

> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE

> Task :bootRun FAILED

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

2020-11-20 19:04:41.957  INFO 11887 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on YukinoMacBook.local with PID 11887 (/Users/yuki/demo/build/classes/java/main started by yuki in /Users/yuki/demo)
2020-11-20 19:04:41.959  INFO 11887 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-11-20 19:04:42.555  WARN 11887 --- [           main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'batchConfiguration': Unsatisfied dependency expressed through field 'jobBuilderFactory'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration': Unsatisfied dependency expressed through field 'dataSource'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
2020-11-20 19:04:42.565  INFO 11887 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2020-11-20 19:04:42.568 ERROR 11887 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

3 actionable tasks: 1 executed, 2 up-to-date

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':bootRun'.
> Process 'command '/Users/yuki/Library/Java/JavaVirtualMachines/corretto-11.0.9.1/Contents/Home/bin/java'' finished with non-zero exit value 1

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 2s
19:04:42: Task execution finished 'bootRun'.

Spring Boot のエントリーポイント(Spring Initializerであれば DemoApplication など)にある @SpringBootApplication アノテーションを以下のように追記すれば Spring Batch は動作するようになります。

- @SpringBootApplication
+ @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

サンプルGit

https://git.pu10g.com/root/springBatchExample/tree/node/37/usecase/inMemoryDb

CSVファイルをDBに取り込む前に、バックアップをしたい

Q. CSVファイルを取り込む前に Amazon S3 へバックアップをしてからデータインポートし、その後に読み込んだCSVファイルを削除するような処理を実装したいが可能だろうか?

A. 1つのバッチJobに複数のStepをセットできるのを利用して実装することができます。

各StepやTaklet, Chunk部分の実装は省略しますが、バッチJobは複数Stepを start , next (または flow , next , end)で繋げることができるので各処理を分割することで対応が可能です。

@Bean
public Job sampleJob() {
    return jobBuilderFactory.get("sampleJob")
        .incrementer(new RunIdIncrementer())
        // ファイルをS3へバックアップ(Taskletモデルで独自実装)
        .start(backupStep())
        // CSVデータをDBにインポート(Chunkモデルの FlatFileItemReader / RepositoryItemWriter で実装)
        .next(importStep())
        // インポートしたCSVデータを削除(Taskletモデルで独自実装)
        .next(deleteStep())
        .build();
}

Batch の開始と終了のタイミングで処理を挟みたい

Q. Batch が開始のタイミングでログ出力したり、バッチが異常終了した場合にメールを送信など行いたいが、可能だろうか?

A. JobLintener を利用することでバッチの前後に処理を実装することができます。

JobExecutionListener インターフェースを実装し、Job に Listener として登録するだけです。

以下のような Listener を実装し……

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.stereotype.Component;

@Component
public class BatchListener implements JobExecutionListener {

    protected final static Logger logger = LoggerFactory.getLogger(BatchListener.class);

    @Override
    public void beforeJob(JobExecution jobExecution) {
        logger.info("Batch start");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        logger.info("Batch end");
        logger.info("Status : " + jobExecution.getExitStatus().getExitCode());

        if (jobExecution.getStatus().isUnsuccessful()) {
            // バッチが正常に終了しなかった場合
            logger.error("Batch job error");
            jobExecution.getAllFailureExceptions()
                    .forEach(Throwable::printStackTrace);
        } else {
            // バッチが正常に終了した場合
            logger.info("Batch job success");
        }
    }
}

Job に listener として登録するだけです、とても簡単。
※以下は Job と Listener を Autowierd する箇所だけ記載しています。

@Autowired
public BatchListener batchListener;

@Bean
public Job importUserJob() {
    return jobBuilderFactory.get("importUserJob")
            .incrementer(new RunIdIncrementer())
            .listener(batchListener)
            .flow(step1())
            .end()
            .build();
}

サンプルGit

https://git.pu10g.com/root/springBatchExample/tree/node/37/usecase/listener

データに応じて ItemWriter をスキップしたい

Q. CSVファイルのデータにDBには取り込みたくないデータが含まれているが、DBに取り込む条件を指定することは可能だろうか?

A. ItemProcessor で取り込みたくないデータの場合は null を返却することで ItemWriter をスキップすることができます。

Spring BootでSpring BatchのChunkモデルを試す

/**
 * ItemWriter をスキップする処理として、名前にアルファベットが含まれる
 * データの場合は DB に書き込まないようにする
 *
 * @return 変換後に保存するアイテム
 */
@Bean
public ItemProcessor<User, User> processor() {
    return user -> {
        final var firstName = user.getFirstName();
        final var lastName = user.getLastName();

        if (user.getFirstName().matches(".*[a-zA-Z].*") || user.getLastName().matches(".*[a-zA-Z].*")) {
            log.info("skip user : " + user);
            return null;
        }
        return new User(firstName, lastName);
    };
}

サンプルGit

https://git.pu10g.com/root/springBatchExample/tree/node/37/usecase/itemWriterSkip

まとめ

Spring Batch を実案件で使う上で必要になりそうなパターンの一部を紹介しました。
まだまだ JobParameter など紹介しきれていないのもあるので、それは追々別記事にまとめようと思います。
もしかしたら上記ユースケースはコンテンツSEOの観点でそれぞれ別記事にするかもです、その際はこちらの記事で告知します。

それでわ。