Categories
Kotlin Topics

How to Correctly Bind Instances With Guice

Yesterday I have had a weird experience with Guice. I always thought that I pretty much knew what it did when you restricted yourself to the simpler binding methods, such as toInstance or toProvider. Take a look at this (Kotlin) code:

class GuiceTest {
  private val context = Context().apply {
    field = "foo"
  }

  @Test
  fun `service can be created`() {
    val injector = Guice.createInjector(
      Module { 
        it.bind(Context::class.java).toInstance(context)
      }
    )
    assertThat(
      injector.getInstance(Service::class.java)
        .context.field,
      equalTo("foo")
    )
  }
} 

class Service @Inject constructor(val context: Context) 
class Context { @Inject lateinit var field: String }

This looks pretty much straight-forward: there’s a Service, it has a Context that is to be injected, the context itself has a field that is also to be injected. In the test a context with an initialized field is created, and this context is supplied to Guice when the injector is created.

Now, if you run this code, you will see that the test fails, because the empty string "" does not equal the string "foo". This is certainly true but—why? What the heck is going on?

As it turns out, toInstance does actually more than simply taking the given object and supplying it everywhere a Context needs to be injected. It does that but, and this causes the observed behaviour, it also runs injection on the given object! So what it does is to try to inject a String into field; String does not have any @Inject-able constructors so the default constructor is used which creates the empty string we see in the test: "". So while Guice itself does not create any new objects it overwrites the field in the object that we created in the test.

Luckily, the fix for this is also straight-forward: use a provider. With Kotlin this is painless:

val injector = Guice.createInjector(
  Module {
    it.bind(Context::class.java).toProvider { context }
  }
)

When using a provider the returned value is taken as is. No injection is performed, Guice will simply inject it everywhere a Context is expected—which is what we wanted in the first place.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.