Skip to content

NeuronDIAtGuiceForScalaTutorial

Christian Schlichtherle edited this page Dec 10, 2018 · 2 revisions

Neuron DI @ Guice For Scala - Tutorial

This tutorial explains how to use Neuron DI @ Guice For Scala. It is assumed that you are already familiar with Guice. If not, please consult the Guice Wiki first or consider using Neuron DI For Scala standalone.

Neuron DI @ Guice provides an API which extends Guice with lazy dependency resolution and caching. It also extends the domain specific language (DSL) for Guice binding definitions to ease some pain.

Note that Neuron DI does not at all compete with Guice: As you will see, Neuron DI is complementary to JSR-330. Guice is an implementation of JSR-330, so combining forces makes perfect sense if you are using Guice anyway. If you are not using Guice however, Neuron DI @ Guice adds nothing which could not be done with standalone Neuron DI.

Pure Guice

Let's start with pure Guice and then see how Neuron DI @ Guice can improve things. Consider the following contrived example for a Greeting trait:

trait Greeting {

    /** Returns a greeting message for the given entity. */
    def message(entity: String): String
}

The Greeting trait is implemented by GuiceGreeting, which depends on the Formatter trait:

class GuiceGreeting @Inject() (formatter: Formatter) extends Greeting {

  def message(entity: String): String = formatter format entity
}

trait Formatter {

    /** Returns a text which has been formatted using the given arguments. */
    def format(args: AnyRef*): String
}

Last but not least, the Formatter trait is implemented by the GuiceFormatter class:

class GuiceFormatter @Inject() (@Named("format") format: String) extends Formatter {

  def format(args: AnyRef*): String = String.format(format, args: _*)
}

In order to disambiguate the format string from other strings when configuring the bindings later, we need to add a qualifier annotation. In this case, its @Named("format").

Now we can define a module to bind the components:

class GuiceGreetingModule extends AbstractModule {

  protected def configure() {
    bind(classOf[Greeting]).to(classOf[GuiceGreeting]).in(classOf[Singleton])
    bind(classOf[Formatter]).to(classOf[GuiceFormatter])
    bindConstant.annotatedWith(Names.named("format")).to("Hello %s!")
  }
}

Note that the qualifying annotation is referenced by Names.named("format") to specifically bind the format string to "Hello %s!". Note also that Greeting is configured to be a singleton, which is required to pass the following test:

class GuiceGreetingModuleSpec extends GreetingModuleSpec {

  override def module: Module = new GuiceGreetingModule
}

abstract class GreetingModuleSpec extends WordSpec {

  protected def module: Module

  protected lazy val injector: Injector = Guice createInjector module

  protected def getInstanceOf[A : ClassTag]: A = (injector getInstance classTag[A].runtimeClass).asInstanceOf[A]

  "Make a greeting" in {
    val greeting = getInstanceOf[Greeting]
    greeting message "world" shouldBe "Hello world!"
    getInstanceOf[Greeting] should be theSameInstanceAs greeting
  }

  "Make a formatter" in {
    val formatter = getInstanceOf[Formatter]
    formatter format "world" shouldBe "Hello world!"
    getInstanceOf[Formatter] should not be theSameInstanceAs(formatter)
  }
}

Note that this is actually an integration test which tests GuiceGreetingModule with its configured implementations of the Greeting and Formatter traits. In practice, you might want to write a unit test for GuiceGreeting and GuiceFormatter instead and skip testing the GuiceGreetingModule.

Note also that the two test cases are almost identical, except for the scopes. This is because GuiceGreeting.greeting(String) just forwards its parameter to Formatter.format(AnyRef*). In practice, this should be different or otherwise you wouldn't need the whole class.

Neuron DI @ Guice

Now let's see how we can extend Guice with Neuron DI. Please read the project setup page first.

Next, import the Neuron DI @ Guice for Scala API:

import global.namespace.neuron.di.guice.scala.*;

Thanks to trait segregation in the design of this example, the Greeting and Formatter traits don't have to change, only their implementations do. So here's the implementation of the Greeting trait using Neuron DI:

@Neuron
trait NeuronGreeting extends Greeting {

  val formatter: Formatter

  def message(entity: String): String = formatter format entity
}

The most notable difference to the GuiceGreeting class is that NeuronGreeting is actually a trait: This improves its reusability because now you can mix-in NeuronGreeting to (non-final) classes. Note that this is not a requirement though: NeuronGreeting could just be an (abstract) class as well. In comparison, injection into traits is not possible with Guice because it does not know how Scala compiles traits to byte code for interfaces and classes.

Let's have a closer look at the annotation: A class or trait annotated with @Neuron is called a "neuron class" or "neuron trait". The annotation signals that the neuron class or trait is subject to dependency injection. Within a neuron class or trait, any abstract, parameterless def or val declaration is called a "synapse value". No special annotation is required for synapse values however, so in this example, formatter is a synapse value. Synapse values are the injection points in Neuron DI, so Neuron DI will resolve and return an instance of the return type when the value is accessed. As an implication of this design, all dependency resolution is inherently lazy.

Next, let's look at the implementation of the Formatter trait:

@Neuron
trait NeuronFormatter extends Formatter {

  private type Named = javax.inject.Named @getter

  @Named("format") val theFormat: String

  def format(args: AnyRef*): String = String.format(theFormat, args: _*)
}

Again, NeuronFormatter is a trait because it allows us to use it as a mix-in. Just as before, the @Neuron annotation is applied to signal that Neuron DI should take care of lazy dependency resolution. The most notable difference is the usage of the @Named("format") annotation on the synapse value theFormat instead of the constructor parameter GuiceFormatter(String).

Next, let's look at the changes required in the Guice module:

class NeuronGreetingModule extends NeuronModule {

  def configure() {
    bindClass[Greeting].toNeuronClass[NeuronGreeting].inScope[Singleton]
    bindClass[Formatter].toNeuronClass[NeuronFormatter]
    bindConstantNamed("format").to("Hello %s!")
  }
}

Note the bindClass[A].toNeuronClass[B <: A] statements: Unfortunately, as of Guice 4.1.0, you cannot extend it with custom just-in-time bindings, so we have to explicitly bind our Neuron classes and traits using a custom statement like this. Without this call, Guice would not know how to create instances of the neuron traits NeuronGreeting and NeuronFormatter at runtime. Removing this call results in the following exception when creating the Guice Injector in the GreetingModuleSpec:

com.google.inject.CreationException: Guice creation errors:

1) No implementation for global.namespace.neuron.di.guice.java.test.NeuronGreeting was bound.
  at global.namespace.neuron.di.guice.java.test.NeuronGreetingModule.configure(NeuronGreetingModule.java:28)

2) No implementation for global.namespace.neuron.di.guice.java.test.NeuronFormatter was bound.
  at global.namespace.neuron.di.guice.java.test.NeuronGreetingModule.configure(NeuronGreetingModule.java:29)

2 errors

    at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:435)
    at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:154)
    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:106)
    at com.google.inject.Guice.createInjector(Guice.java:95)
...

Finally, we can test the NeuronGreetingModule:

class NeuronGreetingModuleSpec extends GreetingModuleSpec {

  override def module: Module = new NeuronGreetingModule
}
Clone this wiki locally