kagamihogeの日記

kagamihogeの日記です。

Spring Batch 5さわる

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()
}

hello worldレベル

まず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より前と変更無い。

mysqlJDBCドライバの依存性を追加する。

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();
  }
}

@ConfigurationPropertiesDataSourcePropertiesに各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)")); // hogehoge
          System.out.println(p.get("localdate").getClass()); // class java.time.LocalDate
          System.out.println(p.get("localdatetime").getClass()); // class java.time.LocalDateTime

          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,identificationFlagidentificationFlagtrue(デフォルト)だとジョブ実行のキー生成にこのパラメータが使用される。spring-batchの同一ジョブ実行判定はjob parameterが同一かどうかで行う。このフラグの導入により、あるjob parameterを同一判定に含むかどうかを切り替えられる。

job parameter(json)

json形式でjob parameterを渡すこともできる。

localdate={\"value\":\"2022-12-12\",\"type\":\"java.time.LocalDate\"}

jsonの場合はJobParametersConverterJsonJobParametersConverterで置き換える。

@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とかに永続化してたり、色々なのでこの辺の設定はプロジェクトによってそれぞれと思われる。