Spring Bootで簡易バッチ(CLI)アプリケーションを作る！！

Spring Bootで簡易的なバッチアプリケーション(CLIアプリケーション)を作る方法を紹介してみたいと思います。SpringにはSpring Batchというバッチアプリケーション向けのフレームワークが用意されておりSpring Boot上で使うこともできますが、ちょっとしたバッチ処理を作るには少し敷居が高い（重厚な仕組み）と感じることもあるのではないでしょうか？
そういった場合は、本エントリーで紹介するSpring Bootの`ApplicationRunner`の仕組みを使うことを検討してみても良いと思います。

# 検証バージョン

* Spring Boot 2.5.5

# 検証コード

* https://github.com/kazuki43zoo/spring-boot-cli-demo

# ApplicationRunnerの実装クラスを作る

Spring Bootにはアプリケーションの初期化処理終了後に、コマンドライン引数を受け取って任意の処理を実行することができる仕組みがあり、この仕組みは`ApplicationRunner`の実装クラスを作成してDIコンテナに登録することで利用することができます。

```java
package com.example.demo;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component // DIコンテナの登録対象としてマーク
public class DemoApplicationRunner implements ApplicationRunner { // ApplicationRunnerを実装したクラスを作成

  @Override
  public void run(ApplicationArguments args) {
    // ... 任意の処理を実装
  }

}
```

# コマンドライン引数を受け取る

コマンドラインで指定した引数は、`ApplicationArguments`より取得することができます。
Javaのmain関数の引数は文字列の配列ですが、`ApplicationRunner`を利用すると`ApplicationArguments`を介して名前付き引数を簡単に扱うことができます。

* `containsOption` : 指定した名前の引数(`--{引数名}`)が存在するかチェックする
* `getOptionValues` : 指定した名前の引数値リストを取得する
* `getNonOptionArgs` : 名前指定のない引数値リストを取得する
* `getOptionNames` : 名前付き引数の引数名リストを取得する
* `getSourceArgs` : main関数に渡された生の引数配列を取得する

```java:引数へアクセスする処理の実装例
@Override
public void run(ApplicationArguments args) {
  if (args.containsOption("h") || args.containsOption("help")) {
    System.out.println();
    System.out.println("[Usage]");
    System.out.println("  java -jar spring-boot-cli-demo.jar {calculation expressions}");
    System.out.println();
    System.out.println("[Command named arguments]");
    System.out.println("  --h (--help)");
    System.out.println("       print help");
    System.out.println("  --v (--version)");
    System.out.println("       print version");
    System.out.println();
    System.out.println("[Exit Codes]");
    System.out.println("  0 : Normal");
    System.out.println("  1 : Application error");
    System.out.println("  2 : Command arguments invalid");
    System.out.println("  3 : Calculation error");
    return;
  }
  if (args.containsOption("v") || args.containsOption("version")) {
    System.out.println();
    System.out.println("Version : " + getClass().getPackage().getImplementationVersion());
    return;
  }
  List<String> values = args.getNonOptionArgs();
  if (values.isEmpty()) {
    // 計算式の指定がない場合は警告ログを出力して処理を終了
    logger.warn("calculation expressions is required.");
    return;
  }
  String expressionString = String.join(" ", values);
  System.out.println("Expression : " + expressionString);
  System.out.println("Result     : " + new SpelExpressionParser().parseExpression(expressionString).getValue());
}
```

上記の実装例では・・・

```
$ java -jar spring-boot-cli-demo.jar --h
```

とすると、以下のようにこのCLIアプリケーションの使い方がコンソールへ出力されます。

```
[Usage]
  java -jar spring-boot-cli-demo.jar {calculation expressions}

[Command named arguments]
  --h (--help)
       print help
  --v (--version)
       print version

[Exit Codes]
  0 : Normal
  1 : Application error
  2 : Command arguments invalid
  3 : Calculation error
```

また・・・

```
$ java -jar spring-boot-cli-demo.jar --v
```

とすると、以下のようにこのCLIアプリケーションのバージョンがコンソールへ出力されます。

```
Version : 0.0.1-SNAPSHOT
```


さらに・・・

```
$ java -jar spring-boot-cli-demo.jar 1 + 1
```

とすると、引数で受け取った文字列をSpELで評価した値が実行結果としてコンソールへ出力されます。

```
Expression : 1 + 1
Result     : 2
```

# 処理内容に応じた終了コードのカスタマイズ

デフォルトの動作だとrunメソッドが正常に終了した場合(=例外をスローしない場合)は、Javaプロセスの終了コードは「`0`」になります。
このままでも大きな問題はないと思いますが、引数として計算式の指定がない場合の終了コードを「`0`」以外(例：`2`)にしたい！！というケースもあると思います。そういった場合は`ExitCodeGenerator`を実装したコンポーネントをDIコンテナに登録し、main関数の中で`ExitCodeGenerator`から返却された値で`System.exit`することで実現することができます。本エントリーでは`ApplicationRunner`の実装クラスで`ExitCodeGenerator`を実装することにします。

```java:処理内容に応じた終了コードのカスタマイズ実装例
// ...
import org.springframework.boot.ExitCodeGenerator;
// ...

@Component
public class DemoApplicationRunner implements ApplicationRunner, ExitCodeGenerator { // ExitCodeGeneratorを実装

  private int exitCode; // 終了コードを保持しておくフィールドを用意

  @Override
  public int getExitCode() { // ExitCodeGeneratorのメソッドを実装
    return exitCode;
  }

  @Override
  public void run(ApplicationArguments args) {
    // ...
    List<String> values = args.getNonOptionArgs();
    if (values.isEmpty()) {
      // 計算式の指定がない場合は警告ログを出力して処理を終了
      this.exitCode = 2; // 終了コードを設定
      logger.warn("calculation expressions is required.");
      return;
    }
    // ...
  }

}
```

```java:処理内容に応じた終了コードのカスタマイズを行う際のmain関数の実装例
package com.example.demo;

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

@SpringBootApplication
public class SpringBootCliDemoApplication {

  public static void main(String[] args) {
    // SpringApplication.exitを呼び出してExitCodeGeneratorから終了コードを取得する
    int exitCode = SpringApplication.exit(SpringApplication.run(SpringBootCliDemoApplication.class, args));
    if (exitCode > 0) {
      // 取得した終了コードを指定してプロセスを終了する
      System.exit(exitCode);
    }
  }

}
```

以下のように計算式を指定しないで実行すると・・・

```
$ java -jar spring-boot-cli-demo.jar
```

終了コードが「`2`」になります。

```
...
2021-09-26 20:20:53.723  WARN 34925 --- [           main] com.example.demo.DemoApplicationRunner   : calculation expressions is required.
$ echo $?
2
```

# 例外発生時の終了コードのカスタマイズ

デフォルトの動作だとrunメソッドから例外をスローした場合は、Javaプロセスの終了コードは「`1`」になります。
このままでも大きな問題はないと思いますが、例外の内容に応じて終了コードを「`1`」以外(例： 計算処理でのエラーは`3`)にしたい！！というケースもあると思います。そういった場合は`ExitCodeExceptionMapper`を実装したコンポーネントをDIコンテナに登録することで実現することができます。本エントリーでは`ApplicationRunner`の実装クラスで`ExitCodeExceptionMapper`を実装することにします。

```java:例外発生時の終了コードのカスタマイズ実装例
// ...
import org.springframework.boot.ExitCodeExceptionMapper;
// ...

@Component
public class DemoApplicationRunner implements ApplicationRunner, ExitCodeGenerator, ExitCodeExceptionMapper { // ExitCodeExceptionMapperを実装

  // ...

  @Override
  public int getExitCode(Throwable exception) {
    return exception.getCause() != null && exception.getCause() instanceof SpelEvaluationException ? 3 : 1;
  }

}
```

以下のように計算処理でエラーが発生する状態で実行すると・・・

```
$ java -jar spring-boot-cli-demo.jar 1 + a
```

終了コードが「`3`」になります。

```
...
Expression : 1 + b
2021-09-26 21:21:47.958 ERROR 35769 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:785) ~[spring-boot-2.5.5.jar!/:2.5.5]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:772) ~[spring-boot-2.5.5.jar!/:2.5.5]
...
$ echo $?
3
```


# バナーとログのカスタマイズ

デフォルトではバナーとINFOログが出力されるため、コンソールには以下のようなSpring Bootの稼働ログが出力されます。

```:デフォルトのコンソール出力例
$ java -jar spring-boot-cli-demo.jar --v

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

2021-09-26 20:35:44.412  INFO 35012 --- [           main] c.e.demo.SpringBootCliDemoApplication    : Starting SpringBootCliDemoApplication v0.0.1-SNAPSHOT using Java 11.0.1 on xxx with PID 35012 (/Users/xxx/git-pub/spring-boot-cli-demo/target/spring-boot-cli-demo.jar started by xxx in /Users/xxx/git-pub/spring-boot-cli-demo/target)
2021-09-26 20:35:44.419  INFO 35012 --- [           main] c.e.demo.SpringBootCliDemoApplication    : No active profile set, falling back to default profiles: default
2021-09-26 20:35:45.482  INFO 35012 --- [           main] c.e.demo.SpringBootCliDemoApplication    : Started SpringBootCliDemoApplication in 1.793 seconds (JVM running for 2.53)

Version : 0.0.1-SNAPSHOT
```

もし余計な情報はできるだけ出したくない！！という場合は、バナーの出力をoffにしログ出力レベルをwarnにすると良いでしょう。


```src/main/resources/application.properties
logging.level.root=warn
spring.main.banner-mode=off
```

上記設定を行うと以下のように不要なバナーやログが出なくなります。

```:バナーとINFOログをoffにした際のコンソール出力例
$ java -jar spring-boot-cli-demo.jar --v

Version : 0.0.1-SNAPSHOT
```

# 参考サイト

* https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-application.command-line-runner

* https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-application.application-exit