GraalVM, PicocliとJavaで
ときめくネイティブ
コマンドライン
アプリを作ろう

hello

今日の話題

talk topics

コマンドラインアプリ:
Java対Go-lang

fight for cli apps

Java パッケージング: 重ッ!

java heavy

改善中

packaging improvements

でも疑問が残る…

best solution

GraalVM ネイティブイメージ

Javaで単一の実行可能ファイル!

Holy Grail.shutterstock 268713941

GraalVMとは

  • JITコンパイル:Oracle Java 8(Java 11は
    もうすぐ)と置き換えて使用可能。早い!

  • Truffle フレームワークで「polyglot」多言語対応:JavaScript, R, Ruby, Python, C, C++, JVM言語, …​

  • AOT (Ahead Of Time) コンパイル:
    ネイティブイメージ!

Oracle の提供。オープンソース。Community版とEnterprise版がある。

GraalVM アーキテクチャ

graal stack

GraalVM AOT
ネイティブイメージ

graal aot

ネイティブイメージの弱点

  • 「閉世界仮説」:ビルド時に全ての到達可能なコードが見える前提

  • 動的なJava機能はコンフィギュレーションが必要:リフレクション、リソース、プロキシ、JNI

  • 実行時にクラスロードはできない

パフォーマンストレードオフ

graal aot jit tradeoffs

AOTはコマンドラインアプリにピッタリでは?

Let’s Try It!

perfect match

「checksum」コマンドラインアプリ を作ろう

$ echo "hi" > hi.txt

$ checksum -a md5 hi.txt
764efa883dda1e11db47671c4a3bbd9e

$ checksum -a sha1 hi.txt
55ca6286e3e4f4fba5d0448333fa99fc5a404a73

ちょっと遠回り…

topics

ときめくCLIアプリとは

cli apps spark joy

Picocli が力になる

help climb

ときめくCLI:使いやすい

標準的なオプションとパラメータ

例えば、UNIX:

OptionsAndParameters

オプション

import picocli.CommandLine.Option;

class CheckSum {

    @Option(names = {"-a", "--algorithm"}, (1)
      description = "MD-5, SHA-1, SHA-256, ...") (2)
    String algorithm = "MD-5"; (3)
1名前付きのオプション
2ヘルプ時に表示する説明
3デフォルト値

位置パラメータ

import picocli.CommandLine.Parameters;

class CheckSum {
  // ...

  @Parameters(index= "0", (1)
        description= "The file whose checksum to calculate.")
  File file; (2)
1位置パラメーターの位置
2型変換:文字列➝ java.io.File

ときめくCLI:ヘルプ付き

標準なヘルプオプション:

  • 使い方のヘルプ: -h, --help

checksum usage help
  • バージョン情報: -V, --version

checksum version

picocliでヘルプを付ける(旧)

@Command(name = "myapp", version = "myapp 1.0") (1)
class MyApp {

  @Option(names = {"-h", "--help"}, usageHelp = true, (2)
    description = "Show this help message and exit.")
  boolean isHelpRequested;

  @Option(names = {"-V", "--version"}, versionHelp = true, (3)
    description = "Print version information and exit.")
  boolean isVersionRequested;
1バージョン情報
2usageHelp 特別なオプション
3versionHelp 特別なオプション

picocli でヘルプを使う(旧)

public static void main(String... args) {
    MyApp myApp = new MyApp();
    CommandLine cmd = new CommandLine(myApp);
    cmd.parseArgs(args);
    if (cmd.isUsageHelpRequested()) { (1)
        cmd.usage(System.out);
    } else if (cmd.isVersionHelpRequested()) { (2)
        cmd.printVersionHelp(System.out);
    } else {
        myApp.run();
    }
}
1ユーザが usageHelp = true
オプションを指定した場合
2ユーザが versionHelp = true
オプションを指定した場合

picocliでヘルプを付ける(新)

@Command(name = "myapp", version = "myapp 3.0",
         mixinStandardHelpOptions = true) (1)
class MyApp {
  //@Option(names = "--help", usageHelp = true)... (2)
  //@Option(names = "--version", versionHelp = true)... (3)
1標準なヘルプオプションを混ぜ入れる
2不要
3不要

picocli でヘルプを使う(新)

@Command(name = "myapp", version = "myapp 4.0",
         mixinStandardHelpOptions = true)
class MyApp implements Runnable { (1)
    public void run() {
        // ビジネスロジック (2)
    }
    public static void main(String... args) {
        new CommandLine(new MyApp()).execute(args); (3)
    }
1Runnable または Callable を実装
2run または call メソッドで
ビジネスロジックを書く
3一行で実行

picocliのヘルプ機能は便利

@Command(name = "myapp", mixinStandardHelpOptions = true,
      version = { (1)
        "@|bold,underline Versioned Command 4.0|@", (2)
        "Picocli " + picocli.CommandLine.VERSION,
        "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})",
        "OS: ${os.name} ${os.version} ${os.arch}" (3)
      })
class MyApp {
    @Option(description = "ディレクトリー ${user.home}", (3)
    // ...
1複数行にわたる
2ANSI カラーは使用可能
3システムプロパティ、環境変数、リソースバンドルのキーは変数として使用可能

ときめくCLI:
他のアプリに協力的

  • System.outSystem.err の使い方

  • 終了コード

picocliの execute メソッド

public static void main(String... args) {
  int exitCode = new CommandLine(new MyApp()).execute(args);
  System.exit(exitCode);
}
  • --help ユーザ依頼なら、ヘルプを System.out に出力

  • 不正なパラメータ入力の場合、エラー
    メッセージとヘルプを System.err に出力

  • 終了コード:0=正常終了、1=ビジネスロジックに例外発生、2=不正なパラメータ

ときめくCLI:デフォルト値

「合理的なデフォルト、
でもコンフィグレーション可能」

# デフォルト値の設定ファイルの例
# /home/remko/.checksum.properties

algorithm = SHA-256

picocliデフォルトプロバイダ

import picocli.CommandLine.PropertiesDefaultProvider;

@Command(defaultValueProvider =
        PropertiesDefaultProvider.class, //... (1)
class CheckSum {
    @Option(names = {"-a", "--algorithm"}) (2)
    String algorithm = "MD-5";

    @Parameters(index = "0",
       descriptionKey = "file", //... (2)
    File file;
1picocli 4.1 からビルトイン・プロバイダ
2キーはオプション名、または descriptionKey

ときめくCLI:優雅に失敗する

$ java CheckSum
Missing required parameter: <file>
Usage: checksum [-hV] [-a=<algorithm>] <file>
Prints the checksum (MD5 by default) of a file to STDOUT.
      <file>      The file whose checksum to calculate.
  -a, --algorithm=<algorithm>
                  MD5, SHA-1, SHA-256, ...
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

execute メソッドが
優雅に失敗する

picocliの execute メソッドは、
不正なパラメータ入力があった場合、
自動的にエラーメッセージとヘルプを表示する。

public static void main(String... args) {
  System.exit(new CommandLine(new CheckSum()).execute(args));
}

ときめくCLI:
ユーザを喜ばせる

オートコンプリート

picocli autocompletion demo
  • BashやZSHの入力補完スクリプト生成

  • JLineシェル内のオートコンプリート

Bash/ZSH自動補完

import picocli.AutoComplete;
import picocli.CommandLine;
//...

CommandLine cmd = new CommandLine(new CheckSum());
String script = picocli.AutoComplete.bash("checksum", cmd);

入力補完スクリプト生成

生成サブコマンド

generate-completion サブコマンドに
自動補完スクリプトを生成させよう

import picocli.AutoComplete.GenerateCompletion;

@Command(subcommands = GenerateCompletion.class, //...
public class CheckSum { //...

ユーザが一行で入力補完スクリプトを
インストールできる:

$ source <(checksum generate-completion)

すると、オートコンプリートが使える!

$ ./checksum <TAB><TAB>

ときめくCLI:
ユーザを喜ばせる

格好よくしよう: ASCIIカラーとASCIIアート!

checksum help ja

picocliの日本語対応

@Command(name = "日本語対応デモ", mixinStandardHelpOptions = true,
  usageHelpWidth = 60,
  description = {
    "123456789012345678901234567890123456789012345678901234567890",
    "123456789012345678901234567890",
    "@|red 漢字|@、@|green ひらがな|@、@|blue カタカナ|@などは" +
        "ローマ字よりも幅が長い。Picocliはそういう文字に" +
        "@|bold,blink 2倍の幅|@を与えます。"})
public class JapaneseDemo implements Runnable {
    @Override public void run() { }

    public static void main(String[] args) {
        new CommandLine(new JapaneseDemo()).execute(args);
    }
}

picocliの日本語対応(結果)

レイアウトは指定された幅に従います。

JapaneseDemo

picocliで作ると簡単!

checksum source ja

ときめくCLI:
苦労なくインストール

そう言えば、GraalVMの話をつづきましょう

reflect.jsonの例

[
  {
    "name" : "CheckSum",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "fields" : [
      { "name" : "algorithm" },
      { "name" : "file" }
    ]
  },
  {
    "name" : "picocli.CommandLine$AutoHelpMixin",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "fields" : [
      { "name" : "helpRequested" },
      { "name" : "versionRequested" }
    ]
  }
]

picocli-codegenアノテーションプロセッサがコンパイル時にreflect-config.json
を生成。楽々!

$ mkdir classes
$ javac -cp .:picocli-4.1.0.jar:picocli-codegen-4.1.0.jar \
  -d classes CheckSum.java

$ tree classes
classes
├── CheckSum.class
└── META-INF
    └── native-image
        └── picocli-generated
            ├── proxy-config.json
            ├── reflect-config.json
            └── resource-config.json

ネイティブイメージを作ろう

$ /usr/lib/jvm/graalvm/bin/native-image \
    -cp classes:picocli-4.1.0.jar --no-server \
    --static -H:Name=checksum  CheckSum

単一の実行可能ファイルの
出来上がり!

サイズは 11MB

$ ll -h checksum
-rwxrwxrwx 1 remko remko 11M Sep  4 01:28 checksum*

実行は一瞬

$ time java -cp classes:picocli-4.1.0.jar CheckSum hi.txt

real    0m0.415s   ← 通常のJavaなら 415ミリ秒で起動
user    0m0.609s
sys     0m0.313s
$ time ./checksum hi.txt

real    0m0.004s   ← ネイティブイメージは 4ミリ秒で起動
user    0m0.002s
sys     0m0.002s

Windows版の制限

C:\apps\graalvm-ce-19.2.1\bin\native-image ^
    -cp picocli-4.1.0.jar --static -jar checksum.jar

しかし、別のマシーンで実行すると、
エラーが出る場合がある。

system error msvcr100.dll not found

msvcr100.dll をアプリと一緒に配布が必要

Windows版でANSI色

dependencies {
    compile 'info.picocli:picocli:4.1.0'
    compile 'info.picocli:picocli-jansi-graalvm:1.1.0'
    compile 'org.fusesource.jansi:jansi:1.18'
    annotationProcessor 'info.picocli:picocli-codegen:4.1.0'
}
import picocli.jansi.graalvm.AnsiConsole;

class CheckSum {
  public static void main(String... args) {
    int exitCode;
    try (AnsiConsole ansi = AnsiConsole.windowsInstall()) {
      exitCode=new CommandLine(new CheckSum()).execute(args);
    }
    System.exit(exitCode);
  }
  // ...

Windowsで実行

ちゃんと動きます!

checksum windows

ビルドツール

Gradle

apply plugin: 'com.palantir.graal'
graal {
    mainClass 'picocli.nativeimage.demo.CheckSum'
    outputName 'checksum'
}

Maven

<plugin>
  <groupId>com.oracle.substratevm</groupId>
  <artifactId>native-image-maven-plugin</artifactId>
  <configuration>
    <mainClass>picocli.nativeimage.demo.CheckSum</mainClass>
    <imageName>checksum</imageName>
  </configuration>

結論

コマンドラインアプリをJavaで作ろう!

  • 単一の実行可能ファイルとして配布

  • PicocliでときめくCLIが簡単にできる

リッチなドキュメンテーション

ありがとうございました

picocli を気に入ったら、GitHubで星をつけて、
友達に連携してください! ;-)

よろしくお願いします。 @RemkoPopma