mike-neckのブログ

Java or Groovy or Swift or Golang

JJUG CCC 2018 Spring にて発表しました #jjug_ccc

表題の通り 2018/05/26 開催された JJUG CCC にて発表してきました。

www.java-users.jp


セッションが1つとLTが1つです。

セッションの方は 「(俺が)はじめての Netty」 で、LT のタイトルは「Spring Boot アプリケーションの起動を速くする 108 の Tips」です。

資料はこちらです。

  • セッション資料

www.slideshare.net

  • LT 資料

www.slideshare.net


題材についてですが、なぜいまさら Netty を選んだか?ですが、僕はこれまで Netty と聞くと「速いサービスで使われている I/O のライブラリーね、なるほどね(理解していない)速いよね(理解していない)」という感じで、技術者としては思考停止に陥っていたので一発奮起して、かつ勉強がてら発表してみようと思って CFP を書きました。

CFP を書く頃はまだチュートリアルを一通りやって、少しだけ Channel とか ChannelHandlerByteBufjavadocを軽く読んだだけの状態で、このあたりを押さえればよいだろうと言う感じで CFP を書いていました。

ただ、実際にその程度の理解では何も発表資料が書けないことが見えてたので、 Netty In Action を購入して、 Comprehensive な知識を習得することにしました。そして、 2週間程度で Netty In Action は読み終わったのですが、その結果として僕はまだ java.nio.channels(NIO) 周りのAPI および内部実装をちゃんと理解できていないことに気づきました。さらには NIO が解決している課題をちゃんと説明できないのではないかという思いも生じていました(C10K 含めて知識としてはあっても、それを初級者がわかるように説明できないのではないか)。そこでもう少し低レイヤーのシステムコールも含めて勉強しました。そのうえで、OIO/NIO の知識を構築し直すと、Netty が何を解決しようとしているのかがわかるようになってきました。また、 NIO が抱える諸問題(といっても大したあれではない)を Netty はどのように解決するか描き出すのが良さそうなことがわかったので、これが最終的な資料の原型になったと思います。

また Netty の内容の説明を全面に押し出そうとおも思っていたのですが、レベル設定を初心者向けにしていたこともあるので、NIO の仕組みを確実に抑えた方が理解に役立つ(むしろ Netty の説明だけしてたらポカーンされる)と考えたので、半分ほどを NIO の話になっています。本当はもっと応用っぽいこともやってみたかったし、その準備もしてました(WebFlux 〜 reactor-netty のあたりも調べてた)。

以上がセッション資料を書くときに考えてたことです。

発表前に まーや さんが面白いことを言ってくれたので会場の雰囲気が軽くなりました。非常に有難かったです。

発表終了後にいくつか質問をいただきました。

  • サーバーでコネクション(ペイロード長)の設定できるのか?
    • NIO の場合、 ServerSocketChannel#<T>setOption(SocketOption<T>, T) で設定します。 SocketOption<Integer> として StandardSocketOptions#SO_RCVBUF を使います。
    • Netty の場合、 ServerBootstrap#<T>childOption(ChannelOption<T>, T) で設定します。 ChannelOption<Integer> として ChannelOption#SO_RCVBUF を使います。
  • Http で Multipart で大きなファイルを受け取る場合の実装方法
    • netty example の http あたりが参考になります -> github.com
  • Netty の http モジュールでサーブレット相当のことができるか?
    • 残念ながら、そこまではできないので、別途 reactor-netty あるいは Spring WebFlux などの上位のフレームワークを使うのがよいです

LT 発表ですが、発表待ちの最中に セバスチャン が突然僕の前でLTすることになって、若干プレッシャーがかかりました。しかし、彼のおかげで LT を聞こうとする人が増えたのでラッキーな発表順だと思いもしました。

LT の方は以前 jsug で発表した内容の焼き直しです。jsug は Spring の勉強ということもあるので、技術的にどうしてこれをやると速くなるのかという説明を入れていましたが、CCC の LT ではそのあたりは省きました。懇親会なのでお酒入っていて理解できないだろうし、ここ数回ほどJJUG CCCでLTをして得ている印象では、そのような説明よりもっとわかりやすい部分を長く強く説明したほうが笑いを取れるだろうと考えたからです。なお、結果的に笑いがとれたので、僕的には満足です。

ちなみに、このようなお言葉をいただきました。

@SpringBootApplication になった人」、いい表現ですね。 じゅくちょー ばりにそれで売っていこうかな…でも、 「俺自身が @SpringBootApplication になる」の Spring Boot アプリケーションを書くのが非常に面倒なのでどうしようかな…


その他

  • 1本しか持っていないコネクターを会社で固定で使っている状態なので ゆうすけ さんと 速水 さんにコネクターをお借りしました。大変助かりました。
  • 某お嬢さんにお会いしました。ここ1年とちょっとほどツイートしていなかったので、某有名企業やめて、Javaもやめて、何かすごいよい条件の会社で働いているのかな?ひょっとしたらドラム缶にコンクリートで埋められて東京湾に投げ込まれてないかなとか心配しましたが、特に変わらず活躍されているご様子で安心しました。
  • 職業が自身のお名前でもある 戸倉彩 さんにご挨拶できました。

Java Day Tokyo 2018 のセッションメモ

Java Day Tokyo 2018 に行ってきたので、そのメモ。


キーノート

目黒駅で東京駅南口行きバスのりばを探すのに時間がかかってしまったため、20分遅刻してしまった。

僕が参加したときには Java の新しいリリースモデルの説明。

OpenJDK は 半年間しかサポートしないので、LTSが欲しい場合は Oracle または他のベンダーに頼る感じと思われる。


Updates from the JCP Program

トラブルで最初の10分くらい何を言っているかわからなかった(通訳レシーバーが故障してた)。

JCP/JSR がどういったもので、 Java に貢献するにはどうしていくかという話だった


Project Vallhala

Value Type というプリミティブをフィールドとするデータ型を定義するようにして、その配列が効率的になるようにメモリの持ち方を工夫するというプロジェクト。

これをさらに進めると、ジェネリクスにプリミティブ型を指定できるようになり、 Stream<Integer> とか IntStream ではなく、 Stream<int> のように扱えるようになる。ただ、型の変位については問題があるようで、 int が継承しているクラスは一体何なのかなどの整理が必要である様子。

また、 Unsafe を置き換えるもので VarHandles があるが、それらの概要を示した上で、 JJUG CCC でそのセッションをやるとのことだった。


Curing you Domain Model Anemia with Effective & Clean Tips from the Real World

どういったモデルを書いていけば、集約度の高いクラスが書けるか的な話。DDD好きにはたまらないやつ。

JPAのエンティティを例にサンプルを書いていってたが、僕が普段意識して書いているコードに近いものになってた。

  • コンストラクターではなく、スタティックファクトリーメソッドを提供する
  • エンティティクラスにはデフォルトコンストラクターだけではなく、フィールドに値を突っ込めるコンストラクターも提供する(そのコンストラクターはファクトリーメソッドからのみアクセスする)
  • エンティティクラスの中で特定の値域をもつようなフィールドは、プリミティブな値をそのまま持つのではなく、 Embeddedable クラスを作ってそちらで検証する
  • コレクションフィールドのゲッターは不変(Collections.unmodifiableList() など使って)にしたコレクションを返す。値の追加はこのクラスの中でのみおこなう

Java SE10/11 移行ガイド

すごい混んでたセッション。引っ込み思案なため、席がとれなくて、床に座ってセッションを聞いてた。

雑なまとめ

  • @Deprecated になっている API は使うな。@Deprecated(forRemoval = true) となっているものはまじでなくなるので使うな
  • --add-exports + ALL-UNNAMED/--add-opens などコンパイルオプションが多い
    • モジュール周りはドキュメント自分で読まないと詳しくなれなそう
  • 一つのパッケージを複数のモジュールに組み込めないので、 現在 Java SE にある JAXB/JAX-WSは取り除かれる。使いたい場合は Java EE から提供されているものを使うこと
    • ただ、 javax.activationmaven central にもないので、自分でパッケージングが必要になりそう…
  • ライブラリーを作っている人は META-INF.MFAutomatic-Module をつけてくれ。でないと、jar ファイルの名前がモジュール(Unnamed-Module)になるのでバージョンが変わるたびにビルドが壊れる

たぶん、このメモ読んでも移行できないので、資料を見たほうがよいかもしれない


Project Loom

軽量プロセス Fiber についてのセッション。今回もっとも面白かったセッション。

  • FiberJava の runtime やユーザーコードに支配されるスレッドで、 Thread よりもコストが低く、 OS のスレッドの 1000倍の個数を扱える
  • サーバーなどのリソースの活用の仕方をモニターすると、 セッションの生存期間が長くて数も増えるが、殆どの時間が I/O 待ちでリソースが有効活用されていない
  • 簡単だが性能がよくない同期モデルのプログラミングにするか、複雑になるが高性能の非同期プログラミングモデルにするかの選択しかなかったが、 Fiber を用いると高性能で簡単なモデルのプログラミングが出来るようになる
  • Fiber をこれまでの Thread の上に作るか、 Thread の parent インターフェースとして Strand を作り、 Fiber インターフェースを Strand を継承したものにするか、どちらかがAPI の候補となっている
  • Fiber を使うと、JAX-RS のコードのスループットが5〜10倍になる
  • もしすでに非同期で書いていたら…それを同期に戻すような修正が必要になるかもしれない
  • Fiber を使うと、golang のチャネルや Python のジェネレーターのようなものが作れるようになる
  • Fiberシリアライズして別のJVMに送ってそちらで計算させることができる(coherence のようなデータがある所で処理させるような用途が可能/超長時間のトランザクションなども実現できる)
  • 課題: Fiber に対応させるために既存の java.netjava.nio 周りを書き直さないといけない

あまり興奮を伝えられていないが、本当に Fiber 最強やんけという感想しかなかった

CompletableFuture の thenApplyAsync メソッドは何度も呼び出せるのか試してみた

最近、自分専用のライブラリーを書いていて、 CompletableFuture#thenApply および CompletableFuture#thenApplyAsync などを複数回呼び出せるのかがわかっていなかったし、 Javadoc にも書いていないようなので実験してみることにしました。


書いたのは次のようなコードです。

@Test
void handlers2() throws InterruptedException {
  final CountDownLatch latch = new CountDownLatch(1);
  final CompletableFuture<String> future =
      CompletableFuture.supplyAsync(
          () -> {
            sleep();
            // ランダムで結果を返す
            // 成功の場合は foo を返す
            // 失敗の場合はRuntimeExceptionが発生
            return Result.random().apply("foo");
          },
          executor);

  // CompletableFuture#thenApplyAsync を2回呼び出す
  final CompletableFuture<String> f1 =
      future.thenApplyAsync(str -> String.format("result -> %s", str));
  final CompletableFuture<String> f2 =
      future.thenApplyAsync(
          str -> {
            throw new RuntimeException(str);
          });

  // 結果を文字列で表示するためのハンドラー
  final CompletableFuture<String> hf1 =
      f1.whenCompleteAsync(
          (str, th) ->
              handle(str, th)
                  .onSuccess(s -> System.out.println(String.format("handler 1 -> %s", s)))
                  .onError(
                      e ->
                          System.out.println(
                              String.format(
                                  "handler 1 error -> %s", e.getClass().getSimpleName()))),
          executor);
  final CompletableFuture<String> hf2 =
      f2.whenCompleteAsync(
          (str, th) ->
              handle(str, th)
                  .onSuccess(s -> System.out.println(String.format("handler 2 -> %s", s)))
                  .onError(
                      e ->
                          System.out.println(
                              String.format(
                                  "handler 2 error -> %s", e.getClass().getSimpleName()))),
          executor);
  CompletableFuture.allOf(hf1, hf2).whenComplete((s, t) -> latch.countDown());
  latch.await();
}

結果は次のようになりました

handler 1 -> result -> foo
handler 2 error -> CompletionException

というわけで、同じ CompletableFutureインスタンスから複数の CompletableFuture を派生させることが可能とわかった


すると次に気になるのは、 CompletableFuture の結果が返ってきてから、 thenApplyAsync を呼び出せるのかという問題

先ほどのメソッドを次のように書き足してみた

@Test
void handlers2() throws InterruptedException {
  // CountDownLatch を追加
  final CountDownLatch finalLatch = new CountDownLatch(1);

  // ... 先ほどのメソッド

  latch.await(); // 先ほどのメソッドの最後の行

  final CompletableFuture<String> f3 =
      future.thenApplyAsync(str -> String.format("after completed: %s", str));
  final CompletableFuture<String> hf3 =
      f3.whenCompleteAsync(
          (str, th) ->
              handle(str, th)
                  .onSuccess(s -> System.out.println(String.format("handler 3 -> %s", s)))
                  .onError(
                      e ->
                          System.out.println(
                              String.format(
                                  "handler 3 error -> %s", e.getClass().getSimpleName()))),
          executor);

  hf3.whenCompleteAsync((s,t) -> finalLatch.countDown());
  finalLatch.await();
}

実行結果はこちら

handler 2 error -> CompletionException
handler 1 -> result -> foo - foo
handler 3 -> after completed: foo - foo

で、 complete した状態の CompletableFuture に対して、関数を適用することも可能だということがわかった。

Component の インデックススキャンでどれくらい速くなるか計測してみた #jsug

JSUG のLT会で 『Spring Boot アプリケーションの起動をほんの少し気持ちだけ速くしてみた』というタイトルでLTしてきたところ、 Spring 5 から @Component のインデックススキャンがサポートされるようになったことを教えてもらったので、試してみました。


追加するライブラリー

以下のとおり、アノテーションプロセッサーにライブラリーを追加します。

  annotationProcessor('org.springframework:spring-context-indexer')

あとは普通にビルドするだけです。

なおビルドすると、 /META-INF の下に spring.components というファイルができます

中身はこんな感じです(一部抜粋)

#
#Wed Apr 18 23:17:29 JST 2018
com.example.jpa.repository.baz.BazRepository=org.springframework.stereotype.Component,org.springframework.data.repository.Repository
com.example.jpa.entity.def.DefEntity=javax.persistence.Entity,javax.persistence.Table
com.example.jpa.repository.waldo.WaldoRepository=org.springframework.stereotype.Component,org.springframework.data.repository.Repository
com.example.App=org.springframework.stereotype.Component
com.example.jpa.entity.garply.GarplyEntity=javax.persistence.Entity,javax.persistence.Table

起動時間の比較

では、早速比較してみます。

  • slow というプロジェクトが何もしないままのプロジェクト
  • indexed というプロジェクトが slow プロジェクトをコピーしてインデックススキャンを有効にしたプロジェクト

次のスクリプトを実行します

#!/usr/bin/env bash

set -e

./gradlew slow:clean slow:bootJar indexed:clean indexed:bootJar

echo slow
java -jar slow/build/libs/slow-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"
echo indexed
java -jar indexed/build/libs/indexed-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"

echo slow
java -jar slow/build/libs/slow-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"
echo indexed
java -jar indexed/build/libs/indexed-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"

echo slow
java -jar slow/build/libs/slow-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"
echo indexed
java -jar indexed/build/libs/indexed-0.0.1-SNAPSHOT.jar 2>/dev/null | grep "JVM running for" | sed -e "s/^.*Started App//g"

実行結果は次のようになりました

BUILD SUCCESSFUL in 4s
10 actionable tasks: 10 executed
slow
 in 6.723 seconds (JVM running for 7.572)
indexed
 in 6.012 seconds (JVM running for 6.875)
slow
 in 6.059 seconds (JVM running for 6.889)
indexed
 in 6.129 seconds (JVM running for 6.996)
slow
 in 6.057 seconds (JVM running for 6.874)
indexed
 in 6.025 seconds (JVM running for 6.924)
  • 何もしない場合の起動時間 : 6.280 sec
  • インデックスつけた場合の起動時間 : 6.055 sec

という感じで、少し速くなりました。が、2回目などは遅くなっている場合もあり、もう少し大きめなプロジェクトで試してみたい気もします