Spring Batchは5.0から色々な変更が入った事を最近知った。
docs.spring.io
www.slideshare.net
なので、良くやる設定などでとりあえず触ってみる。何分初めてさわるバージョンなので間違ってる事書いていたら申し訳ない。
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.4'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-batch'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
}
tasks.named('test') {
useJUnitPlatform()
}
まずsystem.outするtaskletが一つだけのjobを作成する。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleBatch5App {
public static void main(String[] args) {
SpringApplication.run(SampleBatch5App.class, args);
}
}
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class SampleBatch5Config {
@Bean
Job sampleJob(JobRepository jobRepository, PlatformTransactionManager txManager) {
Step step = new StepBuilder("sampleStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
System.out.println("sample job step");
return RepeatStatus.FINISHED;
}, txManager)
.build();
return new JobBuilder("sampleJob", jobRepository)
.start(step)
.build();
}
}
データソースは依存性にh2があれば自動的にそれに対して生成され、spring-batchのメタデータ用テーブルもそちらに自動的に生成される。transaction-managerも同様なので引数に指定すれば自動生成されたものがinjectionされる。
MapJobRepositoryFactoryBean
は削除された。メタデータテーブル使用しない場合にspring-batch5ではどうするかだが、依存性にh2追加すればこうなるので、これが修正方法の第一候補になるだろうか?
複数のjob
以下のように複数jobが有る場合はspring.batch.job.name
プロパティで実行したいjobのbean名を指定する。VM引数の場合は-Dspring.batch.job.name=sampleJob1
のように指定する。
@Configuration
public class SampleBatch5Config {
@Bean("job1")
Job sampleJob(JobRepository jobRepository, PlatformTransactionManager txManager) {
Step step = new StepBuilder("sampleStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
System.out.println("sample job 1");
return RepeatStatus.FINISHED;
}, txManager)
.build();
return new JobBuilder("sampleJob1", jobRepository)
.start(step)
.build();
}
@Bean("job2")
Job sampleJob1(JobRepository jobRepository, PlatformTransactionManager txManager) {
}
}
上述のプロパティを指定しない場合は下記のように実行時エラーになる。
Caused by: java.lang.IllegalArgumentException: Job name must be specified in case of multiple jobs
at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.validate(JobLauncherApplicationRunner.java:116) ~[spring-boot-autoconfigure-3.0.4.jar:3.0.4]
明示的なh2データソースの指定
複数データソース指定の前に、ひとまずauto-configではなく明示的にh2のデータソースを指定する。以下のように src/main/resources/application.properties
に指定する。javaはhello-worldレベルと同じなので省略。
spring.datasource.url jdbc:h2:mem:sampledb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false
spring.datasource.username sa
spring.datasource.password
spring.datasource.driverClassName org.h2.Driver
複数データソース
メインDBをmysql、メタデータをH2と想定する。ここの設定方法はspring-batch 5より前と変更無い。
mysqlのJDBCドライバの依存性を追加する。
runtimeOnly 'com.mysql:mysql-connector-j'
データソース2つ分のプロパティを作成する。
spring.datasource.h2metadata.url jdbc:h2:mem:sampledb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false
spring.datasource.h2metadata.username sa
spring.datasource.h2metadata.password
spring.datasource.h2metadata.driverClassName org.h2.Driver
spring.datasource.mysqlmain.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.mysqlmain.url=jdbc:mysql://localhost:13306/mysql
spring.datasource.mysqlmain.username=root
spring.datasource.mysqlmain.password=password
import javax.sql.DataSource;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.boot.autoconfigure.batch.BatchDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class SampleBatch5Config {
@Bean
@ConfigurationProperties("spring.datasource.h2metadata")
DataSourceProperties h2Properties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("spring.datasource.mysqlmain")
DataSourceProperties mysqlProperties() {
return new DataSourceProperties();
}
@BatchDataSource
@Bean
DataSource h2DataSource() {
return h2Properties()
.initializeDataSourceBuilder()
.build();
}
@Primary
@Bean
DataSource mysqlDataSource() {
return mysqlProperties()
.initializeDataSourceBuilder()
.build();
}
@Bean
JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(mysqlDataSource());
}
@Bean
Job sampleJob(JobRepository jobRepository, PlatformTransactionManager mainTxManager) {
System.out.println(mainTxManager);
Step step = new StepBuilder("sampleStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
System.out.println("asdf");
jdbcTemplate().update("insert into sampletable(rname) values ('sample')");
return RepeatStatus.FINISHED;
}, mainTxManager)
.build();
return new JobBuilder("sampleJob", jobRepository)
.start(step)
.build();
}
}
@ConfigurationProperties
でDataSourceProperties
に各DB用設定をバインドする。さらにそれぞれの設定でDataSource
を作成する。メインであるmysqlには@Primary
を付与し、h2にはメタデータ用データソースを示す@BatchDataSource
を付与する。
transaction-managerは@Primary
を付与したmysqlデータソースに対して自動生成される。が、どちらのDBに対して自動生成されてるのかパッと見わかりにくいので、個人的には明示的にmysqlに対するJdbcTransactionManager
を作った方がいいかも……などと思う。
job parameter
job parameterのフォーマットが変更された。
@Configuration
public class SampleBatch5Config {
@Bean
Job sampleJob(JobRepository jobRepository, PlatformTransactionManager txManager) {
Step step = new StepBuilder("sampleStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
Map<String, Object> p = chunkContext.getStepContext().getJobParameters();
System.out.println(p.get("hoge(int)"));
System.out.println(p.get("localdate").getClass());
System.out.println(p.get("localdatetime").getClass());
return RepeatStatus.FINISHED;
}, txManager)
.build();
return new JobBuilder("sampleJob", jobRepository)
.start(step)
.build();
}
}
上記のstepでの引数の指定例は以下の通り。前バージョンのフォーマットparameterName(parameterType)
のままだとhoge(int)
のように型ごとkeyになる。
hoge(int)=hogehoge \
intkey=10,java.lang.Integer \
localdate=2022-12-12,java.time.LocalDate \
localdatetime=2011-12-03T10:15:30,java.time.LocalDateTime
parameterName=parameterValue,parameterType,identificationFlag
のidentificationFlag
がtrue
(デフォルト)だとジョブ実行のキー生成にこのパラメータが使用される。spring-batchの同一ジョブ実行判定はjob parameterが同一かどうかで行う。このフラグの導入により、あるjob parameterを同一判定に含むかどうかを切り替えられる。
job parameter(json)
json形式でjob parameterを渡すこともできる。
localdate={\"value\":\"2022-12-12\",\"type\":\"java.time.LocalDate\"}
jsonの場合はJobParametersConverter
をJsonJobParametersConverter
で置き換える。
@Configuration
public class SampleBatch5Config {
@Bean
public JobParametersConverter jobParametersConverter() {
return new JsonJobParametersConverter();
}
DefaultBatchConfiguration
spring-batch全体の設定変更するDefaultBatchConfiguration
が追加された。とりあえず使ってみる。
import javax.sql.DataSource;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.batch.BatchDataSource;
import org.springframework.boot.autoconfigure.batch.BatchDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.batch.BatchProperties;
import org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.StringUtils;
@Configuration
@EnableConfigurationProperties(BatchProperties.class)
@Import(DatabaseInitializationDependencyConfigurer.class)
public class SampleBatch5Config extends DefaultBatchConfiguration {
@Bean
public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher,
JobExplorer jobExplorer,
JobRepository jobRepository, BatchProperties properties) {
JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer,
jobRepository);
String jobNames = properties.getJob().getName();
if (StringUtils.hasText(jobNames)) {
runner.setJobName(jobNames);
}
return runner;
}
@Configuration(proxyBeanMethods = false)
static class DataSourceInitializerConfiguration {
@Bean
BatchDataSourceScriptDatabaseInitializer batchDataSourceInitializer(DataSource dataSource,
@BatchDataSource ObjectProvider<DataSource> batchDataSource, BatchProperties properties) {
return new BatchDataSourceScriptDatabaseInitializer(
batchDataSource.getIfAvailable(() -> dataSource),
properties.getJdbc());
}
}
@Bean
Job sampleJob(JobRepository jobRepository, PlatformTransactionManager txManager) {
System.out.println(txManager);
Step step = new StepBuilder("sampleStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
System.out.println("sample job step");
return RepeatStatus.FINISHED;
}, txManager)
.build();
return new JobBuilder("sampleJob", jobRepository)
.start(step)
.build();
}
}
ちなみに、上のサンプルコードは何も設定変更していない(=何も@override
してない)。
ハマった点として、DefaultBatchConfiguration
のbeanがあるとspring-bootのBatchAutoConfigurationが無効になる。なので、spring.batch.job.name
のjob起動とか、メタデータテーブル自動生成とか、その辺が実行されない。なので、上のサンプルコードはその辺の設定をBatchAutoConfiguration
からコピペしている。
とはいえ、ジョブ起動はJobLauncher
だったり、メタデータはmysqlとかに永続化してたり、色々なのでこの辺の設定はプロジェクトによってそれぞれと思われる。