- この記事は 2016年07月03日 にQiitaへ投稿した内容を転記したものです。
- 本記事は執筆から1年以上が経過しています。
前置き
classが適切な場合は、classを使用してDependency Injectionしましょう
ここから先はobjectに対してDependency Injection(DI)する方法について…
なんでclass使わないの?馬鹿なの?死ぬの?
ちょっと待って!ユースケースの説明をさせてくれ、頼む!
例えばUID *1 生成のtraitを作っておき、その生成ロジックはDIで簡単に差し替えられるようにしておきたい。*2
UID は Rest API(JSON) で受け取ることもあるので、簡単な(もしくは厳密な)validation処理も実装しておきたい。
- app/utils/UID.scala
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を受け取る場合、
- 暗黙的な
Reads[User]
などを、(例えば)object Implicits
にまとめて定義する- この
Reads
のvalidationでUID.isValid
を使用したい
- この
- 実際にJSONを読み込むところで
import Implicits._
する
ということがしたい。
…のだけど、object Implicits
にUID
を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 UserDao
のcreate()
内で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.current
でApplication
を取得し、その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
にアクセスできればよいのですが、ソースを確認したところ、Application
はPlay.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
- ProdServerStart.scala
// 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つしかありません。
ダメみたいですね(諦観)
Google Guice
Play FrameworkからInjectorを取得することは諦め、Google Guiceを直接使用します。
この方法の流れは下記の通りです。
UID
にUUID
をDIするMyAppModule
を定義する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
こちらはEnvironment
やConfiguration
が参照可能です。
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.conf
のplay.modules.enabled
へ明示的に追加する必要がありません。
ただし、play.modules.disabled
に"Module"
が含まれている場合、このModule
は除外され、追加されません。
play/src/main/scala/play/api/inject/Module.scala#L104-L111
- Module.scala
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以外のModule
もapplication.conf
のplay.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