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とチャンクモデルを上手く使い分けることで、バッチ処理を読みやすく書くことができます。