the why and how of java8 at line fukuoka
TRANSCRIPT
The Why and How of Java8at LINE Fukuoka
@youhei
Today's hashtag: #LINE_DM
⾃⼰紹介$ whoami新⽥ 洋平2014年9⽉⼊社LINE ファミリーアプリサーバーサイド開発担当ちょっと前は Python や AWS と戯れてましたtwitter: @youhei他もだいたい youhei
今⽇これから話すことLINE Fukuoka の現状なぜ Java を使うようになったかなぜ Java8 を選んだのかどうやって Java8 で開発しているか実際使ってきてどうだったか
LINE Fukuoka の現状
福岡で作ってる LINE Family Apps
LINE 占いLINE MALL
LINE Creaters Market
サーバサイドは全部 PerlLINE Family App は Perl が主⼒です
2014/9 ⼊社間も無い頃「Java でやるプロジェクトがあるので、
youhei さんやってみます?」
Java?
Perl じゃないの?
ということで
六年ぶりにJava はじめました
そもそもなぜ Java なのかWhy we use Java?
まずは社内の状況確認
確認結果弊社 GitHub:enterprise で⼈気最上位の⾔語は Java
Spring を使った Java のプロジェクト多数LINE バックエンドでも⼤活躍Sonatype Nexus でライブラリを管理Jenkins もガンガン使ってる
めっちゃ Java 使ってた
知らなかっただけでめっちゃ Java の会社でした
じゃあなぜ Java 8 なのかWhy we choose Java 8?
プロジェクトアサイン当初つくるもの: JSON-RPC Server
ライブラリの選択は⾃由運⽤上の制約さえ守れば良くしがらみも少ない「よし、」
「できる限り新しいものを使おう」
なぜ新しいもの?
2006 年頃のJava から LL への流れ「Java だとさくさく作れないから LL へ」
元の⽊阿弥にならないために「さくさく作れる Java」でないといけな
い「重厚⻑⼤」は NG
社内をみるとJava 8 で先⾏している Project があった「Java で 1 から 10 まで書いた話」参照Perl をずっと書いてきた⼈にも馴染みや
すい Java
これはすごい
乗るしかないこのビッグウェーブに
その結果、
現在の構成はこうなりました
使っているモジュールavans - Tiny thin web application framework for Java 8
webscrew - Web application toolkit for Java servlet
tinyorm - O/R mapper for Java 8
tinyvalidator - Tiny validation framework
mech2 - HTTP client
jackson - Json parsing and generation
lombok - Reduce boilerplate code
緑字は社内に Author がいる OSS, ⿊字はそうでない OSS
構成図
この構成で
どうやって Java8 で開発しているか
How we use Java8?
よく使う Java 8 の新機能1. Optional2. lambda3. default method4. Stream API
よく使う Java 8 の新機能1. Optional2. lambda3. default method4. Stream API
Optional値が含まれている場合も
含まれていない場合もあるコンテナ・オブジェクト
コンテナ・オブジェクト配列, List, Map などのデータ構造の総称
ランタイムに null 参照をさけるためのコンテナが
Optional
Optional の基本「値がないかも」を明⽰して NullPointerException をさける
チェック漏れはコンパイラがチェックする
// 値がないと nobody// Optional<String> maybeNameString name = maybeName.orElse("nobody");// 値があると someMethod(d) を処理// Optional<T> datadata.ifPresent(d -> someMethod(d));
Real World Exampleavans, tinyorm のコード例
avans で Query Parameter が任意かどうかを明⽰する
利点はコード上に仕様が明記される、デフォルト値の考慮漏れもなくなる、の⼆点。
@GET("/api/items")public WebResponse list(@Param("page") OptionalInt page) { int pageNumber = page.orElse(1); // デフォルト値は 1
tinyorm で単⼀⾏の削除SELECT FOR UPDATE した結果がある場合のみ DELETE を発⾏
TinyORM db = TinyORM(connection);db.single(ItemRow.class) .where("id=?", id) .forUpdate() .execute() // Optional<ItemRow> を返す .ifPresent(row -> row.delete()); // ItemRow がある場合だけ DELETE!
Optional まとめ安全なコードが書けるようになるコードのドキュメント性があがるWeb層、DB層の API が対応しているとより強⼒
よく使う Java 8 の新機能1. Optional2. lambda3. default method4. Stream API
ラムダ式は⼀種の糖⾐構⽂
匿名クラスのインスタンス⽣成の
コードを置き換えられるイメージ
ただ匿名クラスのインスタンス⽣成の糖⾐構⽂ではない。
ラムダ式は匿名クラスとは異なるバイトコードを吐くinvokedynamic で実⾏時にラムダオブジェクトを⽣成する処理に置き換わる匿名クラスファイル(Foo$1.class)を作らずに済むし、インスタンス⽣成のコストも下がる
ラムダ式の基本
ラムダ式の基本(Java 7 以前)匿名クラスで関数を渡していた冗⻑な時代
辞書順にソートするコード。
// Java 7 以前の⽂法に沿った表現Collections.sort(lists, new Comparator<String>() { public int compare(final String o1, String o2) { return o1.compareTo(o2); }});
ラムダ式の基本(Java 8 ラムダ式)同じコードをラムダ式で書く。多くの要素を省略できる。
// 省略なしCollections.sort(lists, (final String o1, final String o2) -> { return o1.compareTo(o2); });// 処理が1⾏だとコードブロックの波括弧と return は省略可Collections.sort(lists, (final String o1, final String o2) -> o1.compareTo(o2));// 型推論で引数の型宣⾔を省略可。final の明⽰はできなくなる。// (引数が⼀個の場合、引数の括弧も省略可)Collections.sort(lists, (o1, o2) -> o1.compareTo(o2));// メソッド参照Collections.sort(lists, String::compareTo);
実質的final匿名クラスではエンクロージングクラス(匿名クラスを宣⾔しているクラス)の変数が final でない場合はアクセスすることは⾔語仕様上できなかった。ラムダ式とこの仕様は相性が良くない。そこで、ラムダ式でエンクロージングクラスの変数を利⽤した場合はその変数を「実質的 final」と定義した。その変数は final が付いているものとしてみなされる。変数に再代⼊してしまうと実質的 final ではなくなるのでラムダ式で使おうとするとコンパイルエラーになる。
実質的final(コード例)familyName に値を代⼊するとコンパイルエラー。知らないと⼤変ハマる。
ラムダ式内での値の変更もコンパイルエラー。
String familyName = "Isono"; // 実質的 finalfamily.forEach( firstName -> { System.out.println( String.format("%s %s", firstName, familyName) });// familyName = "Fuguta"; このコメントを外すとコンパイルエラー
以上がラムダ式の⾔語仕様ちょっと覚えることが多い
Real World Exampleavans, tinyorm のコード例
avans で CSV を返すHttpServletResponse に依存した処理をラムダ式内で扱う。
CallbackResponse のラムダ式は csv() 内ではなく avans に処理を返してから遅延実⾏される。
@GET("/csv")public WebResponse csv() { return new CallbackResponse(resp -> { resp.setContentType("text/csv; charset=UTF-8"); resp.setHeader("Content-Disposition", "attachment;filename=my-file-name.csv"); CSVFormat format = CSVFormat.EXCEL; try (Writer writer = resp.getWriter(); CSVPrinter csvPrinter = new CSVPrinter(writer, format)) { csvPrinter.printRecord( Arrays.asList("こんにちは", "世界")); } });}
tinyorm で 複雑なクエリをマッピングするGROUP BY したり JOIN する複雑なクエリは SQL を直接書くポリシー。
ResultSet をラムダ式内で扱う。
db.executeQuery( "SELECT count(*) as cnt FROM item GROUP BY group_id", rs -> { List<Long> result = new ArrayList<>(); while (rs.next()) { result.add(rs.getLong("cnt")); } return result; });
lambda まとめ特殊な処理をシンプルに局所化できる(関⼼の分離)
リソース解放を意識しないコードを強制できるWeb層、DB層のライブラリが対応してるとより強⼒
よく使う Java 8 の新機能1. Optional2. lambda3. default method4. Stream API
default method の基本interface に default 実装を定義できるようになった。さらっと扱われがちだけどとても⼤きな変更点。
interface Person { String getFirstName; String getFamilyName; default String getFullName() { return String.format("%s %s", getFirstName(), getFamilyName()); }}
interface static methodstatic method も定義できるようになった。
Collections, Paths のようなユーティリティメソッドを集めたコンパニオンクラスを定義する理由は「これまでの慣習を尊重する」以外になくなった。
Collection, Path に static method を定義可能になったため。
interface Person { static Person of() { return new DefaultPerson("Foo", "Bar"); }}
Real World Exampleavans のコード例
avans plugin を実装するavans の plugin は Controller への Mix-in となっている。default method で実装する。
interface は状態を持てないのでその部分を PluginStash として avans が提供している。
// avans pluginpublic interface SessionMixin extends Controller { static final String STASH_KEY = "session"; public default WebSessionManager getSession() { final Object session = this.computePluginStashValueIfAbsent( this.getClass(), STASH_KEY, () -> { return this.buildSessionManager(); }); return (WebSessionManager) session; } // 以下、省略// Controllerpublic class FooController implements SessionMixin {
default method まとめinterface に実装を持てることで継承で実装を追加できる必ずしも「継承よりコンポジション」ではなくなったMix-in は Annotation と並んでフレームワークが拡張性を提供する優れた⼿段
よく使う Java 8 の新機能1. Optional2. lambda3. default method4. Stream API
Stream API とは
Collection を宣⾔的に操作できる API
ここまで出てきたOptional
lambda
default method
を駆使している
Stream API の基本 - 従来の命令型スタイル価格が20ドルより⼤きい場合は10%引きして値引きした商品の⾦額合計を計算
public static void main(final String... args) { int[] prices = {10, 20, 30, 40}; int totalOfDiscountedPrices = 0; for(int price : prices) { if (price > 20) { totalOfDiscountedPrices = totalOfDiscountedPrices + (int) (price * 0.9); } } System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);}
Stream API の基本 - 関数型スタイル同様の計算を関数型スタイルにする
public static void main(final String... args) { int[] prices = {10, 20, 30, 40}; int totalOfDiscountedPrices = Arrays.stream(prices) // IntStream に変換 .filter(price -> price > 20) // 20 を超えるものを抜き出す .map(price -> (int) (price * 0.9)) // 0.9 掛けした値に変換 .sum(); // 合計する System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);}
⼀時的な値が不要で再代⼊がない
宣⾔的で間違いも起こりにくい
ただ、必ずしも短く書けるわけでもない
Stream API のインパクトは意外と⼩さいStream を API で受け渡すことは少ないメソッド内の処理がシンプルになるのみ
Stream API と拡張 for ⽂の関係Stream API の出現で「forEach を使えば for ⽂不要」という話題もあるが lambda の中でチェック例外を扱うよりは、拡張 for ⽂にしてチェック例外はフレームワークに任せた⽅が可読性が⾼いこともあるWeb アプリはチェック例外も Internal Server Error で処理しておしまい、というケースが多いので、現在は Stream API ⼀辺倒にはならずに拡張 for ⽂も使ってる
Stream API まとめ型安全にある程度すっきり書ける(LL よりは冗⻑だけど)
宣⾔的に書けるのでバグを埋め込みにくいとはいえ拡張 for ⽂も使ってる
以上が Java 8 の新機能を使ってみての所感
ただ、
もっとシンプルさが欲しい
そこで
http://projectlombok.org
with lombokimport lombok.NonNull;public class NonNullExample extends Something { private String name; public NonNullExample(@NonNull Person person) { super("Hello"); this.name = person.getName(); }}
Vanilla Javaimport lombok.NonNull;public class NonNullExample extends Something { private String name; public NonNullExample(@NonNull Person person) { super("Hello"); if (person == null) { throw new NullPointerException("person"); } this.name = person.getName(); }}
アノテーションで退屈なコードを減らす
そもそもなぜこんなことが可能か
lombok の仕組み
通常のコンパイルの流れjavac や Eclipse Compiler for Java がソースコードを解釈して抽象
構⽂⽊を⽣成し、バイトコードにコンパイルする
1. ソースコード(.java)2. 抽象構⽂⽊3. バイトコード(.class)
lombok の仕組みコンパイル時に AnnotationProcessor を利⽤して取得した抽象構⽂
⽊を変換している。internal な API を呼んでいるとか。
1. ソースコード(.java)2. 抽象構⽂⽊抽象構⽂⽊ ← ココを横取りして書き換え3. バイトコード(.class)
この仕組みは何が嬉しいか?
ソースコード⽣成と違って取り扱いがラク
良くも悪くも痕跡が残らない
バイトコード変換と違って実⾏時に魔法はない
よく使う Annotation1. @Data2. @Value3. @SneakyThrows
@DataJavaBeans の getter/setter, toString, hashCode, equals を⽣成する。
Jackson で受け渡しする JSON オブジェクトなどに多⽤してる
@Datapublic class ContactForm { @Email // tinyvalidator private String email; @NotNull // tinyvalidator private String body;}
@Value@Data の不変オブジェクト版(setter が⽣成されない)。
java.beans.ConstructorProperties でアノテーションされたコンストラクタを⽣成する。TinyORM は ConstructorProperties を解釈するため、Row を不変オブジェクトにできる。
@Value@Table("contact") // tinyorm@EqualsAndHashCode(callSuper = false) // 継承先を考慮しないpublic class ContactRow extends Row<ContactRow> { @Column // tinyorm private String email; @Column private String body;}
@SneakyThrowsチェック例外を⾮チェック例外にして投げる。
Internal Server Error を返すしかないような例外はチェックせず SneakyThrows を使う。ただ最近は SneakyThrows を使わず throws 節を宣⾔するのがトレンドになってきた。
@POST("/upload")@SneakyThrowspublic WebResponse upload(@UploadFile("file") Part file) { String body = IOUtils.toString( file.getInputStream(), "UTF-8"); return this.renderText(body);}
lombok.valfinal なローカル変数を短く宣⾔できる。使おうかと思ったが、IntelliJ IDEA が未対応なので断念。
Eclipse でもエラーになるという噂がちらほら。ただ、今となってはダイヤモンド演算⼦で⼗分とも思う。
import lombok.val;public class Main { public void main(String... args) { // lombok.val val example = new ArrayList<String>(); // diamond operator final ArrayList<String> example2 = new ArrayList<>();
F.A.Q.
Q. テストは?Model のテストと Controller のテストをしてる。
Model のテストは MySQL をそのまま使う。
Controller は tomcat-embed を起動して HTTP Client を使う。
いわゆる end-to-end なテスト。
別サービスへのアクセスは tomcat-embed と avans で Mock を書く。
Q. 静的解析とかしてる?checkstyle, findbugs を使ってる。
jenkins で動かしてる。
Checkstyle は Java8 対応版を社内ビルドして使ってる。
lombok.GetterLazy が FindBugs で怒られる...
Q. 社内に Author がいるWeb Framework の利点とは
?
現場のニーズに完璧にフィットする
必要にかられて作られている。最低限の必要な機能を提供している。
⾜りない機能は Mix-in, 独⾃ Annotation で⾃分たちで拡張できる。
余分なコードがほぼゼロで⾒通しが良い
Spring Boot や Dropwizard より軽量また、どんな汎⽤ micro framework にも使わない機能のためのコード、使わないミドルウェアのためのコードは必ずあるもの。
実装の意図が把握しやすい直接聞けることは良いことだ。
⾮互換の変更が⼊るとすぐアナウンスをもらえる。
Q. avans, tinyorm ⾃体の良さとは?
Java 8 を前提としているサービスのコードが⾃然と Java 8 らしいコードに矯正される。
依存がヒッジョーに少ない
よって外部要因に左右されにくい
最悪⾃分たちでなんとかできる、というビジネス⾯でのメリット。
再び歩み始めた Java の継続的進化に追随しやすい、という技術⾯でのメリット。
覚えることが少ない別のことに時間を使える
Java SE 8 の⾔語仕様を知ったり、Servlet 3.x API を把握したり。良いコードの書き⽅を知ることに時間を割こう。
最後に
これは現時点でのスナップショット
弊社で Java 8 の本格利⽤は始まったばかり
現在もより良い構成をブラッシュアップしてる最中
紆余曲折を現場で体験出来ているのはエンジニアとしてとても幸せ
LINE Family App の Java は始まったばかり
やらなきゃいけないことも、やりたいこともまだまだたくさんだからこそ楽しい
ご清聴ありがとうございました
参考資料LL から Java に移⾏した⼈がはまりがちなことJava で 1 から 10 まで書いた話Java SE 8 実践プログラミングJava による関数型プログラミングJava の lambda の裏事情ProjectLambda・2部(ラムダ式(実質的にfinal・スコープ)〜メソッド・コンストラクタ参照)
Lombokのチュートリアル記事Reducing Boilerplate Code with Project Lombokをテキトーに訳したavans, webscrew, tinyorm, tinyvalidator, mech2