Play2(Java) on Java8(体験版)
Play framework 2.x Java and 1.x Advent Calendar 2013 - Adventarの13日目かつ2回目のエントリです。
孤高の戦士の一人です。
Play2もScalaのフレームワークとして一定の認知を得てきた感ありますが、Javaは無いことにされがちで寂しい思いをしています。
確かにScalaだと使える機能が使えなかったりとフレームワークとしての力を十全使えるわけではないのでどちらがより良いかと言われたらScalaなのですが、だからと言ってダメダメかと言われたらそうではないはず、きっと。
とは言え、Scalaの文化もあってか、PlayやPlayが提供するクラスのAPIには関数を引数にとるものがそこそこあって、一応Javaのラッパは提供されているのですが、まぁ、しんどいよね*1、という場面がちょいちょいあります。
ただし、Java8のラムダ式が使えたらこの辺ちょっと世界が変わって見えるんじゃない?と思ったので試してみたよ、というのがこのエントリです。
準備
javaは1.8.0-ea、Playのバージョンは2.2.1を使いました。ただし、このままだとPlayアプリのコンパイルがこけます。
sbtがJava8の新しい命令に対応してないのが原因らしいので、sbtのバージョンを0.13.1-RC2以降に引き上げます。
id:xuwei さん曰く
@kamekoopa URL sbt0.13.1-RC2にしたら動いたりしないですかね?自分も完全に問題点を把握できてないんですが、Scala自体も2.10.0だとJava8対応してなくて、2.10.3では一応対応してるとかだったような
2013-11-13 11:10:55 via web to @kamekoopa
@kamekoopa ただ、sbtのこのコミットでは URL とりあえずエラーが出ない程度に無理やりサポートしただけで、新しい命令は読んで捨てていて、完全にサポートされるのはまだ先のようなので、真面目にやるなら待ったほうがいいかもですが
2013-11-13 11:14:26 via web to @kamekoopa
Promise
Play2では非同期処理を簡単にかけます。
ScalaのFutureをラップしているPromiseというクラスが提供されているのですが、このクラスにこんな感じで関数を渡すことで処理を非同期化出来ます。
import play.libs.F.*; public class Hoge { public void hoge(final int a, final int b) { // なんかしら整数を返す計算 Promise<Integer> integerPromise = Promise.promise(new Function0<Integer>(){ @Override public Integer apply() throws Throwable { return a + b; } }); // なんかしら計算結果を文字列に変換 Promise<String> stringPromise = integerPromise.map(new Function<Integer, String>(){ @Override public String apply(Integer integer) throws Throwable { return "result is " + integer; } }); // 更にその結果を出力 stringPromise.onRedeem(new Callback<String>() { @Override public void invoke(String string) throws Throwable { System.out.println(string); } }); } }
ゴテゴテですね。
これが、Java8だとラムダ式が使えるようになるのでこう書けるようになるわけです。
import play.libs.F.*; public class Hoge { public void hoge(int a, int b) { Promise<Integer> integerPromise = Promise.promise(() -> a + b); Promise<String> stringPromise = integerPromise.map(integer -> "result is " + integer); stringPromise.onRedeem(System.out::println); } }
スッキリ!
実質的finalのおかげで、final修飾子も必要無くなりました。
import play.libs.F.*; public class Hoge { public void hoge(int a, int b) { Promise .promise(() -> a + b) .map(i-> "result is " + i) .onRedeem(System.out::println) ; } }
メソッドチェーンで繋げたら更にスッキリしました。つらくない!
これが出来るようになると、Play2(Java)で非同期処理を扱う際のつらさが激減すると思います。Play2では非同期レスポンスを扱う場合、controllerのメソッドの戻り値をPromise
package controllers; import play.libs.F.*; import play.mvc.*; import views.html.index; public class Application extends Controller { public static Result index() throws Exception { return ok(index.render("Your new application is ready.")); } public static Promise<? extends Result> async() throws Exception { return Promise.promise(() -> { System.out.println("何かの処理"); return ok(index.render("Your new application is ready aync")); }); } }
WebSocket
Play2ではWebSocketも簡単に扱えます。Play2でWebSocketを利用する場合は以下のようにします。
package controllers; import play.libs.F.*; import play.mvc.*; public class Application extends Controller { public static WebSocket<String> webSocket() { // 抽象クラスであるWebSocketのonReadyを実装して返す return new WebSocket<String>() { // クライアントが接続してきた時に呼ばれる @Override public void onReady(final In<String> in, final Out<String> out) { // こんにちは!こんにちは! out.write("hello"); // クライアントから何か入力されてきた時の処理を登録する in.onMessage(new Callback<String>() { @Override public void invoke(String message) throws Throwable { out.write("message coming"); out.write(message); } }); } }; } }
簡単だけど面倒くさそう。
Java8が使えるようになった未来なら、これをもうちょっとスッキリ書けます。
static <T> WebSocket<T> ws(Callback2<WebSocket.In<T>, WebSocket.Out<T>> f) { return new WebSocket<T>() { @Override public void onReady(In<T> in, Out<T> out) { try { f.invoke(in, out); } catch (Throwable throwable) { throw new RuntimeException(throwable); } } }; }
ちょっとしたヘルパーメソッドを定義します。InとOutを引数に受け取って何かする関数fを引数にもらい、WebSocketオブジェクトのインスタンスを返します。受け取った関数をonReadyで実行するように定義しているだけですね。
で、このヘルパーを使ってさっきのメソッドを、Java8で実装すると以下のようになります。
package controllers; import play.libs.F.*; import play.mvc.*; public class Application extends Controller { public static WebSocket<String> webSocket() { return ws((in, out) -> { out.write("hello"); in.onMessage( message-> { out.write("message is coming"); out.write(message); }); }); } }
だいぶ見通しが良くなった気がしますね。*2
OptionとかEither
Play2にはScalaではおなじみのOptionやEitherを模倣したクラスがJava用に提供されています。これらのクラスもScalaよろしく関数を引数にとるメソッドがあるので、これまで同様一層簡素に書けるようになります。
Option<String> opt = Option.Some("1"); opt.map(i -> String.valueOf(i)).forEach(s -> System.out.println(s)); Either<Exception, String> e = Either.Right("1"); e.right.map(i -> String.valueOf(i)).forEach(System.out::println);
Java7まではmapメソッドに無名クラスを放り込む必要があったのでとてもじゃないけど普段使いしたいとは思わない感じでしたが、上記のように書けるなら常用する気も起きそうです。
ただし、Optionに関してはJava8からOptionalクラスが標準に追加されていて、そっちの方が高機能なのでそっち使ったほうがよさそうな気がします。
まとめ
Java8が使えるようになった未来、Play2(Java)アプリはこんな感じで書けるようになるんじゃないかな、というエントリでした。
Scalaがベースになっているお陰で処理を引数にとるAPIがちょこちょこあって、それが、JavaからPlayを使う時のつらみの一因になっている気がします。少なくとも僕はPromiseをmapで繋げたりするとウヒィッってなります。
しかし、Java8ならその手のつらさは大幅に解消されそうです。
Play(Java)、案外アリだと思いますがどうですかね?