Dependency Injection in Play! with MacWire

The most recent release of MacWire (0.4) (a Scala macro to generate wiring code for class instantiation, DI container replacement) comes with new utilities which make it easier to integrate with frameworks which require by-class instance lookup. An example of such a framework is Play!, which is quite quickly gaining popularity in the web-development world.

There are two ways to find out how to do no-framework container-less dependency injection in Play! (and other web frameworks) with MacWire:

  1. read this blog
  2. try the MacWire+Play Typesafe Activator

Or you can do both ;)

Motivation

Many web frameworks require some form of generic instance lookups, e.g. when controller instances are referenced from views. In traditional DI containers like Guice, this is provided out-of-the-box, as it is the main way to get the wired objects. For example in Guice we have the Injector which translates Class objects into instances of the given class.

When using MacWire (or when doing “manual” DI), we are doing as much as possible in a type-safe way, simply referencing the objects as fields/methods in a container object, so we don’t usually need such maps. But for web framework integration, that is still needed.

General utilities

MacWire 0.4 comes with two general utilities useful for doing by-class instance lookups.

The first is a macro, valsByClass(object) which generates (at compile-time of course) a map of vals in the given object, keyed by their classes. So for example:

1
2
3
4
5
6
7
8
object Example1 {
   val aString = "Hello World!"
   val userAuthenticator = new UserAuthenticator()
   // here we are using the core MacWire feature: the wire macro
   val userFinder = wire[UserFinder] 
}
 
val theMap = valsByClass(Example1)

will translate the following code:

1
2
3
4
5
val theMap = Map[Class[_], AnyRef](
   classOf[String] -> Example1.aString,
   classOf[UserAuthenticator] -> Example1.userAuthenticator,
   classOf[UserFinder] -> Example1.userFinder
)

The second utility, InstanceLookup, extends that map to allow by-subclass/by-trait lookups. For example, say we have a Database trait and an implementation, MySQLDatabase, which ends up in the vals-by-class map under the Class[MySQLDatabase] key.

However, we would still like to get the instance when querying by Database. We can achieve that by wrapping the map with InstanceLookup:

1
2
3
4
5
6
7
8
9
10
trait Database
class MySQLDatabase extends Database
 
object App {
   val database = new MySQLDatabase()
}
 
val instanceLookup = InstanceLookup(valsByClass(App))
 
require(instanceLookup(classOf[Database]) == App.database)

Play integration

Having these utilities we may now proceed to integrating MacWire with Play.

The main change is that controllers now won’t be objects, but classes (usually with non-empty constructor parameter lists – the dependencies). The challenge is now how to tell Play how to get controller istances?

Let’s say we have the following controller and service class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package controllers
 
import play.api.mvc._
import services.HelloWorldProvider
 
class MainController(helloWorldProvider: HelloWorldProvider) extends Controller {
  def index = Action {
    Ok(views.html.index(helloWorldProvider.text))
  }
}
 
package services
 
class HelloWorldProvider {
   def text = "Hello World!"
}

Here the controller (MainController) has one dependency, the HelloWorldProvider service.

Now that we have the example ready, we can proceed with the integration. There are three easy steps. Firstly we need to wire the dependencies. We can use the wire[] macro, or just go with manual DI:

1
2
3
4
trait OurApplication {
   lazy val helloWorldProvider = new HelloWorldProvider
   lazy val mainController = new MainController(helloWorldProvider)
}

Secondly, we need to specify in the routes file that we want to provide controller instances ourselves, instead of letting Play instantiate the controllers. This is done by adding the @ character before the controller name:

1
2
3
// File: conf/routes
 
GET     /                           @controllers.MainController.index()

Finally, we need to create a Global object in the main package. That’s the main integration point; here we use the by-class instance lookup maps:

1
2
3
4
5
6
7
8
9
import com.softwaremill.macwire.{InstanceLookup, Macwire}
import play.api.GlobalSettings
 
object Global extends GlobalSettings with Macwire {
  val instanceLookup = InstanceLookup(valsByClass(new OurApplication {}))
 
  override def getControllerInstance[A](controllerClass: Class[A]) = 
      instanceLookup.lookupSingleOrThrow(controllerClass)
}

And that’s it! From now on the controller instances will be provided from our map, using the wiring we defined in the OurApplication trait.

Be sure to checkout the Activator if you want to see a working example in action!

Adam

  • Bartosz

    I was playing with the solution and I faced one small issue when initiating in Global. I believe in many cases objects initiation requires application configuration. The configuration is not available in the Global construct though. The creation needs to be moved to onStart method where the config is available.
    Am I correct or do you know a better way of addressing this requirement?

    Thanks.

  • http://www.warski.org/ Adam Warski

    That’s always the problem with frameworks ;) You end up not being in control when you’d like to. I don’t have any better solutions, I’m afraid.

  • loic_d

    Very good job, I love this way of wiring at compile time, without need of a container!