FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

Spring Batchを触ってみた(チュートリアル実施〜Dockerコンテナ化まで)

初めに

こんにちは、ピクトリンク事業部の竹本です。

この記事では、Springアプリケーションでバッチ処理を行う際によく使用されるSpring Batchについて、アプリケーションを作成し、Docker Image化するところまで紹介しようと思います。

記事を作成しようと思ったきっかけとしては、普段業務でSpringを使ったアプリケーション開発を行なってはいるものの、個人的にSpring Batchについては開発経験がなかったため、知見を深めたいと思ったからです。

Spring Batchの概要

Spring Batchのアーキテクチャの概要は下図のようになります。 spring_batch_architecture

JobLauncher,Job,Stepについて

バッチの設定クラス(今回作成するアプリケーションにおけるBatchConfiguration.java)で定義されたJobは、JobLauncherによって起動されます。Jobには、複数のStepを定義することができます。これらのクラスはJobRepositoryによって管理され、DBに永続化されます。

Taskletの種類(チャンクモデルとタスクレットモデル)について

Taskletにはチャンクモデルとタスクレットモデルの2種類が存在し、ユースケースに応じて使い分けることができます。各モデルの特徴は下記の通りです。 - チャンクモデルは、データをチャンクという一定のサイズに分割し順次処理することによって、大量データを効率的に処理できます。ItemReaderItemProcessorItemWriterインターフェースの実装を必ず行う必要があります。 - タスクレットモデルは、単純な処理を行う場合に適しています。

Spring Batchが処理を行う流れ

Spring Batchは以下のような流れでバッチを実行します

  1. 起動したJobは指定した順にStepを呼び出します。
  2. Step内から定義したTasklet(チャンクモデルの場合はItemReaderItemProcessorItemWriter)が呼び出され、そのなかで定義されているビジネスロジックが実行されます。

バッチアプリケーションの作成

Spring Batchの公式チュートリアルを行い、バッチアプリケーションを作成しました。

バッチの概要としては、csvファイルで受け取った氏名データを大文字に変換し、DBに保存する処理を行います。

チュートリアルで作成したクラスについて

Lombokの追加以外は、基本チュートリアルのリポジトリに書いてあることそのままですので、やっていることの概要だけ記載します。

BatchConfiguration.java

バッチ全体の設定クラスです。ItemReaderItemWriterインターフェースの実装、およびjob全体の設定を行います。

JobCompletionNotificationListener.java

ジョブ実行後の通知処理を行います。

PersonItemProcessor.java

ItemProcessorインターフェースの実装クラスです。小文字を大文字に変換する処理を行います。

Person.java

冗長な記述を削減するため、Lombokを導入しました。

@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@ToString
public class Person {

  private String lastName;
  private String firstName;

}

ジョブを定期実行させるための実装を追加

Springのスケジュール機能を用いてバッチを定期実行させてみます。

JobLauncherComponent.java

Jobを実行するためのコンポーネントです。

Scheduledアノテーションを追加して、cron式で1分ごとにJobを定期実行するよう設定しています。

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class JobLauncherComponent {
    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    private Job job;

    @Scheduled(cron = "0 * * * * *")
    public void runJob() throws Exception {
        System.out.println("@@@Job Started");
        var jobParameters = new JobParametersBuilder()
                .addString("jobId", String.valueOf(System.currentTimeMillis()))
                .toJobParameters();
        jobLauncher.run(job, jobParameters);
    }
}

SpringBatchDemoApplication.java

EnableSchedulingアノテーションを付与し、Springのスケジュール機能を有効にします。

@SpringBootApplication
@EnableScheduling
public class SpringBatchDemoApplication {

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

}

その他設定ファイルについて

local/postgresql/init/schema-all.sql

DBにはPostgreSQLを使用することにしました。そのため、PostgreSQLの文法に合わせてSQLの記述を修正しました。

DROP TABLE IF EXISTS people;
CREATE TABLE people  (
    person_id SERIAL NOT NULL PRIMARY KEY,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);

src/main/resources/application.properties

Spring Batch用のSQL初期化スクリプトを実行するよう設定

#database imageの初回pull時のみ有効化する
spring.batch.jdbc.initialize-schema=always

Dockerコンテナ化

build.gradle

  • gradle-docker-pluginを導入し、build.gradleに設定を書くだけで、Dockerfileを書かなくてもdocker imageを作成できるようにしました。
  • ./gradlew dockerBuildImageコマンドでdocker imageを作成できます。
plugins {
    id 'com.bmuschko.docker-spring-boot-application' version '9.0.1'
}

docker {
    springBootApplication {
        baseImage = 'openjdk:17-jdk-slim-buster'
    }
}

docker-compose.yml

  • imageは、上記で作成したものを使用します。
  • PostgreSQLコンテナへの接続URLは、jdbc:postgresql://postgresql:5432/です。
  • application.propertiesspring.batch.jdbc.initialize-schemaプロパティをdocker-compose.ymlの環境変数から渡すようにしました。
version: '3.8'
services:
  app:
    image: "com.example/springbatchdemo:0.0.1-snapshot"
    ports:
      - "8080:8080"
    environment:
      spring.datasource.driverClassName: "org.postgresql.Driver"
      spring.datasource.url: "jdbc:postgresql://postgresql:5432/testdb?user=root&password=pass"
      SPRING_BATCH_JDBC_INITIALIZE_SCHEMA: "always"
#      Spring Batch用のSQL初期化スクリプトを実行させたくない時は下記を使用する
#      SPRING_BATCH_JDBC_INITIALIZE_SCHEMA: "never"
    depends_on:
      - postgresql
  postgresql:
    image: postgres:13.3
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: testdb
    volumes:
      - ./postgresql/data:/var/lib/postgresql/data
      - ./postgresql/log:/var/log/postgresql
      - ./postgresql/init:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"

動作確認

では、docker-compose upして、作成したアプリケーションを実行してみます。

実行時のログ(不要な部分は省略しています)を見ると、1分おきにジョブが実行されているのがわかります。

※実行環境:M1 Mac

app_1 | 2023-09-08T11:13:08.744Z  INFO 1 --- [           main] c.e.S.SpringBatchDemoApplication         : Started SpringBatchDemoApplication in 0.905 seconds (process running for 1.162)
app_1 | 2023-09-08T11:13:08.746Z  INFO 1 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: []
app_1 | 2023-09-08T11:13:08.840Z  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] launched with the following parameters: [{'jobId':'{value=1694170080019, type= a.lang.String, identifying=true}','run.id':'{value=2, type=class java.lang.Long, identifying=true}'}]
app_1 | 2023-09-08T11:13:08.873Z  INFO 1 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
app_1 | 2023-09-08T11:13:08.900Z  INFO 1 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 26ms
app_1 | 2023-09-08T11:13:08.908Z  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] completed with the following parameters: [{'jobId':'{value=1694170080019, type= a.lang.String, identifying=true}','run.id':'{value=2, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 46ms
app_1 | @@@Job Started
app_1 | 2023-09-08T11:14:00.088Z  INFO 1 --- [   scheduling-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] launched with the following parameters: [{'jobId':'{value=1694171640022, type= a.lang.String, identifying=true}'}]
app_1 | 2023-09-08T11:14:00.106Z  INFO 1 --- [   scheduling-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
app_1 | 2023-09-08T11:14:00.133Z  INFO 1 --- [   scheduling-1] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 26ms
app_1 | 2023-09-08T11:14:00.142Z  INFO 1 --- [   scheduling-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] completed with the following parameters: [{'jobId':'{value=1694171640022, type= a.lang.String, identifying=true}'}] and the following status: [COMPLETED] in 46ms
app_1 | @@@Job Started
app_1 | 2023-09-08T11:15:00.072Z  INFO 1 --- [   scheduling-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] launched with the following parameters: [{'jobId':'{value=1694171700034, type= a.lang.String, identifying=true}'}]
app_1 | 2023-09-08T11:15:00.103Z  INFO 1 --- [   scheduling-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
app_1 | 2023-09-08T11:15:00.137Z  INFO 1 --- [   scheduling-1] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 34ms
app_1 | 2023-09-08T11:15:00.148Z  INFO 1 --- [   scheduling-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] completed with the following parameters: [{'jobId':'{value=1694171700034, type= a.lang.String, identifying=true}'}] and the following status: [COMPLETED] in 62ms
app_1 | @@@Job Started
app_1 | 2023-09-08T11:16:00.071Z  INFO 1 --- [   scheduling-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] launched with the following parameters: [{'jobId':'{value=1694171760024, type= a.lang.String, identifying=true}'}]
app_1 | 2023-09-08T11:16:00.083Z  INFO 1 --- [   scheduling-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
app_1 | 2023-09-08T11:16:00.105Z  INFO 1 --- [   scheduling-1] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 21ms
app_1 | 2023-09-08T11:16:00.120Z  INFO 1 --- [   scheduling-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [FlowJob: [name=importUserJob]] completed with the following parameters: [{'jobId':'{value=1694171760024, type=class java.lang.String, identifying=true}'}] and the following status: [COMPLETED] in 39ms

まとめ

この記事では、Spring Batchを用いたバッチアプリケーションを作成し、Docker Image化するところまで行いました。

所感としては、今までコードを読んでなんとなく理解していたSpring Batchについての解像度を上げることができてよかったと感じています(例えば、タスクレットモデルとチャンクモデルの違い等)。また、PostgreSQLを触ったことがなかったのでその点でもいい知見となりました。

弊社では、Spring Batchを使用する際の構成として、

  1. Spring Batchアプリケーションを作成し、Docker Image化する。
  2. 作成したDocker ImageをECRにPushし、サーバーレスコンテナ実行環境(Fargate)を利用してAWS Batchで動かす。

といった構成がよく使用されています。

今回は1までしか行いませんでしたが、また機会があればAWS Batch上で実行させるところまで試してみたいと思います。

あと、今回は時間等の都合でチュートリアルベースでの学習となってしまったため、また機会があれば0からのバッチアプリケーション作成も行なってみたいですね。

参考文献