1. Introduction

Most modern frameworks use dependency injection libraries to provide an easy way to create, reuse, and mock objects. Play framework by default comes with Google Guice, but it is not the only DI framework that can be used. This article will demonstrate some of the most common DI uses for Google Guice and MacWire compile-time dependency injection.

2. Setup

As we already mentioned, Play comes with Guice out of the box. In case we want to use MacWire, we need to add two additional dependencies to our build sbt file:

libraryDependencies += "com.softwaremill.macwire" %% "macros" % "2.4.0" % Provided
libraryDependencies += "com.softwaremill.macwire" %% "util" % "2.4.0"

3. Defining Components: Single Objects and Traits

In the following section, we’ll provide some examples of how we can define singletons.

3.1. Using Guice

Let’s see how we define singletons with the annotation javax.inject.Singleton:

@Singleton
class UserService {
  // ...
}

Alternatively, singletons can be defined in modules:

bind(classOf[UserService])
  .in(classOf[Singleton])

3.2. Using MacWire

On the other hand, to wire components with MacWire, we need to use the wire macro within component traits:

lazy val userService = wire[UserService]

Likewise, we can define a singleton is to explicitly provide the instance:

lazy val userService = new UserService(/*...*/)

4. Defining Components: Collections

Now that we know how to define single objects, it’s time to showcase how we can wire object collections of the same type.

In our example, we’ll wire a collection of OrderPipelineProcessor trait instances:

case class Order(id: Long, userId: Long, date: Long)

trait OrderPipelineProcessor {
  def process(order: Order): Unit
}

4.1. Using Guice

We can wire a collection in Google Guice with the Provider interface or the Provides annotation:

@Provides
  def orderPipelineProcessors(): Seq[OrderPipelineProcessor] =
    Seq(
      (order: Order) => println("Processor 1 processed"),
      (order: Order) => println("Processor 2 processed"),
      (order: Order) => println("Processor 3 processed"),
      (order: Order) => println("Processor 4 processed")
    )

@Singleton
class OrderService @Inject() (orderPipeline: Seq[OrderPipelineProcessor]) {
  def process(order: Order): Unit = orderPipeline.foreach(_.process(order))
}

4.2. Using MacWire

We can wire a Set with MacWire’s wireSet macro:

lazy val p1: OrderPipelineProcessor = (order: Order) =>
  println("Processor 1 processed")
lazy val p2: OrderPipelineProcessor = (order: Order) =>
  println("Processor 2 processed")
lazy val p3: OrderPipelineProcessor = (order: Order) =>
  println("Processor 3 processed")
lazy val p4: OrderPipelineProcessor = (order: Order) =>
  println("Processor 4 processed")

lazy val orderPipelineProcessors = wireSet[OrderPipelineProcessor]

lazy val orderService = wire[OrderService]

5. Composing Multiple Modules

Provided that applications tend to grow bigger over time, splitting our modules is always a good idea.

5.1. Using Guice

As an example, let’s create two modules, OrderModule and UserModule:

class OrderModule(environment: Environment, configuration: Configuration) 
  extends AbstractModule {

  override def configure(): Unit = {
    // ...
  }
}

class UserModule(environment: Environment, configuration: Configuration) 
  extends AbstractModule {
  // ...
}

At last, we need to add the modules to the enabled modules list in the application.conf:

play {
  modules.enabled += "modules.UserModule"
  modules.enabled += "modules.OrderModule"
}

5.2. Using MacWire

In contrast, composing modules with MacWire is essentially trait stacking:

trait UserComponents { // ... }

trait OrderComponents { // ... }

Every component trait is glued together in the ApplicationLoader:

class AppComponents(context: Context) extends BuiltInComponentsFromContext(context)
  with BuiltInComponents
  with HttpFiltersComponents
  with UserComponents
  with OrderComponents {
  
  // ...
}

6. Wiring Multiple Objects of the Same Type

It’s not uncommon for an application to wire more than one object of the same type.

6.1. Using Guice

Named components allow us to bind multiple objects of the same type in Google Guice:

trait OrderValidationService {
  def validate(order: Order): Boolean
}

class BusinessOrderValidationService extends OrderValidationService {
  override def validate(order: Order): Boolean = {
    println("Business order validation")
    true
  }
}

class EnterpriseOrderValidationService extends OrderValidationService {
  override def validate(order: Order): Boolean = {
    println("Enterprise order validation")
    true
  }
}

Components are named during instance definition in module files:

bind(classOf[OrderValidationService])
  .annotatedWith(Names.named("Business"))
  .toInstance(new BusinessOrderValidationService)

bind(classOf[OrderValidationService])
  .annotatedWith(Names.named("Enterprise"))
  .toInstance(new EnterpriseOrderValidationService)

In the end, the application injector will lookup by name the instances via @Named annotation:

class OrderService @Inject()(
  @Named("Business") businessOrderValidationService: OrderValidationService,
  @Named("Enterprise") enterpriseOrderValidationService: OrderValidationService,
  orderPipeline: Seq[OrderPipelineProcessor]) {
  
  // ...
}

6.2. Using MacWire

Qualifiers or tags in MacWire is the framework’s way of telling the instances apart:

trait Business
trait Enterprise

lazy val businessOrderValidationService: BusinessOrderValidationService @@ Business =
  (new BusinessOrderValidationService).taggedWith[Business]
lazy val enterpriseOrderValidationService: EnterpriseOrderValidationService @@ Enterprise =
  (new EnterpriseOrderValidationService).taggedWith[Enterprise]

Tagged dependencies can be used in constructors using the same tags:

class OrderService(
  businessOrderValidationService: OrderValidationService @@ Business,
  enterpriseOrderValidationService: OrderValidationService @@ Enterprise,
  orderPipeline: Set[OrderPipelineProcessor]
) { // ... }

7. Testing with Modules

Sometimes in testing, it makes sense to partially or entirely mock a module. For example, we may need to test our application entirely, but we don’t want to call external systems such as databases or web services.

First, let’s define a service that has a remote API dependency:

trait RemoteApi {
  def remoteCall(): String
}

class ServiceWithRemoteCall @Inject() (remoteApi: RemoteApi) {
  def call(): String = remoteApi.remoteCall()
}

class RealRemoteApi extends RemoteApi {
  override def remoteCall(): String = "Real remote api call"
}

class MockRemoteApi extends RemoteApi {
  override def remoteCall(): String = "Mock remote api call"
}

Next, we’ll show how to mock the RemoteApi.

7.1. Using Guice

In the production module, we bind the RealRemoteApi:

bind(classOf[RemoteApi])
  .toInstance(new RealRemoteApi)

In the mock module, we bind the MockRemoteApi:

bind(classOf[RemoteApi])
  .toInstance(new MockRemoteApi)

Finally, we can use our mocks to write the test cases:

"ServiceWithRemoteCall call" should {
  "invoke mock when remote api is mocked" in {
    val application = new GuiceApplicationBuilder()
      .overrides(new MockApiModule, new ServiceModule)
      .build()
    new App(application) {
      val srv = app.injector.instanceOf[ServiceWithRemoteCall]
      assert(srv.call() == "Mock remote api call")
    }
  }

  "invoke real method when real api is wired" in {
    val application = new GuiceApplicationBuilder()
      .overrides(new ApiModule, new ServiceModule)
      .build()
    new App(application) {
      val srv = app.injector.instanceOf[ServiceWithRemoteCall]
      assert(srv.call() == "Real remote api call")
    }
  }

}

7.2. Using MacWire

Similarly, we are going to use the same service and remote API. MacWire component traits can be written as:

trait ApiComponents {
  lazy val remoteApi: RemoteApi = new RealRemoteApi
}

trait MockApiComponents extends ApiComponents {
  override lazy val remoteApi: RemoteApi = new MockRemoteApi
}

Then, in test suites, we use traits as fixtures:

"ServiceWithRemoteCall call" should {

  "invoke mock when remote api is mocked" in new ServiceComponents with MockApiComponents {
    assert(serviceWithRemoteCall.call() == "Mock remote api call")
  }

  "invoke real method when real api is wired" in new ServiceComponents with ApiComponents {
    assert(serviceWithRemoteCall.call() == "Real remote api call")
  }
}

8. Conclusion

In this article, we presented some of the most common use cases of Play dependency injection functionalities. Along with the default dependency injection that comes with Play, Google Guice, we demonstrated the MacWire compile-time dependency injection alternative. As always, the code of the above examples is available over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments