Spring Bootでコンソールアプリケーションを作成する

n-ozawan

皆さん、こんにちは。技術開発グループのn-ozawanです。
猫のゴロゴロ音のギネス記録は54.6dBであり、やかんの水が沸騰したときの音に匹敵するのだそうです。ゴロゴロ音がそこまでうるさく感じないのは、ゴロゴロ音の周波数が20~50Hzの低周波だからです。

本題です。
Springにはバッチ処理を行うためのFWとして、Spring Batchが提供されています。Spring Batchを使えばスケジューリングから並行処理など、バッチ処理に必要な機能を活用できます。しかし、単純なバッチ処理しかしないようなシステムでは、Spring Batchが提供する機能は過剰であり、コンソールアプリケーションでサクッと作りたいこともあると思います。今回はSpring Bootでコンソールアプリケーションを作成する方法を紹介します。

Spring Bootでコンソールアプリケーション

ApplicationRunner

まずはコンソールに「Hello World!」と表示するコンソールアプリケーションを作成します。必要なパッケージはorg.springframework.boot:spring-boot-starterです。Spring BootでHTTP APIを作成する際に利用するorg.springframework.boot:spring-boot-starter-webと違いますので、ご注意ください。

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
}

ApplicationRunnerを実装したクラスを用意します。void run(ApplicationArguments args)をオーバーライドします。

@Component
public class HelloRunner implements ApplicationRunner {

  @Override
  public void run(ApplicationArguments args) throws Exception {
    System.out.println("Hello World!");
  }
}

実行するとrun()メソッドが呼ばれ、「Hello World!」と表示されます。

./gradlew bootRun

Hello World!

実行時にApplicationRunnerを指定する

非常にシンプルなコンソールアプリケーションが出来ました。もし、プロジェクト配下にApplicationRunnerを実装したクラスが複数ある場合、./gradlew bootRunを実行するとその全てのクラスが実行されます。

バッチが1つしかない場合はそれでいいかもしれません。しかし、一般的には複数のバッチ処理が行われますので、バッチごとにApplicationRunnerを実装することになります。このままだと余計なバッチ処理まで動くことになり不都合が生じます。バッチごとにプロジェクトを用意するのも面倒です。その場合、@ConditionalOnPropertyアノテーションが便利です。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "hello")
public class HelloRunner implements ApplicationRunner { ... }

@ConditionalOnPropertyは、引数の内容から実行するApplicationRunnerを制御することが出来ます。上記のコードでは引数batch.executeの値がhelloの場合に、HelloRunnerを実行してくれます。

./gradlew bootRun --args="--batch.execute=hello"

Hello World!

Gradleで引数を渡す場合は--argsを使います。引数に--batch.execute=helloを指定することにより、HelloRunnerが実行されていることが分かると思います。なお、@ConditionalOnPropertyを指定していないApplicationRunnerがある場合、引数でbatch.executeを指定しているか関係なく、必ず実行されますので注意が必要です。

引数

コンソールアプリケーションに引数を渡すことが出来ます。

  @Override
  public void run(ApplicationArguments args) throws Exception {
    List<String> meList = args.getOptionValues("me");
    String me = (meList != null && !meList.isEmpty() ? meList.get(0) : "everyone");
    System.out.println("Hello " + me  + "!");
  }

args.getOptionValues(String name)は、コマンド実行時の引数を取得することが出来ます。返却値の型はList<String>です。コマンド実行時に引数が指定されていない場合、返却値はnullとなることに注意が必要です。上記を実行すると以下のようになります。

./gradlew bootRun --args="--batch.execute=hello --me=n-ozawan"

Hello n-ozawan!

終了コード

コンソールアプリケーションは処理終了時に終了コードを返却します。一般的に終了コードが0の場合は正常終了であり、それ以外は異常終了となります。ApplicationRunnerで終了コードを返却するには、Applicationクラスに終了コードを返却する1行を追加します。

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    ApplicationContext ctx = SpringApplication.run(Application.class, args);
    System.exit(SpringApplication.exit(ctx));
  }
}

SpringApplication.exit()はSpring Applicationを終了し、ApplicationRunnerの処理結果から終了コードを取得するためのヘルパ関数です。取得した終了コードをSystem.exit()に渡して処理を終了します。

ApplicationRunner側に終了コードを返却する処理を実装します。終了コードを返却する場合はExitCodeGeneratorを実装し、int getExitCode()をオーバーライドします。なお、上記のコードでmeListのnullチェックが無いのはわざとです。この件は次項で扱います。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "hello")
public class HelloRunner implements ApplicationRunner, ExitCodeGenerator {

  private int exitCode;

  @Override
  public int getExitCode() {
    return exitCode;
  }

  @Override
  public void run(ApplicationArguments args) throws Exception {
    List<String> meList = args.getOptionValues("me");
    String me = !meList.isEmpty() ? meList.get(0) : "everyone";
    if (me.equals("n-ozawan")) {
      exitCode = 1;
      return ;
    } 
    System.out.println("Hello " + me + "!");
  }
}

引数men-ozawanを渡すと終了コード1で異常終了するようにしました。実行するとSpring Bootから終了コード1により異常終了した旨、メッセージが表示されます。

./gradlew bootRun --args="--batch.execute=hello --me=n-ozawan"

Execution failed for task ':bootRun'.
> Process 'command '/home/n-ozawan/.sdkman/candidates/java/17.0.8-tem/bin/java'' finished with non-zero exit value 1

echo $?

1

例外処理のハンドリング

デフォルトでは、コンソールアプリケーションの処理中に例外が発生すると終了コード1を返却します。もし、例外によって終了コードを変えたい場合はExitCodeExceptionMapperを実装します。

@Component
@ConditionalOnProperty(value = {"batch.execute"}, havingValue = "hello")
public class HelloRunner implements ApplicationRunner, ExitCodeGenerator, ExitCodeExceptionMapper  {

  // (省略)

  @Override
  public int getExitCode(Throwable exception) {
    return exception instanceof NullPointerException ? 9 : 1;
  }

  // (省略)
}

NullPointerExceptionが発生した場合は終了コード9で異常終了するようにしました。--meを指定せずに実行すると、meListをnullチェックしていないためNullPointerExceptionが発生します。その結果、終了コード9で終了します。

./gradlew bootRun --args="--batch.execute=hello"

Execution failed for task ':bootRun'.
> Process 'command '/home/n-ozawan/.sdkman/candidates/java/17.0.8-tem/bin/java'' finished with non-zero exit value 9

echo $?

1

echo $?の結果が1となっていますが、これは処理終了後にgradleの内部で別の例外が発生しているため、終了コードが1で上書きされたものです。jarファイルを直接実行したら9となりました。

./gradlew build
java -jar ./build/libs/spring-boot-0.0.1-SNAPSHOT.jar --batch.execute=hello
echo $?

9

おわりに

Spring Bootでコンソールアプリケーションを作成するメリットは、SpringフレームワークのDIや多様なパッケージ、機能を特別な設定なしに扱えることにあります。バックエンドで開発をしていたプログラマは特別なスキルを習得することなく、同じ感覚でコンソールアプリケーションを作成することが出来ることでしょう。複雑で高度なバッチ処理を必要としていないのであれば、1つの選択肢としてコンソールアプリケーションは有用かと思います。

ではまた。

Recommendおすすめブログ