Search code examples
kotlinguice

guice injection: difference among getBinding/getExistingBinding/getProvider and getInstance


I have a PropertiesModule that extends AbstractModule and contains application constants that I use throughout the project:

class PropertiesModule: AbstractModule(), Serializable {
  companion object {
    const val APP_NAME = "MyAppName"
    ...
  }
  override fun configure() {
    ...
  }
}

Then I use the PropertiesModule to create the injector:

...
val injector = Guice.createInjector(
  PropertiesModule(),
  ...
)

And later when I use the injector to get the property value, I have multiple choices. I could do:

val appName = injector.getInstance(
  Key.get(String::class.java, Names.named(PropertiesModule.APP_NAME))
)

or

val appName = injector.getExistingBinding(
  Key.get(String::class.java, Names.named(PropertiesModule.APP_NAME))
).provider.get()

or

val appName = injector.getProvider(
  Key.get(String::class.java, Names.named(PropertiesModule.APP_NAME))
).get()

I read that in general getInstance should be avoided. But I'm not sure what is the difference among them.


Solution

  • If you've just created your Injector, use getInstance. Otherwise, try to get your object from a narrower injection.


    getBinding and getExistingBinding are effectively both for reflective purposes:

    [Binding is] a mapping from a key (type and optional annotation) to the strategy for getting instances of the type. This interface is part of the introspection API and is intended primarily for use by tools.

    Though it would probably work to some degree, I'd recommend against getExistingBinding, if only because it has the counterintuitive behavior of not creating Just-In-Time bindings if they do not already exist, as described in the getExistingBinding docs:

    Unlike getBinding(Key), this does not attempt to create just-in-time bindings for keys that aren't bound. This method is part of the Guice SPI and is intended for use by tools and extensions.

    getProvider(Class<T>) will procure a Provider<T> from the graph, which is like a single-key Injector that can only create or retrieve instances of a single key from the Injector. Think of it as creating a lambda that calls getInstance for a specific Key, and nothing more.

    getInstance is the most important method on Injector, and I expect all Guice applications to call it at least once. If you've just created your Injector, you'll presumably want to either get an instance from it, or inject the @Inject fields of an object. The former happens with getInstance, and the latter with injectMembers. In my opinion, that's the best way to get the first object out of your graph.

    However, and this is where the advice you heard comes in: You should probably not use the Injector after that first getInstance or injectMembers call. This is because the goal in Guice is to make independent components that express their dependencies narrowly so you can replace those dependencies easily. This is the major benefit of dependency injection. Though you have the options of stashing the Injector somewhere static, or injecting the Injector itself into your dependencies, that prevents you from knowing exactly what your class's dependencies are: with an Injector injection, you can inject anything in the world.

    To demonstrate (please forgive my Kotlin inexperience):

    class BadIdea @Inject constructor(injector: Injector) {
      fun doSomething() {
        // Bad idea: you don't express this dependency anywhere, and this code
        // is nearly unusable without a real Guice Injector
        val appName = injector.getInstance(
          Key.get(String::class.java, Names.named(PropertiesModule.APP_NAME))
        )
      }
    }
    
    class WorseIdea {
      fun doSomething() {
        // Worse idea: same as the above, but now you rely on a static!
        val appName = StaticInjectorHolder.getInjector().getInstance(
          Key.get(String::class.java, Names.named(PropertiesModule.APP_NAME))
        )
      }
    }
    
    class GoodIdea @Inject constructor(@Named(PropertiesModule.APP_NAME) appName: String) {
      fun doSomething() {
        // Good idea: you express your dep, and anyone can call the constructor
        // manually, including you in your unit tests.
      }
    }