初めに
こんにちは、ピクトリンク事業部の竹本です。
この記事では、Springアプリケーションでバッチ処理を行う際によく使用されるSpring Batchについて、アプリケーションを作成し、Docker Image化するところまで紹介しようと思います。
記事を作成しようと思ったきっかけとしては、普段業務でSpringを使ったアプリケーション開発を行なってはいるものの、個人的にSpring Batchについては開発経験がなかったため、知見を深めたいと思ったからです。
Spring Batchの概要
Spring Batchのアーキテクチャの概要は下図のようになります。
JobLauncher,Job,Stepについて
バッチの設定クラス(今回作成するアプリケーションにおけるBatchConfiguration.java
)で定義されたJob
は、JobLauncher
によって起動されます。Job
には、複数のStep
を定義することができます。これらのクラスはJobRepository
によって管理され、DBに永続化されます。
Taskletの種類(チャンクモデルとタスクレットモデル)について
Tasklet
にはチャンクモデルとタスクレットモデルの2種類が存在し、ユースケースに応じて使い分けることができます。各モデルの特徴は下記の通りです。
- チャンクモデルは、データをチャンクという一定のサイズに分割し順次処理することによって、大量データを効率的に処理できます。ItemReader
、ItemProcessor
、ItemWriter
インターフェースの実装を必ず行う必要があります。
- タスクレットモデルは、単純な処理を行う場合に適しています。
Spring Batchが処理を行う流れ
Spring Batchは以下のような流れでバッチを実行します
- 起動した
Job
は指定した順にStep
を呼び出します。 - 各
Step
内から定義したTasklet
(チャンクモデルの場合はItemReader
、ItemProcessor
、ItemWriter
)が呼び出され、そのなかで定義されているビジネスロジックが実行されます。
バッチアプリケーションの作成
Spring Batchの公式チュートリアルを行い、バッチアプリケーションを作成しました。
バッチの概要としては、csvファイルで受け取った氏名データを大文字に変換し、DBに保存する処理を行います。
チュートリアルで作成したクラスについて
Lombokの追加以外は、基本チュートリアルのリポジトリに書いてあることそのままですので、やっていることの概要だけ記載します。
BatchConfiguration.java
バッチ全体の設定クラスです。ItemReader
、ItemWriter
インターフェースの実装、および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.properties
のspring.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を使用する際の構成として、
- Spring Batchアプリケーションを作成し、Docker Image化する。
- 作成したDocker ImageをECRにPushし、サーバーレスコンテナ実行環境(Fargate)を利用してAWS Batchで動かす。
といった構成がよく使用されています。
今回は1
までしか行いませんでしたが、また機会があればAWS Batch上で実行させるところまで試してみたいと思います。
あと、今回は時間等の都合でチュートリアルベースでの学習となってしまったため、また機会があれば0からのバッチアプリケーション作成も行なってみたいですね。