Field Notes

Guice and Play, part 4: teaching Guice to build what it can't

Guice and Play, part 4: teaching Guice to build what it can't

In part 1 Guice built the graph of my Play 3 template, in part 2 I bound my first trait, and in part 3 I wrapped a MongoDB client in a class called Mongo so it could be an eager singleton with a stop hook. That wrapper only existed because Guice did not know how to build a MongoClient. This part removes it, and then answers a second question: what to do when two dependencies have the same type.

Guice can only call constructors it can see

For my own classes, @Inject() on the constructor is enough. MongoClient and MongoDatabase come from the driver. They have no @Inject constructor, and a client is created with a factory call and a URI from config. Guice offers two ways to supply that code: a @Provides method in a module, or a class that implements Provider[T]. I used one of each.

The client: a Provider class

class MongoClientProvider @Inject() (config: Configuration, lifecycle: ApplicationLifecycle)
    extends Provider[MongoClient]
    with LazyLogging {

  override def get(): MongoClient = {
    val client = MongoClient(config.get[String]("mongo.uri"))
    logger.info(s"Mongo client created (instance ${System.identityHashCode(client)})")

    lifecycle.addStopHook { () =>
      logger.info(s"Closing Mongo client (instance ${System.identityHashCode(client)})")
      Future.successful(client.close())
    }
    client
  }
}

The provider is a normal injectable class, so it gets Configuration and ApplicationLifecycle through its constructor. The stop hook from part 3 moved here. The binding goes in the root Module:

bind(classOf[MongoClient]).toProvider(classOf[MongoClientProvider]).asEagerSingleton()

I chose a class here because I want the client to be eager, and asEagerSingleton() exists only on a binding in configure(). A @Provides method has no equivalent. To be precise: Play builds its injector in Guice's Stage.PRODUCTION in prod mode (GuiceInjectorBuilder), and in that stage Guice creates all singletons when the injector is built (Guice scopes). In dev mode and in my tests the stage is DEVELOPMENT, so a @Provides @Singleton client would wait for its first lookup. With the provider binding it is eager in every mode.

The database: a @Provides method

@Provides @Singleton
def mongoDatabase(client: MongoClient, config: Configuration): MongoDatabase =
  client.getDatabase(config.get[String]("mongo.database"))

Guice calls this method and injects its parameters, the same way it fills a constructor (Guice wiki: @Provides methods). So the chain is: MongoClientProvider builds the client, the client is a parameter of mongoDatabase, and any class can now ask for MongoDatabase directly. The services.Mongo wrapper is deleted.

My rule of thumb after this: use @Provides for a short derivation from things already in the graph. Use a Provider class when the binding must be eager, or when the code is long enough to deserve its own file and test.

What the tests showed

GuiceGraphSpec got two new checks: instanceOf[MongoDatabase].name mustBe "play-template", and instanceOf[MongoClient] eq instanceOf[MongoClient]. sbt test ended with "Tests: succeeded 10, failed 0". Each of the six tests that builds an application logged exactly one "Mongo client created" / "Closing Mongo client" pair from services.MongoClientProvider. That includes the test that asks for the client twice, so the provider ran once per injector.

I also tried to add a @Provides def mongoClient() next to the provider binding, to see what Guice does with two recipes for one type. It refuses to build the injector:

1) [Guice/BindingAlreadySet]: MongoClient was bound multiple times.

Bound at:
1  : Module.configure(Module.scala:24)
2  : Module.mongoClient(Module.scala:30)

Compared with the cake pattern

At work, mongoDatabase would be a lazy val in a components trait. A @Provides method is close to that. The difference is how it gets its dependencies: the cake version reaches the client through a self-type, and the Guice version receives it as a method parameter.

Two clocks, one type

Next to SystemClocky I added UtcClocky, which returns LocalTime.now(ZoneOffset.UTC). A new TimeController serves GET /time and needs both. Two constructor parameters of type Clocky are ambiguous, so each one gets a qualifier:

// Module.configure()
bind(classOf[Clocky]).annotatedWith(Names.named("local")).to(classOf[SystemClocky])
bind(classOf[Clocky]).annotatedWith(Names.named("utc")).to(classOf[UtcClocky])

class TimeController @Inject() (
    val controllerComponents: ControllerComponents,
    @Named("local") local: Clocky,
    @Named("utc") utc: Clocky
) extends BaseController {
  def now(): Action[AnyContent] = Action {
    Ok(s"local ${local.now} / utc ${utc.now}")
  }
}

What helped me understand this: Guice does not look bindings up by type. It looks them up by key, and a key is the type plus an optional qualifier. There are now three keys for one trait.

Table of three keys: Clocky bound by @ImplementedBy and used by Greeter; Clocky @Named local bound to SystemClocky; Clocky @Named utc bound to UtcClocky, both used by TimeController
Three keys for one trait. @ImplementedBy only serves the key with no qualifier.

Greeter still asks for plain Clocky, and I kept @ImplementedBy(classOf[SystemClocky]) on the trait for it. That annotation is a default for the unqualified key only. It does nothing for the two named keys. The test reads them with app.injector.instanceOf(BindingKey(classOf[Clocky]).qualifiedWith("utc")) mustBe a[UtcClocky].

The cost of a string

Qualifier names are strings, and the compiler does not check them. In a scratch copy of the project I changed the binding to Names.named("UTC"). It compiled. Then every test in GuiceGraphSpec failed, including the Mongo tests, because the router needs TimeController and the injector could not be created at all.

Guice CreationException: no implementation for services.Clocky annotated with @Named(utc); Did you mean lists @Named(local) and @Named(UTC); requested by TimeController third parameter utc
The real error from the typo. Guice suggests the near miss, but the mistake only shows up when the injector is built.

The "Did you mean?" list points straight at the typo. Note also that Guice did not fall back to @ImplementedBy. A good error is still a runtime error, which is why I want a test that builds the real graph.

Guice's own docs recommend using @Named sparingly and defining your own qualifier annotations for type safety (binding annotations). That annotation needs runtime retention, and Scala annotations are not visible to Java runtime reflection, so in this project it would be a small .java file in app/. I have not built that yet. With two names in one module, the strings are fine for now.

The unit test does not care

TimeControllerSpec calls the constructor directly: new TimeController(stubControllerComponents(), local = FixedClocky(09:00), utc = FixedClocky(07:00)), and expects "local 09:00 / utc 07:00". The qualifiers play no role there. Only Guice reads them. That fits a pattern from this series: the annotations describe the wiring, and plain code and tests can ignore them.

Keep reading