Playframework 2.4 Dependency Injection (DI)

Motivation

The Playframework 2.4 has been released after one year of development with the biggest new feature being Dependency Injection (DI). But injecting dependencies, what’s the point? And why does Play require it?

Obviously you were already able to use DI in your Play applications, except for the Play API itself. In previous versions of Play, there is the central Global object which holds global mutable state. Another such object is the currently running Application. Both would cause issues in testing and in development mode when you did not organize and isolate code carefully. This leaked into the application lifecycle and the plugin system, for example you have to define a plugin priority and make sure that external components are instantiated lazily and closed after an app restart (in development mode).

Dependency Injection in Play 2.4 essentially tries1 to get rid of global state and simplifies writing isolated, resuable, and testable code by instantiating components on each reload and by providing stop hooks.

Runtime DI

The runtime DI mechanism defined by Java Specification Request 330, with its reference implementation Guice, has been selected as the default DI framework. This makes it rather straightforward to start writing new Play applications without worrying too much about DI, as components are injected automatically by using the @Inject annotation. The official documentation on runtime DI is pretty complete so I will not go into detail about it.

Compile-Time DI

Guice as a standard mechanism makes sense for a framework that offers a first-class Java API.
As a Scala developer however, you might not be too keen on relying on reflection and runtime instantiation. Luckily, Play also provides a compile-time DI mechanism.

In its simplest form, compile-time DI, or any DI for that matter, is just constructor parameter DI. All of your dependencies are declared as constructor parameters in your classes, be it controllers, actors, or any other class. The Application Loader, which replaces the Global object, prepares all dependencies and instantiates your classes by passing their dependencies into their constructor. The link from controllers to route handling is made by creating a (generated) router that maps routes to instantiated controllers – again, via its constructor.

This requires enabling injected routes in your build.sbt:

routesGenerator := InjectedRoutesGenerator

An Application Loader’s main job is to is provide the current Application.

To do this, you will assemble all of the classes required by your app. All parts of Play, now called components, support compile-time DI by providing a trait that ends with Components and builds on three basic dependencies: ApplicationLifecycle, Configuration, Environment2.

ApplicationLifecycle allows your component to register a stop hook. Configuration is the TypesafeConfig that you would previously get from a running application. Environment is a collection of methods that revolve around your running Play app, such as the classloader, dev/test/production mode, and obtaining resources.

The base component trait BuiltInComponentsFromContext extracts environment and configuration from the Application Loader’s context, and instantiates applicationLifecycle.

Let’s say we have a small app with one Application controller that needs to make a webservice call. Now instead of importing WS and the current application, we mix in a trait at the application loader level and pass a concrete WS instance to the controller.

import play.api.{ApplicationLoader, BuiltInComponentsFromContext}
import play.api.libs.ws.ning.NingWSComponents
import play.api.routing.Router
import router.Routes // Routes are generated from routes file

class AppLoader extends ApplicationLoader {
  override def load(context: Context): Application =
    new AppComponents(context).application
}

class AppComponents(context: Context) extends BuiltInComponentsFromContext(context) with NingWSComponents {

  // NingWSComponents has a lazy val wsClient
  lazy val applicationController = new controllers.Application(wsClient)
  lazy val assets = new controllers.Assets(httpErrorHandler)

  override def router: Router = new Routes(httpErrorHandler, applicationController)
}

The controller in turn depends on WSClient which is just a trait:

class Application(ws: WSClient) extends Controller {...}

Finally we need to tell Play to use our Application Loader in application.conf:

play.application.loader="AppLoader"

This way of building our application has several advantages:

  • Dependencies on components are more explicit
  • We avoid using the current Application
  • We can easily switch to a mocked WS implementation when writing tests
  • When we refer to a new controller in the routes file, the compiler will tell us that Routes is missing a dependency

Testing

Since we control how our application components are assembled, it’s easy to create a version for our tests.

class FakeApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) {
  val mockWsClient = ... // You can use Mockito or https://github.com/leanovate/play-mockws
  lazy val applicationController = new controllers.Application(mockWsClient)
  lazy val assets = new controllers.Assets(httpErrorHandler)

  override def router: Router = new Routes(httpErrorHandler, applicationController)
}

ScalaTest

ScalaTest supports Play 2.4 with its OneAppPerSuite trait.

import org.scalatestplus.play.{OneAppPerSuite, PlaySpec}
class ApplicationTest extends PlaySpec with OneAppPerSuite {
  override implicit lazy val app: api.Application = {
    val appLoader = new FakeAppLoader
    val context = ApplicationLoader.createContext(
      new Environment(new File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test)
    )
    appLoader.load(context)
  }

  ... your tests ...
}

Writing Plugins / Components

Play plugins are now deprecated and replaced by DI components.

Any component you write should be compatible with both runtime and compile-time DI. For this you typically write a generic API trait and implement it using a Module or a JSR-330 Provider class, and a components trait for compile-time DI.

A basic example is the ActorSystemProvider in Play itself which is also used in compile-time DI via AkkaComponents.

A more complex example with modules and bindings is the I18n Module.

Other DI frameworks

Other popular Scala DI frameworks are MacWire, a macro-based wiring framework that automatically binds instances in scope and supports compile-time DI, and Scaldi, an injection framework that provides its own ApplicationLoader and supports compile-time bindings. Both are fully compatible with Play 2.4.

Behind the Curtain

So, how does the compile-time DI work, how does it relate to the runtime mechanism, and why do we get rid of global state?

In short, it’s all about the application loader. When not specifying an application loader (via play.application.loader), Play does it for you – it uses GuiceApplicationLoader which of course uses Guice to wire your application.

On each reload (in development mode, in prod mode this only happens once), Play reads the initial configuration, creates the Context and instantiates a new application loader. This also means that code that was previously only controlled by Play is now in your hands. As we have seen the ApplicationLoader is responsible for creating the Application, so it’s no longer an import that might not be initialized yet.

This also means that strictly speaking, compile time DI is not a seperate mechanism but the foundation for any mechanism, be it constructor parameters, Cake, Guice, or whatever else. Compile-time DI means manually constructing the application, while frameworks like Scaldi and runtime DI do it for you.

Migrating your app

In summary, the following things have changed from 2.3 to 2.4 with regards to dependency injection.

Play 2.3 Play 2.4
import play.api.Play.current If possible, don’t use Application; use Environment, ApplicationLifecycle, Configuration instead
Global object ApplicationLoader class
Global.onStart Simply instantiate in your app loader
Global.onStop ApplicationLifecycle.addStopHook
WithFilters ApplicationLoader.httpFilters
Plugins DI Components (use play.api.inject.Module to support runtime DI)
play.api.libs.concurrent.Akka Inject ActorRef or ActorRefFactory/ActorSystem

If you need support to migrate your project to Play 2.4, transform plugins or write DI components, I’m also offering a version upgrade service.

Conclusion

Using Dependency Injection is the first step in removing global state and thus making your app
easier to split into isolated, testable components.

As usual, there’s a code example in my PlayBasics repo.


  1. Tries because we’re not there yet 100%. Play 2.4 is the first step and eases the migration to DI. Play 2.5 and 3.0 will complete this transition.

  2. To find out which components exist, open Play’s ScalaDoc and enter “components”.

Posted in Scala & Playframwork
2 comments on “Playframework 2.4 Dependency Injection (DI)
  1. Hi Marius,

    once again thanks for writing this up.

    I think the code samples might need some corrections (but it could as well be my lack of insight).

    https://github.com/journeymonitor/analyze/commit/d06488876a90e4eed5aebf427746aca21752e268 shows how I’ve set up compile time DI as described here. In my AppLoader class file, I needed other imports than your post suggests:

    import play.api.ApplicationLoader.Context
    import play.api.libs.ws.ning.NingWSComponents
    import play.api.routing.Router
    import play.api.{Application, ApplicationLoader, BuiltInComponentsFromContext}
    import router.Routes

    Also, I tried to find out what the minimal setup would be if I would NOT want to provide a FakeApplication within my tests, but the real one, and came up with:

    override implicit lazy val app: api.Application =
    new AppLoader().load(
    ApplicationLoader.createContext(
    new Environment(
    new File(“.”), ApplicationLoader.getClass.getClassLoader, Mode.Test)
    )
    )

    Maybe you have some suggestions for this, too.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Time limit is exhausted. Please reload CAPTCHA.