Spring Boot で CLI アプリをつくる

こう作ったら小綺麗に書けるってやりかたが、ある程度見えてきた気がするので、まとめておく。

# 環境

- Java 1.8.0_91
- Maven 3.3.9 (Maven wrapper)
- Spring Boot 1.4.3.RELEASE
- Apache Commons CLI 1.3.1

```
$ ./mvnw -version
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T01:41:47+09:00)
Maven home: /Users/yo1000/.m2/wrapper/dists/apache-maven-3.3.9-bin/2609u9g41na2l7ogackmif6fj2/apache-maven-3.3.9
Java version: 1.8.0_91, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.11.5", arch: "x86_64", family: "mac"
```

# つかうもの

- Spring Boot
- Apache Commons CLI

依存関係の一部を抜粋。

```xml:pom.xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.3.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <commons-cli.version>1.3.1</commons-cli.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-cli</groupId>
        <artifactId>commons-cli</artifactId>
        <version>${commons-cli.version}</version>
    </dependency>
</dependencies>
```

# やること

- オプション付きパラメタの受け取り
- オプション無しパラメタの受け取り
- オプションに対するヘルプの表示
- パイプされた標準入力の受け取り
- エラーハンドリング

# CLI アプリとして起動できるように構成

Spring Boot で、CLI アプリを作成する場合の基本中の基本。`org.springframework.boot.CommandLineRunner` インターフェースを実装したコンポーネントを作成します。

このようなクラスを作成しておくと、ターミナルからアプリケーションを起動した場合に、`run` メソッドが呼び出されるようになります。

```java
@Component
public class DemoCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) {
        // 以下 ここに処理を実装していく
    }
}
```

# パラメタの扱い

Apache Commons CLI を使用しているので、コマンドライン引数の扱いは非常に簡単です。以下の様なことができます。

```java:コマンドライン引数を定義する
Options options = new Options();
options.addOption("?", "help", false, "Print this message.");
options.addOption("o", "output", true, "Output directory.");
```

```java:コマンドライン引数をパースする
// void run(String... args) throws Exception {..}
CommandLine cl = new DefaultParser().parse(options, args);
```

```java:オプションが指定されているかどうかを確認する
if (cl.hasOption("o")) {..}
```

```java:オプション付きパラメタの受け取り
String output = cl.getOptionValue("o");
```

```java:オプション無しパラメタの受け取り
// どちらもパラメタの内容自体は同じ
String[] params = cl.getArgs();
List<String> paramList = cl.getArgList();
```

```java:ヘルプの出力
if (cl.hasOption("?")) {
    new HelpFormatter().printHelp("demo [-o <arg>]", options)
}
```

以上のように、パラメタはとても簡単に扱うことができます。

# 標準入力の扱い

CLI アプリであれば、標準入力から値を受け取って、パイプでつなげて処理できたりすると、スマートな感じがして良いのですが、こちらは Apache Commons CLI では処理できません。`System.in` を使って以下のようにすると、標準入力を扱えます。

```java:標準入力から値の受け取り
// パイプされた標準入力を受け取ると、対話式 CLI に使用する System.console() は null を返す
if (System.console() == null) {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
        String stdin = reader.lines().collect(Collectors.joining());
    } catch (IOException e) {
        ..
    }
}
```

以上のように、パイプされた標準入力の扱いには一手間必要ですが、やり方さえわかってしまえば難しいことはないです。

# エラーハンドリング

Spring を使っているので、AOP で例外をまとめて処理して、標準エラーに出力するのが簡単で良いです。

```java:エラーハンドリング
@Aspect
@Component
public class ExceptionHandlerAdvice {
    @Around("execution(* org.springframework.boot.CommandLineRunner+.run(..))")
    public void handleException(ProceedingJoinPoint joinPoint) {
        try {
            joinPoint.proceed()
        } catch (Exception e) {
            System.err.println(e.getMessage())
        }
    }
}
```

# その他

Spring Boot を使用しているため、CLI アプリとしては、バナー表示やフレームワークログの出力などが気になるので、これらを抑制しておきます。

`src/resources/` 配下に、以下のファイルをそれぞれ配置することで、CLI アプリには望ましくないログ出力を抑制することができます。

```yaml:application.yml
spring:
  main:
    banner-mode: log
```

```xml:logback.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
        </encoder>
    </appender>
    <root level="ERROR">
        <appender-ref ref="CONSOLE_APPENDER"/>
    </root>
</configuration>
```

# 起動方法

最後に、作成した CLI アプリの起動方法を確認しておきます。

実行可能 jar をまだ作成していない場合は、Maven から直接起動することになりますが、こちらの場合、パラメタの渡し方に一癖あります。

```:Javaからjarファイルを使って起動する場合
$ java -jar cli.jar -o oParam nonOptionParam
```

```:Mavenからソースを使って起動する場合
$ ./mvnw spring-boot:run -Drun.arguments=-o,oParam,nonOptionParam
```

以上で、Spring Boot を使用して、CLI アプリを作成する上で、必要になりそうな観点は一通り洗えたかと思います。

実際にこれらを取り入れた CLI アプリを作成したので、(Kotlin プロジェクトですが) こちらも参考にしてみてください。
https://github.com/yo1000/pdf2img