Spring Bootでコンソールアプリケーションを作成する
皆さん、こんにちは。技術開発グループの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 + "!");
}
}
引数me
にn-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つの選択肢としてコンソールアプリケーションは有用かと思います。
ではまた。