Let’s Play! - My Play Framework Learning Journey (3) - Dependency Injection with Guice

Play provides dependency injection support based on JSR-330 and uses Guice for dependency injection out of the box. The Play Scala REST API Example project uses Scala-Guice to wrap and extends Guice.

Registering Guice Modules with Play

Play will look for a class called Module and place it in the root package and automatically register it. Alternatively, if you want to give it a different name or put it in a different package, you can register it with Play by appending its fully qualified class name to the play.modules.enabled list in application.conf:

play.modules.enabled += "modules.HelloModule"

As seen in the example project, the class Module is defined as follows:

import com.google.inject.AbstractModule
import net.codingwell.scalaguice.ScalaModule
import play.api.{Configuration, Environment}

class Module(environment: Environment, configuration: Configuration)
    extends AbstractModule
    with ScalaModule

You will notice that the Module class constructor takes in two parameters: an Environment and a Configuration, both are from play.api package. The two parameters are injected by Guice, but simply called by Play as it loads your modules, since Environment and Configuration can easily be determined before the injector is created. This special case exists to allow defining bindings based on configuration or environment. For example, you can access the Configuration object and bind a value in the configuration file to a variable:

bind[Long].annotatedWithName("propertyA").toInstance(configuration.getLong("app.propertyA").getOrElse(5L))

Play’s Built-In Module

Play has a BuiltInModule that can be injected to provide dependencies like Configuration and Environment. To inject the module, just enable it in application.conf:

play.modules.enabled += "play.api.inject.BuiltinModule"

Bindings

Guice provides many types of bindings. The Guice Wiki covers them in details. I will just talk about some points that may not be apparent from the Wiki here (it took me some time to figure them out).

Bind to one and only instance

By default, Guice returns a new instance each time it supplies a value. If you need to modify that behavior, e.g. bind to one instance via the type constructor, you can do use scope annotations such as @Singleton.

Often times you also need to bind some variable to one and only one instance, but that instance is provided by some static method or some other method. For example, binding some variable to values from configuration. In this case, you can use instance bindings:

bind[Long].annotatedWithName("propertyA").toInstance(configuration.getLong("app.propertyA").getOrElse(5L))

Here the bind method is from Scala-Guice’s InternalModule trait, which is implemented by ScalaModule trait.

Alternatively, you can use @Provides methods, which requires writing of a separate method and is suitable if the injection logic is a bit more complex. For example, to bind Slick DatabaseConfig[JdbcProfile] to different DatabaseConfig instances based on some environment variable, a @Provides method can be used:

@Provides @Singleton
def provideDatabaseConfig: DatabaseConfig[JdbcProfile] = {
  val env = Option(System.getProperty("env")).getOrElse(DEFAULT_ENV)
  env match {
    case DEFAULT_ENV => DatabaseConfig.forConfig[JdbcProfile](DEFAULT_ENV)
    case "dev" => DbConfigHelper(conf).getDecryptedConfig(GEMS_DEV_KEY)
    case x => DbConfigHelper(conf).getDecryptedConfig(x)
  }
}

Bind to Default Values

Guice 4.1 (and Scala-Guice) now supports Optional Bindings. It might be useful to provide default values. However, as you can see in the example above, since Play’s Configuration class returns values read as Options themselves, default values can be provided easily using the Option API.

Just-in-time bindings

With just-in-time bindings, you don’t need to create bindings for concrete classes that have injectable constructor. An injectable constructor is either a non-private, no-arguments constructor, or a constructor with the @Inject annotation.

Scala-Guice Binding annotations

Scala-Guice provides easy to use methods to allow defining bindings in a more “Scala” way via ScalaModule trait. One such method is annotatedWith as shown above, instead of doing something like:

bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class)

Injections

Dependencies can be injected in the following ways:

  • Constructor Injection
  • Method Injection
  • Field Injection

The recommended way of dependency injection by Play, Guice and many others is constructor injection. And here is a more Scala-specific description that references it.

One implication of using Constructor Injection is that all the dependencies to be injected will appear as constructor parameters. Some may not like it. However, it can be seen as a way to force the developers to think about the dependencies. Alternatively, the dependencies can be put in some global static object. This is undesirable if the static object has state. As explained by Greg Methvin, Tech Lead of Play Framework at Lightbend (via Lightbend’s wonderful Support Program):

When you depend on global static state, there are a few negative consequences:

  1. Tests require that state to be set up properly. The type system doesn’t enforce that that state is provided. On the contrary, if you require dependencies to be passed to the constructor, the compiler will make sure all the dependencies are provided.
  2. It can be difficult to guarantee that code can run in parallel, since the state is not limited to the instance of your class. So you typically can’t run tests in parallel using global state.
  3. As your application becomes more complex, it becomes harder to ensure the order in which that state needs to be initialized. This was a serious problem with the plugins system that existed in Play 2.3, and one of the key reasons Play moved to dependency injection.”

The idea of mutating static state is against the principles of both object oriented and functional programming. Functional programming emphasizes immutability, and if you need to mutate static state for your code to work, that inherently makes your code more difficult to understand. Object oriented programming emphasizes encapsulation, so objects should limit their exposure to the outside world.

Additional References

Go Top
comments powered by Disqus