Spring Batch を使用して指定したジョブを実行する際の考慮点


概要

公式ページ のチュートリアルに従って、プロジェクトをビルドして、IntelliJ IDEA より Spring Batch を実行しました。通常の Spring Boot アプリケーションの機能を利用できる上、ジョブの実行結果、リトライ機能などを有しておりバッチフレームワークとしての品質は高いと感じました。

しかしながら、IntelliJ からアプリケーションのメインクラスを実行すると、定義しているジョブがすべて実行される動作となっており、指定したジョブだけを実行する方法はどのようにしたらよいのかがわからなかったので調査することに。


アプリケーション要件

バッチアプリケーションの要件として、最低限下記を実現したいと考えました。


  • コマンドラインで指定したジョブのみ実行する

  • MySQL に接続する


    • log4jdbc で接続できるようにする。



  • 環境によってアプリケーションの プロファイル を切り替える


実現方法

既存のアプリケーションがトランザクションスクリプトであり、煩雑になってきたので Spring Batch の導入を検討している背景があります。既存のアプリケーションの実行の仕方が、後述の CommandLineJobRunner のインターフェースに類似しているのでこちらでの実現を検討しましたが、この方法では要件を実現できませんでした。しかし、JobLauncherCommandLineRunner から起動する方法で要件を実現できました。

各種ファイルなどで、下記のディレクトリ構造となりました。

├── build.gradle

└── src
└── main
├── java
│   └── hello
│   ├── Application.java
│   ├── BatchConfiguration.java
│   ├── JobCompletionNotificationListener.java
│   ├── Person.java
│   └── PersonItemProcessor.java
└── resources
├── application-development.properties
├── application-production.properties
├── application.properties
├── log4jdbc.log4j2.properties
├── sample-data.csv
└── schema-all.sql


build.gradle

MySQL への接続を実施するため、下記の依存性を追加しました。

    runtime("mysql:mysql-connector-java")

compile "org.lazyluke:log4jdbc-remix:0.2.7"
compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")


application.properties

Spring Boot の機能を利用すると、親となる application.properties とは別に、指定したプロファイルに紐づいた application--${profile}.properties がフレームワークに読み込まれ、環境情報が Environment オブジェクト にセットされます。

環境情報には、Datasource が必要とする spring.datasource.* を含むことができます。

プロファイルごとに application.properties を用意すれば、環境編数か、あるいは、コマンドラインの引数で接続するデータベースを切り替えることができます。


application.properties

spring.batch.initialize-schema=ALWAYS


✳︎ JobRepository を用意するため、上記を環境共通で設定。これで対象のスキーマに JobRepository が使用するテーブルが存在しない場合は自動的にテーブルを生成してくれる。


appliaction-production.properties

spring.datasource.username=production_user

spring.datasource.password=production_password
spring.datasource.url=jdbc:log4jdbc:mysql://kasakaid.production.jp:3306/kasakaidDB?useSSL=true
spring.datasource.driver-class-name=net.sf.log4jdbc.DriverSpy


appliaction-development.properties

spring.datasource.username=root

spring.datasource.password=mysql
spring.datasource.url=jdbc:log4jdbc:mysql://127.0.0.1:3306/kasakaidDB?useSSL=false
spring.datasource.driver-class-name=net.sf.log4jdbc.DriverSpy


log4jdbc.log4j2.properties

log4jdbc を使用するために必要な設定ファイルです。Spring Batch とは関係ありませんが、要件実現のため記述。


log4jdbc.log4j2.properties

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator



システム・プロパティの指定

以上の構成をした後、Spring Batch のアプリケーションを実行する際は、2 つの システム・プロパティ を指定します。

No
システム・プロパティ
説明

1
spring.profiles.active
アクティブにするプロファイル

2
spring.batch.job.names
実行するジョブの名前

1 つ目の spring.profiles.active は有効化するプロファイルです。2 つ目の spring.batch.job.names で実行するジョブ名が指定できます。


実行と結果

システム・プロパティを指定してバッチを実行します。システム・プロパティは、実行時の引数として明示しても、環境変数としてセットしていてもOKです。

下記の例では実行時の引数に 2 つのシステム・プロパティを実行しています。

java -Dspring.profiles.active=development -Dspring.batch.job.names=importUserJob -jar gs-batch-processing-0.1.0.jar

development のプロファイルを指定して、MySQL にアクセスできました!

もちろん、Spring Boot のアスキーアートも表示されています。


. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.5.RELEASE)

2019-01-20 23:16:58.028 INFO 86837 --- [ main] hello.Application : Starting Application on sakaidaikki-no-MacBook-Air.local with PID 86837 (/Users/kasakaid/dev/java/gs-batch-processing/complete/build/libs/gs-batch-processing-0.1.0.jar started by kasakaid in /Users/kasakaid/dev/java/gs-batch-processing/complete/build/libs)
2019-01-20 23:16:58.034 INFO 86837 --- [ main] hello.Application : The following profiles are active: development
2019-01-20 23:17:03.570 INFO 86837 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
2019-01-20 23:24:25.447 INFO 87366 --- [ main] o.s.b.c.r.s.JobRepositoryFactoryBean : No database type set, using meta data indicating: MYSQL
2019-01-20 23:24:25.906 INFO 87366 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : No TaskExecutor has been set, defaulting to synchronous executor.
2019-01-20 23:24:25.934 INFO 87366 --- [ main] jdbc.audit : 1. Connection.getMetaData() returned com.mysql.jdbc.JDBC4DatabaseMetaData@14cd1699
2019-01-20 23:24:25.935 INFO 87366 --- [ main] jdbc.audit : 1. Connection.clearWarnings() returned
2019-01-20 23:24:25.942 INFO 87366 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executing SQL script from class path resource [org/springframework/batch/core/schema-mysql.sql]


CommandLineJobRunnerでの実現について

公式ページ を見ると、CommandLineJobRunner クラスを利用すると、下記引数でジョブを指定できるとあります。

No
引数
説明

1
jobPath
The location of the XML file that will be used to create an ApplicationContext. This file should contain everything needed to run the complete Job

2
jobName
The name of the job to be run.


<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay schedule.date(date)=2007/05/05


この方法でバッチを実現するとすると、最初に実行される main 関数を持つクラスは、CommandLineJobRunner クラスにする必要があります。

そこで、build.graldle の buildJar タスクのセクションを変更して、Start-Class をデフォルトのhello.Application クラスから CommandLineJobRunner に変えます。

bootJar {

baseName = 'gs-batch-processing'
version = '0.1.0'
manifest {
attributes 'Start-Class': 'org.springframework.batch.core.launch.support.CommandLineJobRunner'
}
}

この状態で bootJar タスクを実行すると、build/libs 配下に jar が生成されます。jar を解凍して、jar ファイルの仕様 にある META-INF/MANIFEST.MF を確認すると build.gradle の変更が反映していることが確認できます。

Main-Class には、これまどおり、org.springframework.boot.loader.JarLauncher が指定されています。そして、Spring Boot の仕様である Start-Class はデフォルトでは、hello.Application ですが、CommandLineJobRunner に変わります。


MANIFEST.MF

Manifest-Version: 1.0

Start-Class: org.springframework.batch.core.launch.support.CommandLineJobRunner
Main-Class: org.springframework.boot.loader.JarLauncher


引数を指定して実行

main 関数を持つクラスを jar ファイルに設定できました。これで、この jar を実行する際は、最初に CommandLineJobRunner の main 関数が起動します。-jar の引数に生成された jar を指定して実行します。

java -Dspring.profiles.active=development -jar gs-batch-processing-0.1.0.jar hello.BatchConfiguration importUserJob

システム・プロパティとして、相変わらずプロファイルは必要ですが、spring.batch.job.names にて指定していたジョブ名は、jobPath として第一引数に指定するようになります。第二引数は、@Bean で定義している importUserJob を指定します。正常終了するかと思いましたが、アプリケーションを実行すると下記のエラーが出ました。

21:12:52.267 [main] ERROR org.springframework.batch.core.launch.support.CommandLineJobRunner - Job Terminated in error: Error creating bean with name 'writer' defined in hello.BatchConfiguration: Unsatisfied dependency expressed through method 'writer' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'writer' defined in hello.BatchConfiguration: Unsatisfied dependency expressed through method 'writer' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:732)

サンプルに入っている writer の定義で dataSource を引数として dataSource を 1 つ指定していますが、引数は 0 であるとのことであると思います。


BatchConfiguration.java

    @Bean

public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<Person>()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
.dataSource(dataSource)
.build();
}


@SpringBootApplication を指定

これはどうしたことか、とあれこれと考えてみたのですが、BatchConfiguration クラスにあるアノテーションは下記 2 つだけです。


  • @EnableBatchProcessing

  • @Configuration

Spring Boot に関する定義が一つもないと思い、アノテーションを @Configuration から @SpringBootApplication に変えてみます。というのも、@SpringBootApplication には @Configuration も含まれているためです。


BatchConfiguration.java

@SpringBootApplication

@EnableBatchProcessing
public class BatchConfiguration {


これで正常終了します。


プロファイルが反映されない

ところが、バッチ処理実行後に DB をのぞいてみると、データが people テーブルを含めて、一切入っていません。実行結果のログをみてみると、インメモリである hsql にアクセスしているようです。そういえば、Spring Boot のアスキーアートの文字列も出てきません。

21:30:30.720 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection org.hsqldb.jdbc.JDBCConnection@2f8bc07b

21:30:30.724 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'hikariPoolDataSourceMetadataProvider'
21:30:30.724 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'hikariPoolDataSourceMetadataProvider'
21:30:32.546 [main] INFO org.springframework.batch.core.repository.support.JobRepositoryFactoryBean - No database type set, using meta data indicating: HSQL
21:30:33.032 [main] INFO org.springframework.jdbc.datasource.init.ScriptUtils - Executing SQL script from class path resource [org/springframework/batch/core/schema-hsqldb.sql]
21:30:33.044 [main] INFO org.springframework.jdbc.datasource.init.ScriptUtils - Executed SQL script from class path resource [org/springframework/batch/core/schema-hsqldb.sql] in 12 ms.
21:30:33.753 [HikariPool-1 connection closer] DEBUG com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Closing connection org.hsqldb.jdbc.JDBCConnection@17776a8: (connection evicted)

これはなぜだろうと思って、CommandLineJobRunner クラスと SpringApplication クラスを見比べてみました。

ここでわかったのは、CommandLineJobRunner は、AnnotationConfigApplicationContext で Spring のコンテキストを生成しているだけだということです。そのため、Bean はコンテナに登録されますが、それ以外の動作は期待できそうにもありません。


CommandLineJobRunner.java

    int start(String jobPath, String jobIdentifier, String[] parameters, Set<String> opts) {

ConfigurableApplicationContext context = null;

try {
try {
context = new AnnotationConfigApplicationContext(Class.forName(jobPath));
} catch (ClassNotFoundException cnfe) {
context = new ClassPathXmlApplicationContext(jobPath);
}


一方で SpringApplication だと、Environment に関する機能を初期化しています。また、初期化した Environment インスタンスにて Spring Boot のアスキーアートの文字列を printBanner メソッドで生成しているようです。


SplingApplicaton.java

    public ConfigurableApplicationContext run(String... args) {

StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments); // ここで Environment のプロパティをセットしている。application.properties の値は DataSourceProperties クラスにセットされて、Datasource にセットされる。
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);

SpringApplication クラスでは、様々な初期化が完了すると、最終的に JobLauncherCommandLineRunner クラスからジョブが実行されます。

CommandLineJobRunner クラスでは SpringApplication の初期化処理が存在しないので、プロファイルに関する機能が使えないのです。

今回の調査は要件を実現するためのプロファイル機能に絞りましたが、他にも使えない機能はあるかもしれません。


@PropertySource を検討

プロファイルの機能はおろか、デフォルトで読み込まれるべき application.properties が使用できないことがわかりました。そこで、@PropertySource を使用して、プロパティファイルを明示的に指定する方針を検討してみました。


BatchConfiguartion.java

@SpringBootApplication

@EnableBatchProcessing
@PropertySource("application.properties")
public class BatchConfiguration {
}

そして、application.properties のキーの最後には環境を加えてみます。


application.properties

spring.batch.initialize-schema=ALWAYS

spring.datasource.username.development=root
spring.datasource.password.development=mysql
spring.datasource.url.development=jdbc:log4jdbc:mysql://127.0.0.1:3306/kasakaidDB?useSSL=false
spring.datasource.driver-class-name.development=net.sf.log4jdbc.DriverSpy
spring.datasource.username.production=production_user
spring.datasource.password.production=production_password
spring.datasource.url.production=jdbc:log4jdbc:mysql://production.kasakaid.io:3306/kasakaidDB?useSSL=false
spring.datasource.driver-class-name.production=net.sf.log4jdbc.DriverSpy

システム・プロパティには System.getEnv("spring.profiles.active") でアクセスできるはずです。これで環境の情報は区別できるのでは、と思いますが、この方法はとても煩雑だとおもいます。


キーが煩雑

似たようなキーが大量に並んで見分けづらい


環境を自力で判断

Spring Boot の機能を使うと、現在の環境がなにであるかの判定は、プロファイルを元に Spring が自動で適切な application.properties を拾ってきてくれます。ところが、この方法だと実装者が環境がなにであるかを常に意識しないといけません。フレームワークを使用して得られる恩恵が大幅に失われます。

実装はこんなイメージです。


SpringUtils.java

@Component

public SpringUtils {
@Autowired
Environment env;
public String property(String key) {
return env.getProperty(key + "." + System.getEnv("spring.profiles.active"));
}
}


Spring が自動でやっていることを手動でやらないといけない

今回パッと出るのは、Datasource のプロパティの設定です。Spring Boot では、application.properties の spring.datasource.* の値は勝手に Datasource にセットされます。ところがこの方法だと、Datasource の値を明示的にセットする必要があります。


Configuration.java

    @Autowired

SpringUtils springUtils;
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName(springUtils.property("spring.datasource.driver-class-name"));
ds.setUsername(springUtils.property("spring.datasource.username"));
ds.setPassword(springUtils.property("spring.datasource.password"));
ds.setJdbcUrl(springUtils.property("spring.datasource.url"));
return ds;
}


CommandLineJobRunner 結論

以上のようにデメリットを想像を含めて列挙してみました。列挙した内容は、3 つですが、本来考えなくても良いことを実装者が考えないといけないのは問題があります。また、いざ開発を進めるともっと不都合が出てくきて、考慮する要素が衝突しあって致命的な不都合が発生するとも限りません。

このため、CommandLineJobRunner での Spring Batch の実行はやめたほうがよい、と考えるに至りました。