Play 2.4 の Module の作り方と Plugin からの移行について

Play 2.4 では今までの Plugin の仕組みが deprecated となり、 新たに Module という仕組みが導入されています。

Module はこれまた新たに導入された Runtime Dependency Injection の上に乗っかっています。 Play では Guice をデフォルトの DI 実装として利用しますが、 Guice 以外の実装を利用するユーザーもいるだろうということで、 ライブラリとして提供するモジュールについてはそこだけ Guice ではなく、Play 独自の Module という仕組みに沿った形で実装します。

アプリケーションの起動時と終了時にメッセージを表示する Play Module を実装してみましょう。

まずは処理の本体です。

package example

import play.api._
import javax.inject._
import play.api.inject._
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global


@Singleton
class Hello @Inject() (lifecycle: ApplicationLifecycle) {
  lifecycle.addStopHook(() => Future.successful { println("Goodbye") })
  println("Hello")
}

今までの Plugin インタフェースのメソッドを実装する形で実装していた、 Play アプリケーションの起動時の処理は、シングルトンクラスの初期化処理の中で行います。 終了時の処理は injection された lifecycle オブジェクトの addStopHook メソッドを利用して登録します。

これを Play Module として読み込むには play.api.inject.Module を継承して、binding メソッドを実装します。

package example

import play.api._
import javax.inject._
import play.api.inject._
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global

class HelloModule extends Module {
  def bindings(environment: Environment, configuration: Configuration) = {
    Seq(
      bind[Hello].toSelf.eagerly
    )
  }
}

bind の仕方を DSL で記述します。 Play アプリケーション起動時の処理を行うためは、起動時に bind する必要があるので、 .eagerly を使用します。

作った Module を利用するときは application.conf でその Module を有効にします。

play.modules.enabled += "example.HelloModule"

シングルトンオブジェクトなので toSelf で自分自身に bind してしまいましたが、 より一般的にはインタフェースを作ってそれに bind します。

package example

import play.api._
import javax.inject._
import play.api.inject._
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global

class HelloModule extends Module {
  def bindings(environment: Environment, configuration: Configuration) = {
    Seq(
      bind[HelloInterface].to[Hello].eagerly
    )
  }
}

@Singleton
class Hello @Inject() (lifecycle: ApplicationLifecycle) extends HelloInterface {
  lifecycle.addStopHook(() => Future.successful { println("Goodbye") })
  println("Hello")
}

trait HelloInterface

さらに、インスタンスの生成が複雑な場合は Provider を利用することもありそうです。

package example

import play.api._
import javax.inject._
import play.api.inject._
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global

class HelloModule extends Module {
  def bindings(environment: Environment, configuration: Configuration) = {
    Seq(
      bind[HelloInterface].toProvider[HelloProvider].eagerly
    )
  }
}


@Singleton
class HelloProvider @Inject() (lifecycle: ApplicationLifecycle) extends Provider[HelloInterface] {
  lazy val get = new Hello(lifecycle)
}

class Hello (lifecycle: ApplicationLifecycle) extends HelloInterface {
  lifecycle.addStopHook(() => Future.successful { println("Goodbye") })
  println("Hello")
}

trait HelloInterface

Java っぽさが増したので、特に Java の DI になじみのない LL からのユーザーは戸惑うかもしれません。 Runtime Dependency Injection の導入経緯やテストへの影響などは playframework-dev のメーリングリストを読むと面白いかと思います。