Spring Bootで簡易的なバッチアプリケーション(CLIアプリケーション)を作る方法を紹介してみたいと思います。SpringにはSpring Batchというバッチアプリケーション向けのフレームワークが用意されておりSpring Boot上で使うこともできますが、ちょっとしたバッチ処理を作るには少し敷居が高い(重厚な仕組み)と感じることもあるのではないでしょうか?
そういった場合は、本エントリーで紹介するSpring BootのApplicationRunner
の仕組みを使うことを検討してみても良いと思います。
検証バージョン
- Spring Boot 2.5.5
検証コード
ApplicationRunnerの実装クラスを作る
Spring Bootにはアプリケーションの初期化処理終了後に、コマンドライン引数を受け取って任意の処理を実行することができる仕組みがあり、この仕組みはApplicationRunner
の実装クラスを作成してDIコンテナに登録することで利用することができます。
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関数に渡された生の引数配列を取得する
@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
を実装することにします。
// ...
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;
}
// ...
}
}
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
を実装することにします。
// ...
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にすると良いでしょう。
logging.level.root=warn
spring.main.banner-mode=off
上記設定を行うと以下のように不要なバナーやログが出なくなります。
$ java -jar spring-boot-cli-demo.jar --v
Version : 0.0.1-SNAPSHOT