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

# 概要
[公式ページ](https://spring.io/guides/gs/batch-processing/) のチュートリアルに従って、プロジェクトをビルドして、IntelliJ IDEA より Spring Batch を実行しました。通常の Spring Boot アプリケーションの機能を利用できる上、ジョブの実行結果、リトライ機能などを有しておりバッチフレームワークとしての品質は高いと感じました。
しかしながら、IntelliJ からアプリケーションのメインクラスを実行すると、定義しているジョブがすべて実行される動作となっており、指定したジョブだけを実行する方法はどのようにしたらよいのかがわからなかったので調査することに。

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

+ コマンドラインで指定したジョブのみ実行する
+ MySQL に接続する
    + log4jdbc で接続できるようにする。
+ 環境によってアプリケーションの [プロファイル](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html) を切り替える

# 実現方法
既存のアプリケーションがトランザクションスクリプトであり、煩雑になってきたので Spring Batch の導入を検討している背景があります。既存のアプリケーションの実行の仕方が、後述の [CommandLineJobRunner](#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 オブジェクト](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/env/Environment.html) にセットされます。
環境情報には、Datasource が必要とする [spring.datasource.*](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-connect-to-production-database) を含むことができます。


```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
```

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

## Spring Batch のデータベーススキーマ
__2019/09/07 追記__

Spring Batch では、例えば、このジョブは成功しました、あるいは失敗しました、などといったジョブ実行時の結果を Spring Batch に組み込まれたデータベーススキーマに書き込みます。
そのスキーマを作成するための DDL は、MySQL や PostgreSQL といった各種のデータベースに対応できるよう、複数提供されていますが、これらは、Spring Batch のプロジェクトの [build.gradle](https://github.com/spring-projects/spring-batch/blob/547533fab072289c062916e51c589de36ea3dfe2/spring-batch-core/build.gradle) にてビルド時に、[このファイル](https://github.com/spring-projects/spring-batch/blob/547533fab072289c062916e51c589de36ea3dfe2/spring-batch-core/src/main/sql/schema.sql.vpp) をテンプレートとして生成されるようです。スキーマの作成の動作 は、[公式のページ](https://docs.spring.io/spring-boot/docs/current/reference/html/howto-database-initialization.html) に、

>If you use Spring Batch, it comes pre-packaged with SQL initialization scripts for most popular database platforms. Spring Boot can detect your database type and execute those scripts on startup. If you use an embedded database, this happens by default. You can also enable it for any database type, as shown in the following example:

>
>spring.batch.initialize-schema=always
>

とあるように、Spring Batch 起動時に常に実行される様設定できます。しかしながら、商用環境を含め、継続的にアプリケーションを稼働させている環境においては、Spring Batch 起動時に常にこのスキーマ作成の動作を実行しないほうがよいと考えられます。というのは、Spring Batch が起動したらバッチ処理は成功するものの、このスキーマ作成の動作が実施されるとすでにスキーマが生成されているため、必ずスキーマ作成時にエラーが吐き出されるためです。どうも現在のバージョンでは「スキーマができていたらなにもしない」と動作変更する設定がないようなのです。このエラーはノイズとなるため、しかたがないのであらかじめ、Spring Batch のためのスキーマを該当環境に作成しておきます。DDL 自体は、IntelliJ などで、org.springframework.batch:spring-batch-core:<version>.RELEASE の org.springframework.batch.core 配下を見ると、確認できます。これらのうち、該当環境において使用されているデータベースの DDL を選択して該当環境に実行します。


![Screen Shot 2019-09-07 at 15.18.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191966/f5960ea1-7c0d-ec92-789c-946accee0dc5.png)
![Screen Shot 2019-09-07 at 15.18.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191966/f7a53e80-30a2-f2d8-937e-da83cb04add9.png)

スキーマを作成したら特に Spring Batch 起動時にスキーマを作る動作は不要になるので、商用環境などではスキーマを作成しないよう none をセットします。

```application-production.properties
spring.batch.initialize-schema=none
```
一方、テスト時は大抵 H2 などのインメモリの揮発性のデータベースが使用されるはずですので、テスト時は必ずスキーマを作成するようにします。(ただし、Spring Batch の機構は使用せず Service クラスなどからテストを行うのみとする場合は特にスキーマは作成する必要はありません)

```application-test.properties
spring.batch.initialize-schema=always
```

## log4jdbc.log4j2.properties
[log4jdbc](http://log4jdbc.brunorozendo.com/) を使用するために必要な設定ファイルです。Spring Batch とは関係ありませんが、要件実現のため記述。

```log4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
```

## システム・プロパティの指定
以上の構成をした後、Spring Batch のアプリケーションを実行する際は、2 つの [システム・プロパティ](http://itdoc.hitachi.co.jp/manuals/link/cosmi_v0950/03Y3020D/EY300058.HTM) を指定します。

|No| システム・プロパティ | 説明 |
|---|---|---|
|1| spring.profiles.active | アクティブにするプロファイル |
|2| spring.batch.job.names |実行するジョブの名前 |


1 つ目の spring.profiles.active は有効化するプロファイルです。2 つ目の [spring.batch.job.names](https://docs.spring.io/spring-boot/docs/current/reference/html/howto-batch-applications.html
) で実行するジョブ名が指定できます。

## 実行と結果
システム・プロパティを指定してバッチを実行します。システム・プロパティは、実行時の引数として明示しても、環境変数としてセットしていても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での実現について
[公式ページ](https://docs.spring.io/spring-batch/4.0.x/reference/html/job.html#runningJobsFromCommandLine) を見ると、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 ファイルの仕様](https://docs.oracle.com/javase/jp/1.5.0/guide/jar/jar.html#The%20META-INF%20directory) にある META-INF/MANIFEST.MF を確認すると build.gradle の変更が反映していることが確認できます。
[Main-Class](https://docs.oracle.com/javase/tutorial/deployment/jar/appman.html) には、これまどおり、org.springframework.boot.loader.JarLauncher が指定されています。そして、Spring Boot の仕様である [Start-Class](https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#executable-jar-launcher-manifest) はデフォルトでは、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`](https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-using-springbootapplication-annotation.html) に変えてみます。というのも、`@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`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/PropertySource.html) を使用して、プロパティファイルを明示的に指定する方針を検討してみました。

```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 の実行はやめたほうがよい、と考えるに至りました。