Play2(Java)でJavaから意地でPlay2(Scala)の機能を使う
Play framework 2.x Java and 1.x Advent Calendar 2013 - Adventarの17日め3回目のエントリです。
Play2(Java)は巷で言われているほどつらくないよ、みたいな話は何度かしましたが、それでもPlay2(Scala)で提供されているすべての機能がJava向けに提供されているわけではないので、やはりScalaに比べて劣るところはあると思います。*1
しかし考えてみるとJavaもScalaも結局はJVM上で動く言語です。
ということはJavaからScalaのAPIを直接呼び出してやればJava用に提供されていない機能を利用することができるしPlay2Javaの弱点を補うことも出来るはずです。
関数、trait、シングルトンオブジェクト
ScalaにあってJavaにない概念といえば上記の3つが思い当たります。これらをJavaから扱う方法がわかれば大体の事は出来そうです。
関数
関数はJavaから見るとFunctionXインタフェースとして見えます。じゃあこのインタフェースの無名クラスを実装すれば楽ちんじゃん、と思うのですが、素直にやってみると何かよく分からん関数の実装を要求されることと思います。
new Function1<String, Integer>(){ @Override public void $init$() { } @Override public Integer apply(String v1) { return null; } @Override public <A> Function1<A, Integer> compose(Function1<A, String> g) { return null; } @Override public <A> Function1<String, A> andThen(Function1<Integer, A> g) { return null; } };
scala.runtimeパッケージにAbstractFunctionX系のクラスがあり、これはFunctionXと継承関係にあり、applyメソッドの実装だけを要求されます。
new AbstractFunction0<String>() { @Override public String apply() { return null; } }
関数オブジェクトの実装にはこちらを使うのが良さそうです。
trait
traitは実装付きのインタフェースのようなもので、Javaからみるとインタフェースのように見えます。
が、traitは実装を含む場合があり、実際普通にimplementsしたり無名クラス化したりすると、Scalaコード的には実装が書いてあるはずなのにJava側からでは実装しろと怒られる状況になるかと思います。
new Filter(){ @Override public void $init$() { //scalaコード見ると実装はある } @Override public Future<SimpleResult> apply(Function1<RequestHeader, Future<SimpleResult>> f, RequestHeader rh) { //実装しなきゃいけないメソッドはここだけのはずなのに…。 return null; } @Override public EssentialAction apply(EssentialAction next) { //scalaコード見ると実装はある } };
どうやらJavaから見た時、traitの振る舞いは「{トレイト名}$class」と言う名前の別のクラスに切りだされているみたいです。
以下のようにするとScalaのtraitとJavaの世界を橋渡しできます。
public abstract class JavaFilter implements Filter { //何故かoverrideアノテーションつけると怒られたので外した public void $init$() { Filter$class.$init$(this); } @Override public EssentialAction apply(EssentialAction next) { return Filter$class.apply(this, next); } }
※追記
id:xuwei さんにコメントして頂いたように、実際には実装を持つtraitをJavaから使う場合Scala側で抽象クラス化する方がはるかに楽ですのでそうしましょう。
シングルトンオブジェクト
Scalaの世界でobjectキーワードを利用して宣言できるシングルトンオブジェクトは、上記のtraitと同じようにある命名規則を持った別のクラスとして定義されます。
それが「{クラス名}$」と言う命名規則です。
シングルトンオブジェクトなのでこのクラスをnewすることは出来ず、実際にはこのクラスのシングルトンインスタンス経由でオブジェクトのメンバへアクセスすることになります。
シングルトンインスタンスは「MODULE$」と言う名前になっており、とどのつまり、あるオブジェクトのメンバにJavaからアクセスするには「{クラス名}$.MODULE$.hogehoge()」のようにします。
フィルタを実装する
サンプルコードでバレている感じがありますが、Play2(Scala)で提供されていてJava向けにAPIが定義されていない機能のうちのひとつにFilterがあります。*2
先ほど紹介した方法を使うことでJavaからでも任意のフィルタを定義することが出来ます。
で、実装したのが以下。リクエストの処理にかかった時間をログに吐き出すフィルタです。
// JavaFilter.java import play.api.mvc.*; public abstract class JavaFilter implements Filter { public void $init$() { Filter$class.$init$(this); } @Override public EssentialAction apply(EssentialAction next) { return Filter$class.apply(this, next); } } // TimeMeasuringFilter.java import play.api.Logger; import play.api.libs.concurrent.Execution$; import play.api.mvc.RequestHeader; import play.api.mvc.SimpleResult; import scala.Function1; import scala.concurrent.Future; import scala.runtime.AbstractFunction0; import scala.runtime.AbstractFunction1; public class TimeMeasuringFilter extends JavaFilter { private final Logger logger = Logger.apply(TimeMeasuringFilter.class); @Override public Future<SimpleResult> apply(Function1<RequestHeader, Future<SimpleResult>> next, RequestHeader rh) { final long start = System.currentTimeMillis(); logger.info(new AbstractFunction0<String>() { @Override public String apply() { return "start: " + start + "(ms)"; } }); return next.apply(rh).map(new AbstractFunction1<SimpleResult, SimpleResult>() { @Override public SimpleResult apply(SimpleResult result) { final long end = System.currentTimeMillis(); logger.info(new AbstractFunction0<String>() { @Override public String apply() { return "end: " + (start - end) + "(ms)"; } }); return result; } }, Execution$.MODULE$.defaultContext()); } } //Global.java public class Global extends GlobalSettings { @Override public <T extends EssentialFilter> Class<T>[] filters() { return new Class[]{ TimeMeasuringFilter.class }; } }