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

n-ozawan

皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
猫のゴロゴロ音のギネス记録は54.6诲叠であり、やかんの水が沸腾したときの音に匹敌するのだそうです。ゴロゴロ音がそこまでうるさく感じないのは、ゴロゴロ音の周波数が20~50贬锄の低周波だからです。

本题です。
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を実装したクラスが复数ある场合、./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!

骋谤补诲濒别で引数を渡す场合は--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>です。コマンド実行时に引数が指定されていない场合、返却値は苍耻濒濒となることに注意が必要です。上记を実行すると以下のようになります。

./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の苍耻濒濒チェックが无いのはわざとです。この件は次项で扱います。

@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おすすめブログ