Spring Bootを使った開発において、Webアプリケーションのような常駐するプロセスではなく、一連の処理を実行するだけのバッチ処理を書く時、Spring Batchというものが使えます。
この記事では、Spring Batchの使い方を解説します。
文字列を出力するだけの簡単なバッチ (Taskletを使用)
まずは、文字列を出力するだけの簡単なバッチを実装します。
まずは依存ライブラリの設定です。build.gradleのdependenciesのところにSpring Batchのライブラリを記述します。
build.gradle
dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' }
Application.java
Spring Bootを使うので、@SpringBootApplication
アノテーションを付与したクラスが必要です。
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
HelloTasklet.java
処理の内容を記述するクラスです。
Taskletインターフェースを継承したクラスを実装します。
@Component public class HelloTasklet implements Tasklet { // このメソッドの引数は、Taskletを使う際にはお決まりの引数、というように考えて良いです @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { System.out.println("Hello!"); return RepeatStatus.FINISHED; } }
BatchConfig.java
バッチ処理を理解する上で最も重要なクラスです。
バッチ処理を動かすためには、@EnableBatchProcessing
アノテーションを付与したクラスで、Jobクラスのインスタンスを@Bean登録します。
Jobは1つ以上のStepから構成されています。Stepには処理内容を記述するのですが、Taskletに処理の内容を記載し、そのTaskletをStepに登録する、というような記述方法をします。
import lombok.RequiredArgsConstructor; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; // Bean登録を行うため、このアノテーションが必要 @Configuration // 上記の通り、バッチ処理を有効にするために必要 @EnableBatchProcessing // コンストラクタを自動生成するLombokのアノテーション @RequiredArgsConstructor public class BatchConfig { private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; private final HelloTasklet helloTasklet; @Bean public Job job() { return jobBuilderFactory // Job名を指定 .get("job") .start(helloStep()) .build(); } @Bean public Step helloStep() { return stepBuilderFactory // Step名を指定 .get("step") // この記述がないと、一度成功したステップを再度実行できない .allowStartIfComplete(true) // taskletを登録 .tasklet(helloTasklet) .build(); } }
このコードを実行、すなわちApplication
クラスのmain()
メソッドを実行すると、HelloTasklet
クラスに記述した処理が実行され、Hello!
が出力されます。
また、はまりやすいポイントとしては、allowStartIfComplete
のところでしょうか。
実はバッチ処理では、以前にその処理が成功したかどうかを記録しており、すでに成功したステップに関しては、この記述がないと実行することができません。
ちなみに、この記述を忘れると以下のようなエラーログが出力されます。
Job: [SimpleJob: [name=job]] launched with the following parameters: [{}] Step already complete or not restartable, so no action to execute: StepExecution: id=4, version=3, name=step, status=COMPLETED, exitStatus=COMPLETED, readCount=0, filterCount=0, writeCount=0 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=1, rollbackCount=0, exitDescription= Job: [SimpleJob: [name=job]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 17ms
DBからデータを取得して処理を行うバッチ (Taskletを使用)
上記のコードは非常にシンプルな内容でしたが、もう少し実用的な処理を書いてみます。
今回は、動物園で飼育されている動物の情報をまとめたデータベースのデータを使い、パンダがいる動物園の名前を出力するバッチ処理を書いてみます。
このような処理は、チャンクモデルというものを使うとSpring Batchらしい書き方ができるのですが、この方法は複雑なので後述します。
まずはチャンクモデルを使わず、先ほどと同じくTaskletを使う方法で書いてみます。
DBの用意
DBはMySQLを使用します。
test
データベースを作成し、そこにzoo
テーブルを作成します。レコードを3件挿入しておきます。
create table zoo(zoo varchar(100), animal varchar(100)); insert into zoo values("上野動物園", "パンダ"); insert into zoo values("多摩動物公園", "コアラ"); insert into zoo values("アドベンチャーワールド", "パンダ");
build.gradle
build.gradleのdependenciesに、バッチの依存ライブラリに加え、DBの依存ライブラリを記述します。
dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'mysql:mysql-connector-java' }
Application.java
こちらの内容は先ほどと全く同じで大丈夫です。
application.yml
DBの接続情報を記述します。
spring: batch: initialize-schema: ALWAYS datasource: url: "jdbc:mysql://localhost:3306/test?sslMode=DISABLED&allowPublicKeyRetrieval=true&socketTimeout=10000" username: "user" #DBのユーザー名 password: "password" # DBのパスワード driverClassName: "com.mysql.cj.jdbc.Driver" jpa: database: MYSQL
注意点として、Spring Batchを使う場合はspring.batch.initialize-schema
を書かないとエラーになります。
Zoo.java
DBのレコードに対応するエンティティクラスです。
import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @Entity @Table(name = "zoo") @NoArgsConstructor @AllArgsConstructor public class Zoo { @Id private String zoo; private String animal; }
ZooRepository.java
レポジトリクラスです。正確にはクラスではなくインターフェースです。JpaRepositoryを使うので、特にメソッドなどは書く必要がありません。
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface ZooRepository extends JpaRepository<Zoo, String> { }
PandaTasklet.java
先ほどのHelloTasklet
クラスと同じ要領で、Taskletを継承したクラスを作成して処理を記述します。
import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class PandaTasklet implements Tasklet { private final ZooRepository zooRepository; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { List<Zoo> zooList = zooRepository.findAll(); for (Zoo zoo : zooList) { if (zoo.getAnimal().equals("パンダ")) { System.out.println(zoo.getZoo() + "にはパンダがいます。"); } } return RepeatStatus.FINISHED; } }
BatchConfig.java
先ほど説明した「Hello!」を出力する際のバッチ処理と、ほぼ全く同じです。(taskletの種類が変わっただけです。)
@Configuration @EnableBatchProcessing @RequiredArgsConstructor public class BatchConfig { private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; private final PandaTasklet pandaTasklet; @Bean public Job job() { return jobBuilderFactory .get("job") .start(pandaStep()) .build(); } @Bean public Step pandaStep() { return stepBuilderFactory .get("step") .allowStartIfComplete(true) .tasklet(pandaTasklet) .build(); } }
このコードを実行すると、以下のように出力されます。
上野動物園にはパンダがいます。 アドベンチャーワールドにはパンダがいます。
同じ処理を、チャンクモデルを使って実装してみる
バッチ処理の多くは、以下のような流れになっています。
- DBなどからデータを取得し、
- データのうち必要なデータだけを抽出したり、データの加工などをしたりしてから、
- DBなどに書き込んだりする
今回のコードであれば、それぞれ以下に相当しますね。
- DBから動物園と、そこにいる動物のデータを取得し
- パンダがいる動物園のデータだけ抽出し
- 動物園の名前を出力する
多くのバッチ処理がこのような流れになっていることから、Spring Batchではこのような処理を簡単に書けるようになっており、そのような書き方は「チャンクモデル」と呼ばれます。
「チャンクモデル」では1つのStepの中で連続して、上記の1番の処理(データ取得)をItemReader
クラスで、2番の処理(データ抽出)をItemProcessor
クラスで、3番の処理(データ出力)をItemWriter
クラスで行います。
また、これらのクラスはインターフェースなので、ItemReader
インターフェースの実装クラスで処理を行う、という言い方が正確です。(ItemProcessor
とItemWriter
についても同様)
ではコードを見てみましょう。先ほどと変わる部分はBatchConfig
クラスのみとなり、この書き方ではTaskletクラス(先ほどのコードではPandaTasklet
クラス)は使用しません。
BatchConfig.java
import java.util.List; import javax.sql.DataSource; import lombok.RequiredArgsConstructor; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.BeanPropertyRowMapper; @Configuration @EnableBatchProcessing @RequiredArgsConstructor public class BatchConfig { private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; private final DataSource dataSource; @Bean public Job job() { return jobBuilderFactory .get("job") .start(pandaStep()) .build(); } @Bean public Step pandaStep() { return stepBuilderFactory .get("step") .allowStartIfComplete(true) // チャンクモデルを使用するための設定 .<Zoo, Zoo>chunk(1) .reader(zooReader()) .processor(zooProcessor()) .writer(zooWriter()) .build(); } // ItemReader DBからデータを読み込む @Bean public ItemReader<Zoo> zooReader() { return new JdbcCursorItemReaderBuilder<Zoo>() .dataSource(dataSource) .name("zooReader") .sql("SELECT * FROM zoo") .rowMapper(new BeanPropertyRowMapper<>(Zoo.class)) .build(); } // ItemProcessor パンダがいる動物園のデータのみItemWriterに渡す @Bean public ItemProcessor<Zoo, Zoo> zooProcessor() { return new ItemProcessor<Zoo, Zoo>() { @Override public Zoo process(Zoo zoo) throws Exception { if (zoo.getAnimal().equals("パンダ")) { return zoo; } else { return null; } } }; } // ItemWriter 出力を行う @Bean public ItemWriter<Zoo> zooWriter() { return new ItemWriter<Zoo>() { @Override public void write(List<? extends Zoo> zooList) throws Exception { for (Zoo zoo : zooList) { System.out.println(zoo.getZoo() + "にはパンダがいます。"); } } }; } }
このコードを実行すると、先ほどと同じく以下のような出力がされます。
上野動物園にはパンダがいます。 アドベンチャーワールドにはパンダがいます。
また、zooProcessor
とzooWriter
に関しては、ともにメソッドが1つしかないインターフェースを実装したクラス(無名クラス)なので、以下のようにラムダ式を使って簡潔に書くことも可能です。
@Bean public ItemProcessor<Zoo, Zoo> zooProcessor() { return (Zoo zoo) -> { if (zoo.getAnimal().equals("パンダ")) { return zoo; } else { return null; } }; }
@Bean public ItemWriter<Zoo> zooWriter() { return (List<? extends Zoo> zooList) -> { for (Zoo zoo : zooList) { System.out.println(zoo.getZoo() + "にはパンダがいます。"); } }; }
まとめ
以上のように、バッチ処理ではJobに登録されたStepが順番に実行されていきます。
StepにはTaskletを登録でき、Taskletに処理内容を記述します。
あるいは、データの読み出し→データの抽出や加工→書き込みや出力といった処理を行う場合には、Taskletを使わずにチャンクモデルを使うと簡単に書くことができます。
Taskletとチャンクモデルを上手く使い分けることで、バッチ処理を読みやすく書くことができます。