mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

JJUG CCC 2018 (Winterでなく)Fall に参加してきた

表記の通り、 JJUG CCC 2018 (もう 12 月だけど) Fall に参加してきた

www.java-users.jp

で、恒例のそのメモ

f:id:mike_neck:20181019112748p:plain


Pivotal認定講師によるSpring Framework 5.1ハンズオン!

日本で唯一の Pivotal 認定講師カサレアルの 多田さん とカサレアルのスタッフによる Spring Framework 5.1 のハンズオン。 (【2018/12/17 9:08 追記】後ほど教えてもらった話によると、正確には多田さんは Spring Cloud と Cloud Foundry の唯一の認定講師で、 Spring の認定講師には 土岐さんもいらっしゃるとのこと)

教材がかなり作り込まれており、ひょっとしたら、カサレアルで行っている研修内容に近いのではないかと思えるくらいクオリティの高いハンズオンでした(カサレアルのページによると、 『はじめての Spring Mvc によるシステム開発』 というコースで 10 万円だそうで、このセッションに参加するだけで5万円くらいはとってもよさそうなくらいのクオリティ)。

また、多田さんは日本で唯一の Pivotal 認定講師ですが、その一方で、「アブストラクアノテーションコンフィグディスパッチャーサーブレットイニシャライザー」を早口で舌を噛まずに言える日本人唯一の人なので、それを聞くためにも参加して損はしないハンズオンでした。

参加した意図は Spring にここ最近触れていないので、感覚を取り戻しておきたかったからです。ほとんど変わっていないことは確認できる一方、 @RepositoryCrudRepository につけなくてもよくなっていたりで、より簡略できるようになっていることが確認できました。

あとどうでも良い話ですが、前日に準備してくださいと言われていたので、準備した際に、宗教的な理由により maven で構築されたプロジェクトを gradle に移行して本セッションに臨みました。


思考停止しないアーキテクチャー設計

無職を辞めて最近会社を作った 川島さん のセッション。昼休みから戻った時点で部屋が6〜7割埋まってしまうほどの大人気セッションでした。

雑にまとめた内容

  • 非機能要件からアーキテクチャーを決定していく
  • ただし機能要件からもアーキテクチャー要求が発生することがある
  • どんなに丁寧にやってもアーキテクチャー設計の考慮漏れを防ぐのは難しい
  • 企画した後に前提が変わることもあり、技術的負債のメモ/金額換算をしておいて、リスク等を低減する

なお、冒頭に会社設立のゲーム化したもののデモプレイが流れました。 URL を取り忘れたので、誰かあとで教えてください。

(【2018/12/17 9:08 追記】起業クエスト はこちら → 起業クエスト tada_suzu さん ありがとうございます)


複雑なドメインに泥臭く立ち向かう

Koji Sudoさんによる 複雑な業務をどのようにモデル化して、実装に落とし込むかの理論的な話と実際の取り組み例を紹介するセッションでした。すごく話し上手で、聞いているだけで自分があたかもデキる人に感じてしまう魅惑なセッションでもあります。

以下雑なまとめ

  • アプリケーションの最も実現したい部分をまず作ってから、複合的なパターンを構築していく
    • 複雑なものを最初からすべて実現するように作ると、複雑で読解困難なものができてしまう
  • 複雑な業務をモデル化するために
    • 業務について書かれている書籍やガイドなどをもとに勉強する
    • 業務を実際に体験する(ロールプレイングする)
  • モデルを抽出するのは、複雑なものを簡単にするためではなく、複雑なものを人間が理解できるようにするため
  • 複数人で開発する場合、分野により個々の力量差があるため、モブプログラミングを採用するなどして複数のプログラムにレベルの差が現れないようにする
  • データと情報を区別する
    • データ : クラスを構成する値など
    • 情報 : データから導き出される値など

次の時間は眠気で休んでいました

しかし、久保田さんのセッションと思しきツイートがいくつか見られました。自分の勤務先でも現在 Java8 から Java 11 への移行を計画していますが、一度フレームワーク/ライブラリー等の移行のための動作確認等を行っておいたほうがよいかと思いました。


コードをど真ん中にそえた設計アプローチ

いろふさん のセッション。上級者向けとあり、世の中の Java 上級エンジニアが大挙して集まるセッションでした。内容はコードとドキュメントと設計をどのようにして楽におこなうかという話。

以下雑なまとめ

  • ドキュメントは基本的にメンテされないので、動くコードから生成する
  • ロジックよりは型に意図を込めていく
  • ドキュメントとするためにコードが歪むのは本末転倒

また、 JIG というライブラリーを作っていて、こちらからドキュメントを生成するような仕組みを作っているとのことです。

github.com


アンカンファレンス「OSSとコミュニティの運営など」

本当は 「GCを発生させないJVMとコーディングスタイル」 を見ようと思っていたのですが、長蛇の列を見て諦めました。で、とりあえず入ったのがこのセッション。

  • OSS のフリーライド等
    • US の企業で OSS を無償(ノーサポート)で使っていてそのトラブルに見舞われて解決できない場合などは、その企業は株主にかなり責め立てられるそうです(当然、株は売られるので株価が下がる)。したがって、 OSS を使う場合にお金を払うのは企業の姿として至極当然のようです(Red Hat はこのあたりをビジネスにしている)。
  • Meetup のドタキャン
    • 日本だけの問題でもないとのこと
    • わりと企業が Meetup のスポンサーになるそうで、懇親会をしてから本編をやるなどの工夫があるようです

懇親会

本来は懇親会に参加の予定はなかったのですが、 Java チャンピオンの せろさん の機転により参加できました。


今回振り返ってみてですが、あまり先進的なトピックのセッションではなく、開発プロセスの話を多く聞いたように思います。開発プロセスの話に面白そうなのが多かったというのが理由です。

Vim でも AWS lambda 関数

普段 vimh/j/k/l/d/x/0/$/:wq くらいしか使わないのですが、ひょんなことから Vim で Web サーバーがたてられると聞いたので、サーバーが建てられるなら、 http クライアントも動くのだろうと考えて、 ラムダで動くかどうか試してみることにしました。

f:id:mike_neck:20181208195209p:plain


使用する vim

おそらく lambda を動かしている vm には、必要がないから vim は入ってないだろうと考え、 vim をビルドします。なお、このビルドは Amazon Linux 2 で実行しています。

yum install -y git ncurses-devel 
yum groupinstall "Development Tools"
git clone https://github.com/vim/vim
cd vim
./configure --with-features=normal --prefix=$HOME/release

vim の設定はなるべく軽いものにしたかったので、 --with-features オプションは normal にしました。また、リリースするのが目的なので、 --prefix$HOME の下の release ディレクトリーにしました。

また、試しに実行したときに libtinfo.so.6 がないため起動できなかったので、これを release ディレクトリーの下の lib ディレクトリーにコピーします。

このビルドが終わると、 release ディレクトリーの下はこのような形になります。

release
├── bin
│   └── vim
├── bootstrap
└── lib
    └── libtinfo.so.6

vim スクリプト

最初 bootstrap は単なる bash スクリプトにしようかと思いましたが、極力 vim でやろうと思ったのと、 shebang をつけてうまく終わるようにスクリプトを書けば、 vim の画面を表示しないでも vim を実行できる見通しがたったので、 bootstrapvim スクリプトを書いています。ちなみに vim スクリプトを書くのはこれが初めてです。

#!bin/vim -u

:let header = system("mktemp")[:-2]
:let request = "http://" . $AWS_LAMBDA_RUNTIME_API . "/2018-06-01/runtime/invocation/next"

:while 1
:  let event = system("curl -L -D " . header . " " . request)
:  let payload = "{\"receive\":" . event . "}"

:  let headers = readfile(header)
:  let id = ""
:  for item in headers
:    if item =~ "Lambda-Runtime-Aws-Request-Id"
:      let id = substitute(item, "Lambda-Runtime-Aws-Request-Id", "", "g")
:      let id = substitute(id, ":", "", "g")
:      let id = substitute(id, " ", "", "g")
:    endif
:  endfor

:  echo "==request=="
:  echo event
:  echo "==id=="
:  echo id

:  let response = "http://" . $AWS_LAMBDA_RUNTIME_API . "/2018-06-01/runtime/invocation/" . id . "/response"
:  echo response
:  let result = system("curl " . response . " -d '" . payload . "'")
:endwhile

:exit

リリース/デプロイ

先程のディレクトリーの構造

release
├── bin
│   └── vim
├── bootstrap
└── lib
    └── libtinfo.so.6

これを維持したまま、 zip でアーカイブして、 lambda にリリースします。


実行

lambda のコンソールから実行してみました。

次のようなペイロードを投げると、 receive というプロパティに同じオブジェクトが入った json が返ってくるはずです。

テストイベント

{
  "editor": ["vim","emacs","nano"],
  "language": "vimrc"
}

期待するレスポンス

{
  "receive": {
    "editor": ["vim","emacs","nano"],
    "language": "vimrc"
  }
}

そして実行結果の画面キャプチャーはこちら

f:id:mike_neck:20181208195011p:plain
テスト実行


というわけで、 AWS Lambda のカスタムランタイムとして vim を走らせることができました。 Vim スクリプトは サーバーレスアプリケーションを記述する言語になりうるし、 Vim はサーバーレスアプリケーション用のフレームワークと言うことができる。

JEP-330 Single-FIle Source-Code Programs って何? Java ファイルが java コマンドで実行できるってホント?! shebang でどう指定するの?調べてみた!

Java11 で取り込まれている JEP-330 すでにきしださんがブログにしている古い機能ですが、自分用のメモ。

f:id:mike_neck:20181019112748p:plain

(『古い機能』という表現はその手の界隈に人達への忖度表現)


TL;DR ってなんだ???

  • Java 11 にとりこまれた JEP-330 は 1つの java ファイルを java コマンドで実行できる仕組み
  • java という拡張子でなければ、ファイルの先頭に shebang をつけて実行可能ファイルとして実行できる
  • 内部的には javac に依存しているので、カスタムランタイムなどでは実行できない
  • 外部の jar も利用できる
  • shebangclasspath の指定をする場合は順番に要注意

JEP-330(Launch Single-File Source-Code Programs)

JEP-330 では一つのソースコードファイルだけであれば、 java コマンドで実行できるようにしてしまう機能です。

JEP 330: Launch Single-File Source-Code Programs

d.hatena.ne.jp

物は試しに次のようなファイル Hello.java を作ります。

class Hello {
  public static void main(String... args) { System.out.println("Hello"); }
}

このファイルに対して、次のコマンドを実行します。

java Hello.java

結果のスクショをどうぞ。

f:id:mike_neck:20181208130230p:plain
java ファイルを実行できる

ちなみに、上記コマンドの正式な実行の仕方は以下の通りだそうです。

java --source 11 Hello.java

もちろん、この 11Java 11 の 11 で、 11 よりもやや小さい値でも実行できるようです。

f:id:mike_neck:20181208131051p:plain
--source 8

6 を指定すると警告が大量に出ますが、実行できるようです。

f:id:mike_neck:20181208131206p:plain
6 の場合は警告が大量に…

サポートの終わっている 5 では残念ながら実行できません。

f:id:mike_neck:20181208131324p:plain
5 では実行できない


shebang のついてる java

java コマンドで java ファイルを一発で実行できるということは、 java ファイルを解釈するインタプリターとして java コマンドが使えるということなので、次のようなファイルでも valid な java ファイルと言えます。そこで、先程の Hello.java の先頭行に shebang をつけます。

#!/usr/bin/env java --source 11
class Hello {
  public static void main(String... args) { System.out.println("Hello"); }
}

さらに、実行可能にします。

chmod +x Hello.java

では実行してみましょう。

f:id:mike_neck:20181208131706p:plain
失敗した?!

どうやら、 先程のような実行するスクリプトとは認識せずに、 コンパイルするファイルとして認識されてしまった模様です。

このコードの名前を少し変えてみましょう。

mv Hello.java hello
./hello

実行結果はこちらです。

f:id:mike_neck:20181208132114p:plain
実行できた!

どうやら、拡張子が .java になっていると実行ファイルとみなされないようです。


java コマンドだけあればいい?

ここで気になるのが、 この機能は java コマンドだけあればいけるのかということです。たとえば、前回のエントリーのようにカスタムランタイムを作った場合に、実行する内容がシングルファイルで書ける内容であればコンパイルせずに lambda に乗せて実行できるのでしょうか?

mike-neck.hatenadiary.com

試してみます。次のコマンドで作ったカスタムランタイムを shebang で指定してみます。

jlink \
  --compress=2 \
  --module-path $JAVA_HOME/mods \
  --add-modules java.base,java.net.http \
  --output runtime

f:id:mike_neck:20181208134421p:plain
実行できませんでした!

先程のツイートにきつねくんから回答をもらっていて、内部的には javac を実行しているとのことです。 java ファイルとカスタムランタイムをリリースして Java で Script なで JavaS(以下自粛) という淡い夢は儚くも散りました。


ライブラリー使えるの?

次に気になるのが、ライブラリーがある場合はどうなのでしょう?

これも試してみましょう。

ここでは AssertJ を使ってみます。

先程のコードも次のように書き直します。 falseisTrue なわけがないので、落ちるテストです。

import static org.assertj.core.api.Assertions.*;
public class Assert {
  public static void main(String... args) {
    assertThat(false).isTrue();
  }
}

次のコマンドで実行します。

java \
  --class-path assertj-core-3.9.1.jar \
  Assert.java

実行結果はこちら。

f:id:mike_neck:20181208140023p:plain
外部の jar も取り込める


shebang の中で classpath 指定したい

さらにもう一歩進んで、 shebang の中で jar ファイルを指定してみましょう。先程の Assert.javaassert に改名して、 shebang をつけます。

#!/usr/bin/env java --class-path assertj-core-3.9.1.jar --source 11 
import static org.assertj.core.api.Assertions.*;
public class Assert {
  public static void main(String... args) {
    assertThat(false).isTrue();
  }
}

こちらを実行したらどうなるでしょうか?

f:id:mike_neck:20181208140845p:plain
shebang の中で classpath を指定する

うまく実行できるようです。なお、 shebang の中での --class-path--source の順番を入れ替えてみました。すると…

f:id:mike_neck:20181208141142p:plain
shebang に書く順番を間違えると実行できない

実行できなくなるようです。


JEP-330 の Single-File Source-Code Programs について紹介してきましたがいかがでしたでしょうか。

これまでちょっとしたアプリケーションも javac でコンパイルしなければならなかったので、ずいぶんと便利になりましたね。

もし何かぬけもれがあるようでしたら、ぜひ教えてくださいね。

今回も最後までお読みいただきありがとうございました。

AWS Lambda のカスタムランタイムにて Java のカスタムランタイムで関数を動かす

これは Java Advent Calendar 2018 の 7 日目のエントリーです。

f:id:mike_neck:20181205013811p:plain


Java をラムダで動かす

Lambda SDK を使って、チュートリアルどおりに作成します。以上。


そうではないですね。


今回やりたいのは AWS Lambda のカスタムランタイムで Java のカスタムランタイムで動く関数を動かすことです。これを会社の同僚に言ったところ、「おまえは何を言っているんだ」というような顔をされました。

f:id:mike_neck:20181205010007j:plain
おまえは何を言っているんだ


大事なことなのでもう一度言いますが、「 AWS のカスタムランタイムで Java のカスタムランタイムを動かしたい!」、これが今回のエントリーの目標です。


Java 9 のモジュールシステムと jlink によって、アプリケーションが利用する必要最低限のモジュールだけを選別したカスタムランタイムイメージが作れるようになりました。カスタムランタイムはフットプリントを軽くできるので、ロードする時間も短くなることから多分起動時間も短くなるはずです。そして、ラムダのような呼び出されてから起動を開始するようなモデルに、カスタムランタイムはマッチしているわけで、これを使わないわけにはいきません。


ランタイムの作り方

AWS のドキュメントの以下のページを読むと、大まかな作り方がわかります。また、「AWS Lambda カスタムランタイム」で検索すると、様々な人がすでに挑戦していますので、参考にされるとよいと思います。

docs.aws.amazon.com

docs.aws.amazon.com

ポイントとしては、以下のとおりです。

  • bootstrap という名前のスクリプト(あるいは実行バイナリー)が呼び出される
  • 以下の処理をループで実行する
    • API エンドポイント http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/nextGET でアクセスして、リクエストID と リクエスペイロード(たいていはjson形式)を取得する
    • 関数を呼び出す
    • API エンドポイント http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/リクエストID/responsePOST でレスポンスを返す
    • API エンドポイント http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/リクエストID/errorPOST でエラーを返す

これをざっくり1ファイル/クラスで書くとこんな感じになります。ただし、今回はエラー処理を入れていません。

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class LambdaApp {

  @SuppressWarnings("InfiniteLoopStatement")
  public static void main(String[] args) {
    System.out.println("Start lambda");
    final String awsLambdaRuntimeApi = System.getenv("AWS_LAMBDA_RUNTIME_API");
    if (awsLambdaRuntimeApi == null) {
      System.out.println("error AWS_LAMBDA_RUNTIME_API is not available.");
      System.exit(1);
    }
    System.out.println(awsLambdaRuntimeApi);
    final HttpClient client = HttpClient.newHttpClient();
    System.out.println("client prepared.");
    while (true) {
      final URI uri = URI.create("http://" + awsLambdaRuntimeApi + "/2018-06-01/runtime/invocation/next");
      System.out.println("uri : " + uri);
      final HttpRequest getEvent = HttpRequest.newBuilder(uri).GET().build();
      try {
        final HttpResponse<String> response =
            client.send(getEvent, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        final String requestId =
            response.headers().firstValue("Lambda-Runtime-Aws-Request-Id").orElseThrow();
        final String body = response.body();
        System.out.println(body);
        final String payload = "{\"receive\":" + body + "}";
        final URI resultUrl =
            URI.create(
                "http://"
                    + awsLambdaRuntimeApi
                    + "/2018-06-01/runtime/invocation/"
                    + requestId
                    + "/response");
        final HttpRequest request =
            HttpRequest.newBuilder(resultUrl)
                .POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8))
                .build();
        final HttpResponse<String> result =
            client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        System.out.println(result.statusCode());
        System.out.println(result.body());
      } catch (InterruptedException | IOException e) {
        e.printStackTrace();
      }
    }
  }
}

このクラスを含むプロジェクトには次のような module-info.java をつけました。

module lambda.example {
  requires java.base;
  requires java.net.http;
}

さて、このプログラムで使っているのは上記のモジュールだけですので、 jlink コマンドでそれらを指定して、カスタムランタイムを作ります。

jlink --compress=2 \
    --module-path ${JAVA_HOME}/jmods \
    --add-modules java.base,java.net.http \
    --output lambda-custom-java-runtime

これでカスタムランタイムができました。

ちなみに lambda-custom-java-runtime ディレクトリーの大きさを見てみると 28 MB くらいに収まっているようです。

f:id:mike_neck:20181206235140p:plain
Java カスタムランタイム は 28MB

比較として、比較対象としては適切ではありませんが、通常の JDK の jmods の大きさは 77 MB くらいあるようです。

f:id:mike_neck:20181206235450p:plain
jmods は全部で 77 MB

このアプリケーションを起動するために次のような bootstrap をシェルで組みます。

#!/usr/bin/env bash

JAVA=./lambda-custom-java-runtime/bin/java

${JAVA} -p java-custom-runtime.jar -m lambda.example

たぶん、これで動くはずですが、普段は Mac を使っている僕はここから先が大変です。


ビルド

Mac でビルドしたクラスファイルは特に問題がありませんが、Mac 用の java では Linux を使っている Lambda 上では動かせません。したがって、ビルドは Linux 上でやることになります。

今回は最初 Docker でやろうとしたのですが、(typo していてうまく動かせなかったため) Amazon Linux 上でビルドしようと考え、 ec2 に立てた Amazon Linux 上でビルドしました。

その際に、次のコマンドでカスタムランタイムを作ります。

MODULES=$(jdeps --list-deps build/libs/java-custom-runtime.jar | tr "\n" "," | tr -d [:space:])

jlink --compress=2 \
    --module-path ${JAVA_HOME}/jmods \
    --add-modules ${MODULES} \
    --output build/mod/lambda-custom-java-runtime

リリース/デプロイ

次のように、成果物をあつめて、 zip で固めます。

bootstrap
java-custom-runtime.jar
lambda-custom-java-runtime
zip lambda.zip bootstrap java-custom-runtime.jar
zip -r lambda.zip lambda-custom-java-runtime

あとは、 aws コマンドを叩くだけです。

aws lambda create-function \
    --function-name java11-custom-runtime \
    --runtime provided \
    --role 適切なロールを見繕って指定 \
    --zip-file fileb://lambda.zip \
    --handler hogehoge #今回は汎用的な仕組みではないので、ハンドラーの名前は適当

今回は単なるオウム返しするだけの関数なのでテストデータもデフォルトの json を使います

f:id:mike_neck:20181205012527p:plain
適当なデータ

次の通り実行されました

f:id:mike_neck:20181205012656p:plain


以上 Java のカスタムランタイムにて Java のカスタムランタイムで関数を動かしました