Spring Web の例は前回行ったので、今回は Spring Batch での Hello World を実施する。
要件とやりたいこと
- IntelliJ IDEA + Kotlin + Spring Boot 2.0.0 + Gradle
- 複数の
Job
を作成しコマンドライン引数で実行を分ける - ユニットテストでの実行を行えるようにする
初期設定
IntelliJ IDEA の新規プロジェクトから Spring Initializr を選択。 Gradle Project で言語を Kotlin にしパッケージングは jar を選択。
依存関係のところでは勿論 Batch を入れるのだが Spring Batch はデータベースが使用できるようになっていないと実行できない。
その為対応する JDBC ドライバが必要なのでこれも導入する。
ここでは PostgreSQL がローカルに既にインストールされているものとする。
生成されたプロジェクトの build.gradle
の依存関係は以下のようになる:
dependencies {
compile('org.springframework.boot:spring-boot-starter-batch')
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
runtime('org.postgresql:postgresql')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.batch:spring-batch-test')
}
この時点で Application
クラスを実行すると以下のようにエラーが出力される:
Failed to auto-configure a DataSource: 'spring.datasource.url' is not specified and no embedded datasource could be auto-configured.
DB の URL 設定がなされていないというわけで、プロパティファイルより YAML で書いたほうが便宜がいいということですでにある application.properties
を削除し application.yaml
として以下を定義する:
spring:
datasource:
url: jdbc:postgresql://localhost/postgres
username: postgres
password: postgres
driverClassName: org.postgresql.Driver
この状態で同様に実行し、まだバッチの実装をしていないので何も起きないがとりあえず上記のエラーが出ずに正常にアプリケーションの実行が終了することを確認する。
Tasklet の実装
ネットの情報を見てみると CommandLineRunner
で実装する方法と Tasklet
若しくは Reader, Processor, Writer
で逐次処理をする方法があるようだが、ここでは Spring Batch 公式の Quick Start に記載してある通り Tasklet
を使用する。
以下のような BatchConfiguration
クラスを定義する:
@Configuration
@EnableBatchProcessing
class BatchConfiguration(val jobBuilderFactory: JobBuilderFactory, val stepBuilderFactory: StepBuilderFactory) {
@Component
class Tasklet1 : Tasklet {
override fun execute(contribution: StepContribution?, chunkContext: ChunkContext?): RepeatStatus {
System.out.println("Hello World!")
return RepeatStatus.FINISHED
}
}
@Component
class Tasklet2 : Tasklet {
override fun execute(contribution: StepContribution?, chunkContext: ChunkContext?): RepeatStatus {
System.out.println("Hello World2!")
return RepeatStatus.FINISHED
}
}
@Bean
fun step1(): Step? = stepBuilderFactory.get("step1")?.tasklet(Tasklet1())?.build()
@Bean
fun step2(): Step? = stepBuilderFactory.get("step2")?.tasklet(Tasklet2())?.build()
@Bean
fun job1(): Job? = jobBuilderFactory.get("job1")?.start(step1())?.build()
@Bean
fun job2(): Job? = jobBuilderFactory.get("job2")?.start(step2())?.build()
}
従来の Spring だと、この場合の JobBuilderFactory
と StepBuilderFactory
を DI する為に @Autowired
アノテーションを付けていたようだが、最近のバージョンだと付けなくても注入してくれるようだ。
勿論 @Autowired
を付けても良い。
この例だと job1()
と job2()
という 2 つのジョブが定義されていることになる。
ジョブは更にステップという単位に分割され順々に実行することができるようだが、ここでは 1 ジョブ 1 ステップ構成ということでそれぞれ step1()
と step2()
を定義している。
また Tasklet
の定義はラムダ式でも良いのだが、後述するが Tasklet
単位でのユニットテストを行いたいのであえてクラスとして定義している。
この状態で実行してみると以下のエラーが出る:
java.lang.IllegalStateException: Failed to execute CommandLineRunner
...(中略)...
Caused by: org.postgresql.util.PSQLException: ERROR: relation "batch_job_instance" does not exist
この batch_job_instance
というテーブルは何なのだろうか。
調べてみると、どうも Spring Batch がジョブの実行状態を管理するために内部的に生成するメタデータらしい。
このメタデータのテーブルがまだ DB に無いため does not exist と言われてしまうわけだ。
このメタデータを生成したい場合は application.yaml
に以下のように書けば良い:
spring:
batch:
initialize-schema: always
これで先ほどのように実行すれば問題なく動作するのだが、このメタデータで管理されているのが逆に煩わしく感じる。 多くのプロジェクトの場合ジョブの管理など不要で、実行したい時に実行できればそれでいいはずだ。 以下、このメタデータを使わなくてもバッチを実行できるようにする。
メタデータを使用しないでいい方法
これに関しては Spring Batchのメタデータテーブルを作らせない/使わせないが大変参考になった。
記事を参考に同様の実装を Kotlin で行う。
以下のような MyBatchConfigurer
を定義する:
@Component
class MyBatchConfigurer : BatchConfigurer {
private val transactionManager = ResourcelessTransactionManager()
private val mapJobRepositoryFactoryBean = MapJobRepositoryFactoryBean(transactionManager).also { it.afterPropertiesSet() }
private val jobRepository = mapJobRepositoryFactoryBean.`object`!!
private val jobExplorer = MapJobExplorerFactoryBean(mapJobRepositoryFactoryBean).also { it.afterPropertiesSet() }.`object`!!
private val jobLauncher = SimpleJobLauncher().also {
it.setJobRepository(jobRepository)
it.afterPropertiesSet()
}
override fun getJobRepository(): JobRepository = jobRepository
override fun getJobLauncher(): JobLauncher = jobLauncher
override fun getJobExplorer(): JobExplorer = jobExplorer
override fun getTransactionManager(): PlatformTransactionManager = transactionManager
}
この状態で再度実行すると Tasklet1
と Tasklet2
の実装が呼び出され Hello World! と Hello World2! が出力される。
Tasklet のユニットテストを行う
以下のように @Autowired
を使用して注入した Tasklet
インスタンスに対して普通に実行してみる:
@RunWith(SpringRunner::class)
@SpringBootTest
class TestApplicationTests() {
@Autowired
lateinit var tasklet1: BatchConfiguration.Tasklet1
@Autowired
lateinit var tasklet2: BatchConfiguration.Tasklet2
@Test
fun tasklet1() {
tasklet1.execute(null, null)
}
@Test
fun tasklet2() {
tasklet2.execute(null, null)
}
}
こうすると以下のように 2 重に実行されてしまう:
Hello World!
Hello World2!
Hello World!
Hello World2!
Spring Boot Batch のデフォルトの挙動としてアプリケーションの main()
が実行された時点ですべてのジョブを実行するので、その実行の後にこのテストケースでの各 Tasklet
が実行されてしまう。
やりたいのは各 Tasklet
の実行だけであり、アプリケーション起動時の全ジョブの実行は不要だ。
この実行を無効化するには application.yaml
に以下のように書く:
spring:
batch:
job:
enabled: false # main() での全ジョブ実行を行わない
再度ユニットテストを実行し Hello World! が 2 重に表示されないことを確認する。
JAR から実行するジョブを分けたい
このアプリを JAR ファイルにする。
IntelliJ IDEA のサイドメニューの Gradle から Tasks -> build -> bootJar を選択すると JAR ファイルがビルドされプロジェクトの build/libs
下に置かれる。
これをコマンドラインから job1()
だけ実行したい場合は以下のように呼び出す:
java -jar (JAR ファイルパス) --spring.batch.job.names=job1 --spring.batch.job.enabled=true
コマンドライン引数で application.yaml
の設定を一時的に上書きすることができるので、もし先ほどの設定で spring.batch.job.enabled=false
が指定されている場合は JAR ファイルを実行してもバッチが実行されないので spring.batch.job.enabled=true
を明示的に渡すことで実行できる。