Compile Time Dependency Injection with Play 2.4

June 3, 2015

Update 23/07/2015 : How to add stop hooks for your modules, how to inject play modules (WSClient, I18n, Cache, …)
Update 15/12/2015 : How to configure Play logs and Http filters

Play Framework, in its new version, provides a lot of new stuff to handle dependency injection at runtime, using Guice.
With Scala, I always prefer using compile time dependency injection, as it allows to see errors as soon as possible. I must also admit that I find compile time DI a lot more elegant as it needs no container or proxy at runtime!

Fortunately, Play team also added the ability to control the way routes, controllers and other components are binded together at compile time.

Controller and service

In this example, we have a controller that takes a LinkService dependency.
We want to be able to mock this service in our controller tests.

class Application(linkService: LinkService) extends Controller {

  def findLinks(query: String) = Action.async {
    val links = linkService.findLinks(query)
    links.map(response => Ok(views.html.index(response)))
  }

}

The routes file is defined as follows :

# Home page
GET     /                           controllers.Application.findLinks(query: String)
GET     /assets/*file               controllers.Assets.at(path="/public", file)

How to wire this

To have a fully functional application we need to tell the Play router how to find our LinkService dependency and how to wire it into our controller.

This can be done by defining a custom ApplicationLoader :

class SimpleApplicationLoader extends ApplicationLoader {
  def load(context: Context) = {
    new ApplicationComponents(context).application
  }
}

class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) {  
  lazy val logService = new LogService
  lazy val linkService = new LinkService(logService)

  lazy val applicationController = new controllers.Application(linkService)
  lazy val assets = new controllers.Assets(httpErrorHandler)
  override lazy val router = new Routes(httpErrorHandler, applicationController, assets)
}

Note : the Routes constructor is generated by Play at compile time, depending on the controllers referenced in the routes file.

To enable this configuration we need to add this line in application.conf :

play.application.loader=SimpleApplicationLoader

And this line in build.sbt :

routesGenerator := InjectedRoutesGenerator

How to test it

In this example, the tests will be written using Specs2.
To test our controller, we can easily wire a mocked linkService :

class ControllerSpec extends Specification with Mockito {

  val mockLinkService = mock[LinkService]
  mockLinkService.findLinks("hello") returns Future("http://coucou.com")

  "Application" should {

    "display query results" in {

      val app = new Application(mockLinkService)
      val response = app.findLinks("hello")(FakeRequest())
      
      status(response) must equalTo(OK)
      contentType(response) must beSome("text/html")
      contentAsString(response) must contain("http://coucou.com")

    }
  }
}

To be able to test the routes directly with our real services, e.g. for integration tests, we need to define our own ‘WithApplication’ helper :

class WithDepsApplication extends WithApplicationLoader(new SimpleApplicationLoader)

Of course if you want your router to use controllers with mocked services, you can define a new ApplicationLoader for tests instead of our SimpleApplicationLoader.

Then, we can use WithDepsApplication in our tests :

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {

  "Application" should {

    "render the index page" in new WithDepsApplication{
      val home = route(FakeRequest(GET, "/?query=hello")).get

      status(home) must equalTo(OK)
      contentType(home) must beSome.which(_ == "text/html")
      contentAsString(home) must contain ("Results :")
    }
  }
  
}

Note that you can also use macwire macros to wire automatically services, controllers and routes. A good example can be found here.

How to inject Play modules

As Ryan pointed out in the comments, it is possible to inject modules provided by Play APIs. The BuiltInComponentsFromContext can be mixed with several traits, provided by the framework.
Let’s consider that our link service needs to use the Web service client API :

class LinkService(logService: LogService, wsClient: WSClient){

  def findLinks(query: String) = {
    val duckDuckUrl = Play.current.configuration.getString("duckduck.url").getOrElse("http://api.duckduckgo.com/?format=json&q=") + query
    wsClient.url(duckDuckUrl).get().map{ response =>
      //...
    }
  }

  def cleanup = Logger.info("cleanup") // we will use this later
}

To inject an instance of WS client in your controller, you have to mix BuiltInComponentsFromContext with the NingWSComponents trait and then pass the wsClient to your service :

class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) with NingWSComponents {  
  lazy val logService = new LogService
  lazy val linkService = new LinkService(logService, wsClient)  

  lazy val applicationController = new controllers.Application(linkService)
  lazy val assets = new controllers.Assets(httpErrorHandler)
  override lazy val router = new Routes(httpErrorHandler, applicationController, assets)
}

You can use other traits like the Cache API, the I18n API, etc.

How to add stop hooks for your modules

You may need to add some behavior to your dependencies when your application is stopped.
BuiltInComponentsFromContext contains an applicationLifecycle value, which is a DefaultApplicationLifecycle. This class provides an addStopHook method. You can pass an asynchronous function to this method, that will be executed when your application will be stopped.

In this example, we are using LinkService cleanup method in the stop hook :

class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) with NingWSComponents {  
  lazy val logService = new LogService
  lazy val linkService = new LinkService(logService, wsClient)
  lazy val applicationController = new controllers.Application(linkService)  

  applicationLifecycle.addStopHook(() => Future.successful(linkService.cleanup)) // <-- stop hook

  lazy val assets = new controllers.Assets(httpErrorHandler)
  override lazy val router = new Routes(httpErrorHandler, applicationController, assets)
}

Important note : stop hooks are called in the reverse order they have been added in applicationLifecycle.

You can find the sources of this examples in this Github project. Next time we will see another new feature of Play 2.4, its (experimental) integration with Akka-Streams!

More tips

When you’re using compile time dependency injection with Play, some basic stuff that used to work out of the box don’t work anymore.

To activate Play logs, you need to add this line in the ApplicationLoader :

Logger.configure(context.environment)

So the SimpleApplicationLoader definition looks like :

class SimpleApplicationLoader extends ApplicationLoader {
  def load(context: Context) = {
    Logger.configure(context.environment)
    new ApplicationComponents(context).application
  }
}

To activate Http filters (or essential filters), you need to override a value from BuiltInComponentsFromContext :

val myFilter = new MyFilter(configuration)
override lazy val httpFilters = Seq(myFilter)

So the ApplicationComponents definition looks like :

class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) with NingWSComponents {  
  lazy val logService = new LogService
  lazy val linkService = new LinkService(logService, wsClient)
  lazy val applicationController = new controllers.Application(linkService)  

  applicationLifecycle.addStopHook(() => Future.successful(linkService.cleanup))

  lazy val assets = new controllers.Assets(httpErrorHandler)
  override lazy val router = new Routes(httpErrorHandler, applicationController, assets)
  val myFilter = new MyFilter(configuration)
  override lazy val httpFilters = Seq(myFilter)
}

Discussion, links, and tweets

comments powered by Disqus