harryのブログ

ロードバイクとか模型とかゲームについて何か書いてあるかもしれません

Play Framework 2.5でobjectにDependency Injection(DI)する

  • この記事は 2016年07月03日 にQiitaへ投稿した内容を転記したものです。
  • 本記事は執筆から1年以上が経過しています。

前置き

classが適切な場合は、classを使用してDependency Injectionしましょう

ここから先はobjectに対してDependency Injection(DI)する方法について…

なんでclass使わないの?馬鹿なの?死ぬの?

ちょっと待って!ユースケースの説明をさせてくれ、頼む!

例えばUID *1 生成のtraitを作っておき、その生成ロジックはDIで簡単に差し替えられるようにしておきたい。*2

UID は Rest API(JSON) で受け取ることもあるので、簡単な(もしくは厳密な)validation処理も実装しておきたい。

package utils

trait UID {
  type Value = String
  def generate: Value
  def isValid(id: Value): Boolean
}

object UUID extends UID {
  private val UUIDRegex = """\p{Alnum}{8}(?:-\p{Alnum}{4}){3}-\p{Alnum}{12}"""

  def generate: String = java.util.UUID.randomUUID().toString

  def isValid(id: String): Boolean = id.matches(UUIDRegex)
}

さて、JSONからUIDを受け取る場合、

  1. 暗黙的なReads[User]などを、(例えば)object Implicitsにまとめて定義する
    • このReadsのvalidationでUID.isValidを使用したい
  2. 実際にJSONを読み込むところでimport Implicits._する

ということがしたい。 …のだけど、object ImplicitsUIDをDIをする手段がなくて困る、という話です。 (@Singleton class Implicits @Inject() (uid: UID)を作ってclass FooController @Inject() (implicits: Implicits)して…とかやりたくない)

  • app/models/Implicits.scala
object Implicits {

  // uid.isValid の uidはどこからくるの・・・?
  private val validId: Reads[String]   = minLength[String](1) andKeep maxLength[String](255) andKeep verifying(uid.isValid)
  private val validName: Reads[String] = minLength[String](1) andKeep maxLength[String](32)

  implicit val userFormat = Format((
      (JsPath \ 'userId ).read[String](validId) and
      (JsPath \ 'name   ).read[String](validName) and
      (JsPath \ 'created).read[Long]
    )(User),
    Json.writes[User]
  )

}

さらにはobject UserDaocreate()内でuid.generateしたい場合などにも困ります。 こちらはclass UserDao @Inject() (uid: UID)などでもよいのですが…

@Inject

シングルトンであるobjectにDIはできないのですが、classやtraitのメンバには@Injectできます。

UIDをDIするtraitを定義して継承すれば…

trait UIDInjector {
  @Inject
  var uid: UID
}

object Implicits extends UIDInjector {
  // use `uid.isValid`
}

var!? 正気か!?

メソッドにも@Injectできますので、一応、下記のように書くこともできます。 …が、状況はほとんど変わりません。

trait UIDInjector {
  private var _uid: UID = _

  def uid = _uid

  @Inject
  def setUid(_uid: UID): Unit = this._uid = _uid
}

object Implicits extends UIDInjector {
  // use `uid.isValid`
}

Play.current

Play FrameworkのDIで使用するModule(Injector)はApplicationが保持しています。 そのためPlay.currentApplicationを取得し、そのinjectorからUIDにDIされてるインスタンスを取得することができます。

import play.api.Play

object Implicits {
  private def uid = Play.current.injector.instanceOf(classOf[UID])
}

やったか!?

なおplay.api.Play.currentは、version 2.5.xから@deprecatedの模様。*3 これあかんやつや…

なんとかしてApplicationにアクセスできればよいのですが、ソースを確認したところ、ApplicationPlay.start()に渡されるのみです。

/play-server/src/main/scala/play/core/server/DevServerStart.scala#L155-161 /play-server/src/main/scala/play/core/server/ProdServerStart.scala#L42-L49

      // Start the application
      val application: Application = {
        val environment = Environment(config.rootDir, process.classLoader, Mode.Prod)
        val context = ApplicationLoader.createContext(environment)
        val loader = ApplicationLoader(context)
        loader.load(context)
      }
      Play.start(application)

つまりApplicationにアクセスする方法は、以下の2つしかありません。

  • deprecatedPlay.currentから取得する
  • classコンストラクター@Inject() (appProvider: Provider[Application])する*4

ダメみたいですね(諦観)

Google Guice

Play FrameworkからInjectorを取得することは諦め、Google Guiceを直接使用します。

この方法の流れは下記の通りです。

  1. UIDUUIDをDIするMyAppModuleを定義する
  2. Guice.createInjector(new MyAppModule).getInstance(classOf[UID])で取得する

具体的なコードは下記の通りです。 objectのメンバに直接DIすることもできますが、traitにしてobjectにミックスインしています。

念のためPlay FrameworkのDIで使用する可能性も考慮し、application.confでPlay Framework側のModuleにも追加しています。*5 今のところPlay FrameworkのDIでUIDを使用する予定はないため、この設定はOptionalです。

  • app/inject/Injector.scala
package inject

import com.google.inject.{AbstractModule, Guice}
import utils.{UID, UUID}

trait Injector {
  protected val injector = Guice.createInjector(new MyAppModule)
}

trait UIDInjector extends Injector {
  protected val uid = injector.getInstance(classOf[UID])
}

class MyAppModule extends AbstractModule {

  override def configure(): Unit = {
    bind(classOf[UID]).toInstance(UUID)
  }

}
  • app/models/Implicits.scala
object Implicits extends UIDInjector {

  private val validId: Reads[String]   = minLength[String](1) andKeep maxLength[String](255) andKeep verifying(uid.isValid)
  private val validName: Reads[String] = minLength[String](1) andKeep maxLength[String](32)

  implicit val userFormat = Format((
      (JsPath \ 'userId ).read[String](validId) and
      (JsPath \ 'name   ).read[String](validName) and
      (JsPath \ 'created).read[Long]
    )(User),
    Json.writes[User]
  )

}
  • conf/application.conf
play.modules {
  enabled += "inject.MyAppModule"
}

成し遂げたぜ。

Appendix: Dependency Injection

本文で省略したPlay FrameworkにおけるDIの方法について

とりあえずclassのコンストラクタ引数にDIしたい型の変数を書き、その前に@Inject()をつけると…

あら不思議!そのクラス内でDIされたインスタンスが使用できます。

package controllers

import javax.inject.{Inject, Singleton}

import play.api.mvc._
import utils.UID

@Singleton
class FooController @Inject() (uid: UID) extends Controller {

  // FooControllerがGuiceによってインスタンス化されるとき
  // 引数の `uid: UID` にDIしている `UUID` をセットして
  // インスタンス化してくれます

}

DIの設定方法

com.google.inject.AbstractModuleまたはplay.api.inject.Moduleを継承したclassで、どの型に何のインスタンスをDIするか指定します。

com.google.inject.AbstractModule

import com.google.inject.AbstractModule
import utils.{UID, UUID}

class MyModule extends AbstractModule {

  override def configure(): Unit = {
    // `UID` に `object UUID` をDIする
    bind(classOf[UID]).toInstance(UUID)
  }

}

play.api.inject.Module

こちらはEnvironmentConfigurationが参照可能です。

import play.api.{Configuration, Environment}
import play.api.inject.{Binding, Module}
import utils.{UID, UUID}

class MyModule extends Module {

  override def bindings(env: Environment, conf: Configuration): Seq[Binding[_]] = Seq(
    // `UID` に `object UUID` をDIする
    bind[UID].toInstance(UUID)
  )

}

Moduleの設定方法

root package Module

プロジェクトのroot packageにclass Moduleを配置します。

Activatorのplay-scalaテンプレートではこちらの方法が使用されています。 templates/play-scala/app/Module.scala

このModuleは自動的に追加されるため、application.confplay.modules.enabledへ明示的に追加する必要がありません。 ただし、play.modules.disabled"Module"が含まれている場合、このModuleは除外され、追加されません。

play/src/main/scala/play/api/inject/Module.scala#L104-L111

  private val DefaultModuleName = "Module"

// (中略)

    // Construct the default module if it exists
    // Allow users to add "Module" to the excludes to exclude even attempting to look it up
    val defaultModule = if (excludes.contains(DefaultModuleName)) None else try {
      val defaultModuleClass = environment.classLoader.loadClass(DefaultModuleName).asInstanceOf[Class[Any]]
      Some(constructModule(environment, configuration, DefaultModuleName, () => defaultModuleClass))
    } catch {
      case e: ClassNotFoundException => None
    }

application.conf

root package以外のModuleapplication.confplay.modules.enabledにclass名を追加することでDIのModuleに追加することができます。

逆にplay.modules.disabledに追加するとDIのModuleから除外されます。

参考リンク

ScalaDependencyInjection https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection

Play2.4のDIについて動作確認(Guiceの使い方) http://qiita.com/mtoyoshi/items/768a1a8ece5a9be7254e

Appendix: Play FrameworkのDIの今後

2016年7月現在、Issues #5822の対応として、guice supportをcoreから分離する変更がPR #6178で入っています。

どうもcompile-time DIなどを使う際には不要だから外したいというような要望に対して、Guice周りの実装(package play.api.inject.guice)を/framework/src/play-guiceに分けたようです。

現状、何かしらのruntime DIを使用していれば起動時にGuiceApplicationLoaderが使用されると思いますので*6、今後も今まで通りのDIを使用する場合、build.sbtに下記の記述を追加する必要がありそうです。

  • build.sbt
libraryDependencies += guiceSupport

元々のIssueのマイルストーンがversion 2.6.xなので、次のマイナーバージョンアップデートからのようですが?

(2016/09/24 追記) guiceSupportからguiceに変わったようです。 https://github.com/playframework/playframework/pull/6463


*1:Unique Identifier or User Identifier

*2: そんな頻度で差し替えませんが、保守性は上がります(変更時のdiffも見やすい)

*3:https://www.playframework.com/documentation/2.5.x/Migration25#Deprecated-play.Play-and-play.api.Play-methods

*4:https://www.playframework.com/documentation/2.5.x/Migration25#Handling-legacy-components

*5:追加していない場合、Play FrameworkのDIで使用した際、GuiceApplicationLoaderでのload時にエラーとなります

*6:https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection#Advanced:-Extending-the-GuiceApplicationLoader