Compile Time DI in Play, Part 2

Part 2 of 3: Implementation

This is Part 2 of a 3-part blog post about compile time DI in Play 2.5. You should probably read Part 1: Introduction before this post. As a point of reference, I pushed the completed app that we’ll be building to Github.

So how does compile time DI work in Play? As I mentioned in the previous post, Play 2.4 deprecated the global, static instance of Application. In it’s place, they added the ApplicationLoader interface which is responsible for building an Application instance upon start. It has one method:

def load(context: Context): Application

A Context provides the generic building blocks of an Application such as configuration, file system access, etc.

By default, Play uses GuiceApplicationLoader which is, as you might imagine, a runtime implementation of ApplicationLoader. In this post, we’ll build a compile time implementation.

App Elements

Before we get up to ApplicationLoader, we’ll start at the bottom of the dependency hierarchy. First, let’s add a simple User model:

case class User(
firstName: String,
lastName: String,
email: String
)

Next we’ll add RandomUserClient which uses the randomuser.me API to retrieve, you guessed it, random User instances. Notice that it declares a dependency on WSClient via a constructor parameter. It also extends the generic UserClient interface to help facilitate testing — more on that in the next post.

class RandomUserClient(
ws: WSClient
) extends UserClient {

def getUser(id: Int)(implicit ec: ExecutionContext): Future[Option[User]] = {
ws.url("https://randomuser.me/api?seed=" + id).get().map { response =>
if (response.status == 200) {
(response.json \ "results")(0).asOpt[JsValue].flatMap { result =>
for {
firstName <- (result \ "name" \ "first").asOpt[String]
lastName <- (result \ "name" \ "last").asOpt[String]
email <- (result \ "email").asOpt[String]
} yield User(firstName, lastName, email)
}
} else None
}
}

}

Now we’ll add UserController, which wraps aUserClient instance in an HTTP interface. It declares that dependency via a constructor parameter as well.

class UserController(
userClient: UserClient
)(implicit ec: ExecutionContext) extends Controller {
  def get(id: Int) = Action.async {
userClient.getUser(id).map {
case Some(user) => Ok(Json.toJson(user))
case _ => NotFound
}
}
}

And finally, the routes file:

GET /users/:id  controllers.UserController.get(id: Int)

Component Traits

Although the ApplicationLoader will ultimately be responsible for providing an Application instance, we’ll have it delegate most of that responsibility to an ApplicationComponents class. This class will know how to build every dependency in the app by in turn delegating to component traits.

Wherein Homer represents our ApplicationComponents class, and his employees represent the component traits

Component traits are responsible for building a group of dependencies and, by convention in Play, their names end with the suffix “Components.” Play implements component traits for its framework classes, and we’ll implement our own for our application classes.

Analogous to how application classes declare dependencies on one another via constructor arguments, component traits declare dependencies on one another via their self types. Let’s build a component trait that provides an instance of UserClient to illustrate this concept:

trait DefaultClientComponents extends ClientComponents {
self: AhcWSComponents =>
lazy val userClient: UserClient = new RandomUserClient(wsClient)
}

As we discussed, RandomUserClient depends on WSClient, so DefaultClientComponents needs to pass an instance of WSClient into RandomUserClient's constructor. Conveniently, Play provides a component trait called AhcWSComponents which just so happens to provide an instance of WSClient. So, by declaring the self type as AhcWSComponents, we can access its wsClient member, and pass it to RandomUserClient.

This works because the self type can be read roughly as “when I get mixed in, the thing that I’m being mixed into must also mix in an instance of AhcWSComponents.” If this requirement is not met, the compiler will complain.

Also, the trait extends an abstract ClientComponents interface for the sake of testing. I’ll explain why in the next post.

ApplicationComponents

Now we can build our top-level ApplicationComponents (that’ actually just the name we use at Even — you can call it whatever you’d like). Here’s what it looks like:

class ApplicationComponents(context: Context)
extends BuiltInComponentsFromContext(context)
with AhcWSComponents
{

self: ClientComponents =>

lazy val router: Router = new Routes(
httpErrorHandler,
new UserController(userClient)
)

}

There’s a lot going on here also, so let’s break it down line-by-line:

  • class ApplicationComponents(context: Context)
    The class takes a Context argument which is the same thing that the load method takes in ApplicationLoader.
  • extends BuiltInComponentsFromContext(context)
    The Context instance gets passed right along to the abstract parent class BuiltInComponentsFromContext, which extends the BuiltInComponents component trait provided by Play. BuiltInComponents does most of the heavy lifting — as long as we implement its abstract router method, it will build an Application instance for us.
  • with AhcWSComponents
    The aforementioned component trait that provides an instance of WSClient.
  • self: ClientComponents =>
    For testing purposes, we declare a self type of the abstract ClientComponents instead of mixing in DefaultClientComponents directly. This will be explained in complete detail in the next post.
  • lazy val router: Router = new Routes(
    Thanks to BuiltInComponents, our only job is to implement the abstract router member. The class Routes is generated by Play based upon our routes file and implements the required Router interface.
  • httpErrorHandler,
    The first argument to Routes is an instance of HttpErrorHandler, which does what you think it does — this is a reasonable default implementation that BuiltInComponentsFromContext provides.
  • new UsersController(userClient)
    The rest of the arguments to Routes are instances of each of the controllers referenced in the routes file. Our app only has UserController, which we can now build because the ClientComponents self type provides its UserClient dependency.

ApplicationLoader

The last step is to build our implementation of ApplicationLoader. Here it is:

class ApplicationLoader extends play.api.ApplicationLoader {

def load(context: Context) = {
new ApplicationComponents(context)
with DefaultClientComponents
}.application

}

The load method builds an anonymous instance of ApplicationComponents that mixes in DefaultClientComponents, fulfilling the requirement of the ClientComponents self type. DefaultClientComponents has its own self type of AhcWSComponents, which ApplicationComponents already mixes in, so the compiler will be happy there too.

Notice that we can pass the Context instance that’s passed into load right along into the ApplicationComponents constructor. And, by virtue of extending BuiltInComponentsFromContext, ApplicationComponents has an application member of type Application, which is exactly what the load method needs to return.

Now that we have our compile time instance of ApplicationLoader, we just need to override the default GuiceApplicationLoader in the config:

play.application.loader = init.ApplicationLoader

Addendum: Hard-Earned Wisdom

As I said in the first post, compile time DI is not quite as well-supported as runtime DI in Play. Here are three tips that might save you a lot of time.

1. Runtime Incantations

As we know, Play 2.4 deprecated global objects such as WS. But if you’ve set up compile time DI, and your code, or code you depend on, uses some of these objects (including WS), you might encounter cryptic errors such as:

java.lang.InstantiationException: play.api.libs.ws.WSAPI
at java.lang.Class.newInstance(Class.java:427)
at play.api.inject.NewInstanceInjector$.instanceOf(Injector.scala:51)
at play.api.inject.SimpleInjector$$anonfun$instanceOf$1.apply(Injector.scala:87)

This is because when you call a method on WS, it will actually use runtime DI to lazily get an instance of WSClient and delegate to that. To support this trickery, BuiltInComponents builds an Application instance that is capable of resolving some dependencies at runtime. But it doesn’t know how to resolve instances from Play-supported libraries outside of the core framework, which is where WS lives.

To fix, you’ll need to properly configure an Injector instance in your ApplicationComponents:

override lazy val injector: Injector = {
new SimpleInjector(NewInstanceInjector) +
router + crypto + httpConfiguration + wsApi + global
}

I’ll leave the meaning of this incantation as an exercise for the reader.

2. Logging Configuration

Logging does not get automatically set up correctly in compile time DI. If you’re not seeing logs in production, add the following to ApplicationComponents:

LoggerConfigurator(context.environment.classLoader).foreach { 
loggerConfigurator =>
loggerConfigurator.configure(context.environment)
}

3. Dependency Ordering

If you encounter NullPointerExceptions while Play is building your app, it’s probably because your dependency hierarchy implicitly requires that they be build in an order that is different than the order in which they’re actually being built.

You can sidestep this headache altogether by simply making all of the dependencies in your component traits lazy val’s.


In Part 3 of this blog post, we’ll go into detail about how we can leverage compile time DI to build high quality unit and system tests for our app.

Like what you read? Give Kevin Hyland a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.