Journey to Play 2.4

by Oleg Ilyenko / @easyangel

Scaldi

Lightweight Scala Dependency Injection Library

Inject Bindings


inject [SomeClass]

inject [Database] (identified by 'remote and by default new Riak)
          

Bind


class UserModule extends Module {
  bind [MessageService] to new OfficialMessageService

  binding identifiedBy "greeting.official" to "Welcome"
}
          

Usage


class OfficialMessageService(implicit inj: Injector)
          extends MessageService with Injectable {

  val officialGreeting =
    inject [String] (identified by "greeting.official")

  def getGreetMessage(name: String) =
    s"$officialGreeting, $name!"
}
          

Injector Composition


def tokenModule = new Module {
  bind [Tokens] to new TokenRepo(db = inject [Database])
}

def dbModule = new Module {
  bind [Database] to new Riak
}

def appModule = tokenModule :: dbModule
          

and then test it


def mocksModule = new Module {
  bind [Database] to new InMemoryDb
}

implicit val testModule = mocksModule :: appModule
          

Some Features

Constructor injection


class TokenRepo(db: Database, metrics: Metrics) extends Tokens

bind [Tokens] to injected [TokenRepo]
          

Conditions


class UserModule extends Module {
  bind [MessageService] when (inDevMode or inTestMode) to
    new SimpleMessageService

  bind [MessageService] when inProdMode to
    new OfficialMessageService
}
          

Past

"DI" in Play 2.3


object Pets extends Controller {

  def show(id : Long) = Action {
    Ok(views.html.pet(Pet.findById(id)))
  }

}

object Pet {

  def findById(id : Long) : Pet =
    DB.withConnection({ implicit c =>
      SQL(basicQuery + "id = {id}").on("id" -> id)().map(rowToPet)
    }).head

}
          

DI Library Integration


package play.api

trait GlobalSettings {

  def getControllerInstance[A](controllerClass: Class[A]): A = ...

  // ...
}
          

Play 2.3 Scaldi Integration


package scaldi.play

trait ScaldiSupport extends GlobalSettings with Injectable {
  def applicationModule: Injector
  private var currentInjector: Option[Injector] = None

  abstract override def onStart(app: Application) =
    currentInjector = Some(createApplicationInjector(app))

  abstract override def onStop(app: Application) =
    currentInjector foreach (_.destroy())

  override def getControllerInstance[A](controllerClass: Class[A]): A =
    currentInjector map (_.getBinding(...)) getOrElse (...)
}
          

Scaldi Usage


class Application(implicit inj: Injector)
       extends Controller with Injectable {
  val messageService = inject [MessageService]
}
          

class MyModule extends Module {
  binding to new Application
  bind [MessageService] to new OfficialMessageService
}
          

object Global extends GlobalSettings with ScaldiSupport {
  def applicationModule = new MyModule :: new SomeOtherModule
}
          

GET  /   @controllers.Application.index
          

Play 2.4

JSR 330


import javax.inject.{Inject, Provider, Singleton}

@Singleton
class RoutesProvider @Inject() (
            injector: Injector,
            environment: Environment,
            configuration: Configuration,
            httpConfig: HttpConfiguration) extends Provider[Router] {
  // ...
}

@Singleton
class Crypto @Inject() (config: CryptoConfig) {
  // ...
}
          

Scaldi JSR 330 Support


implicit val inj = new Module {
  binding to annotated [RoutesProvider]

  binding identifiedBy qualifier[SomeQualifier] to annotated [MyService]

  binding identifiedBy annotation(SomeQualifierImpl.of("dep1")) to
    annotated [MyService]
}
          

+ OnDemandAnnotationInjector

Under the Hood

BuiltinModule


package play.api.inject

class BuiltinModule extends play.api.inject.Module {
  def bindings(env: Environment, configuration: Configuration) =
    Seq(
      bind[ApplicationLifecycle].to(bind[DefaultApplicationLifecycle])
      bind[Router].toProvider[RoutesProvider],
      bind[ActorSystem].toProvider[ActorSystemProvider],
      bind[ExecutionContext].toProvider[ExecutionContextProvider],

      // ...
    )
}
          

Plugins


class EhCacheModule extends play.api.inject.Module {
  def bindings(environment: Environment, configuration: Configuration) = {
    // ...

    Seq(
      bind[CacheManager].toProvider[CacheManagerProvider],
      bind[CacheApi].to(bind[CacheApi]
            .qualifiedWith(named(defaultCacheName))),
      bind[JavaCacheApi].to[DefaultJavaCacheApi]
    )
  }
}
          

reference.conf


play {
  modules {
    enabled += "play.api.cache.EhCacheModule"
  }

  cache {
    # ...
  }
}
          

Deprecations

  • GlobalSettings should not be used anymore
  • Plugin is just another Module
  • FakeApplication is just a helper to construct a Module

Using Scaldi in Play 2.4

ScaldiSupport trait is also deprecated...

... use application.conf instead


play.application.loader = scaldi.play.ScaldiApplicationLoader

play.modules.enabled += "modules.MyModule"
play.modules.enabled += "modules.SomeOtherModule"
play.modules.enabled += "scaldi.play.ControllerInjector"
          

ScaldiApplicationLoader


package scaldi.play

class ScaldiApplicationLoader extends ApplicationLoader {
  def load(context: Context)  =
    new ScaldiApplicationBuilder()
      .in(context.environment)
      .loadConfig(context.initialConfiguration)
      .prependModule(new Module {
        bind [OptionalSourceMapper] to
            new OptionalSourceMapper(context.sourceMapper)
        bind [WebCommands] to context.webCommands
      })
      .build
}
          

Testing


val application = new ScaldiApplicationBuilder()
  .prependModule(new TestModule).build()

running(application) {
  val home =  route(FakeRequest(GET, "/")).get

  // ...
}
          

or simply


withScaldiApp(modules = Seq(new TestModule)) {
  val home =  route(FakeRequest(GET, "/")).get

  // ...
}
          

Full Control


val module = new TestModule :: new MyModule :: new ControllerInjector

withScaldiApp(modules = Seq(module, new EhCacheModule, new BuiltinModule),
              loadModules = (_, _) => Seq.empty) {

  val home =  route(FakeRequest(GET, "/")).get

  // ...
}
          

build.sbt


libraryDependencies +=
  "org.scaldi" %% "scaldi-play" % "0.5-play-2.4.0-RC3-10"
          

Example Project

https://github.com/OlegIlyenko/scaldi-play-example

Conclusion

  • Was fun to implement
  • 👍👍 Play finally got proper DI mechanism
    • 👍👍👍 No global variables and singletons in future
  • Despite my dislike of annotations, they are necessary in this case
  • Took me a weekend* to implement JSR 330 spec
  • Lesson learned: TCK does not cover all cases
  • 👍 Play core and plugins are DI library agnostic
  • 👍 Compile-time and runtime DI mechanism supported

Future

  • Polish integration code
  • Review testing support
  • Play 2.3 and 2.4 would be both supported

Thank you!

Questions?