Spring Bootの実行可能jarに複数のCommandLineRunner/ApplicationRunner実装クラスを入れて、起動時のプロパティで実行するクラスを切り替える

# 概要

ちょっとしたバッチを作成したい時に Spring Boot で CommandLineRunner/ApplicationRunner インターフェースを実装したクラスを作れば簡単・便利だと思ったのですが、いくつかバッチを作ってみると、

* バッチをいくつも作りたい場合、バッチ１つに付き実行可能 jar ファイルを１つ作らないといけないのだろうか？
* 一部のクラスや実行可能 jar に含まれる lib ディレクトリの下の外部ライブラリの jar ファイルを外に出して共通化しようと思ったが -jar オプションと -classpath オプションが共存できないらしく、jar ファイルからクラスや外部ライブラリの jar ファイルを外に出して共通化する方法が分からない。

というような点で悩みました。さすがにバッチ１つに付き実行可能 jar １つを作りたくはなかったので、１つの実行可能 jar に CommandLineRunner/ApplicationRunner インターフェースを実装したクラスを全て入れて起動時のプロパティで切り替える方法を考えてみました。

# どうやって実行するクラスを切り替えるか？

* Spring Boot では CommandLineRunner/ApplicationRunner インターフェースを実装したクラスの Bean が生成されていればその run メソッドが実行されるので、実行時に指定するプロパティの値で Bean を生成するのか否かを制御します。
* Bean 生成の制御は Spring Boot で提供されている @ConditionalOnProperty アノテーションを利用します。このアノテーションを @Bean アノテーションと一緒にメソッドに付けておくと、指定されたプロパティと値の組み合わせがある場合のみ Bean が生成されるようになります。

# 試した時の開発環境、Spring Boot のバージョン

以下の環境で試しています。

* JDK 8u102
* IntelliJ IDEA Ultimate 2016.2
* Spring Boot 1.4

# サンプルプロジェクトの仕様

multi-applicationrunner-sample-0.0.1-SNAPSHOT.jar という実行可能 jar を作成し、`-Dbatch.execute=...` で実行する CommandLineRunner/ApplicationRunner インターフェースを実装したクラスを指定します。

以下のように動作します（実際は Spring Boot のロゴや INFO ログも出力されます）。

```
> java -Dbatch.execute=Sample01 -jar multi-applicationrunner-sample-0.0.1-SNAPSHOT.jar
Sample01バッチが起動しました

```

```
> java -Dbatch.execute=Sample02 -jar multi-applicationrunner-sample-0.0.1-SNAPSHOT.jar
Sample02バッチです

```

# サンプルプロジェクトのパッケージ・ファイル構成

サンプルプロジェクトは Spring Initializr で Gradle Project として作成しています。以下のファイル以外に build.gradle があります。

```
src
└ main
  ├ java
  │ └ ksbysample
  │   └ batch
  │     ├ MultiApplicationRunnerSampleApplication
  │     ├ PrintService.java
  │     ├ Sample01BatchRunner.java
  │     └ Sample02BatchRunner.java
  └ resources

```

# 各ファイルの内容

## build.gradle

Spring Initializr で生成されたままです。

```groovy
buildscript {
    ext {
        springBootVersion = '1.4.0.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot'

jar {
    baseName = 'multi-applicationrunner-sample'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}


eclipse {
    classpath {
         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
    }
}

```

## MultiApplicationRunnerSampleApplication.java

こちらも Spring Initializr で生成されたままです。

```java
package ksbysample.batch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MultiApplicationRunnerSampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(MultiApplicationRunnerSampleApplication.class, args);
    }
}

```

## PrintService.java

PrintService クラスは Sample01BatchRunner、Sample02BatchRunner クラスに @Component アノテーションを付加しない場合でも @Autowired を記述できることを試すために作成したものです。

```java
package ksbysample.batch;

import org.springframework.stereotype.Service;

@Service
public class PrintService {

    public void println(String msg) {
        System.out.println(msg);
    }

}

```

## Sample01BatchRunner.java

```java
package ksbysample.batch;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class Sample01BatchRunner implements ApplicationRunner {

    @Autowired
    private PrintService printService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        printService.println("Sample01バッチが起動しました");
    }

    @Configuration
    public static class Sample01BatchConfig {
        @Bean
        @ConditionalOnProperty(value = { "batch.execute" }, havingValue = "Sample01")
        public Sample01BatchRunner sample01BatchRunner() {
            return new Sample01BatchRunner();
        }
    }

}

```

ポイントは、

* Sample01BatchRunner クラスは ApplicationRunner インターフェース（あるいは CommandLineRunner インターフェース）を実装し、run メソッドを override します。
* Sample01BatchRunner クラス自体には @Component アノテーションを付加しません。
* Sample01BatchRunner クラス内に @Autowired アノテーションを付加したフィールドは記述可能です。Bean として生成される時にインスタンスが DI されます。
* @Configuration アノテーションを付加した Sample01BatchConfig クラスを記述します。@Configuration アノテーションを付加しているのはこのクラスの内部で @Bean アノテーションを付加したメソッドを定義するためです。
* Sample01BatchConfig クラス内で Sample01BatchRunner Bean を生成するためのメソッドを定義します。このメソッドには @ConditionalOnProperty アノテーションを付加し、起動時のプロパティで Bean が生成されるか否かを制御されるようにします。

## Sample02BatchRunner.java

```java
package ksbysample.batch;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class Sample02BatchRunner implements ApplicationRunner {

    @Autowired
    private PrintService printService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        printService.println("Sample02バッチです");
    }

    @Configuration
    public static class Sample02BatchConfig {
        @Bean
        @ConditionalOnProperty(value = { "batch.execute" }, havingValue = "Sample02")
        public Sample02BatchRunner sample01BatchRunner() {
            return new Sample02BatchRunner();
        }
    }

}

```