Guice Concepts

Categories: Java

Intro

Guice is a Java library for performing Dependency Injection. Guice is really pretty simple to understand; here you will find the basic concepts that I wish somebody had told me when I first started working with Guice..

The important things to grasp are:

  • How an Injector works
  • How a Provider works
  • That class implies scope (mostly)
  • That objects are created in trees (mostly)

A Recap on Dependency Injection

In short, Dependency Injection is a technique that allows your regular application logic to avoid:

  • creating objects of specific types using “new” calls (which binds your code to concrete implementations of that type)
  • using lookup functions (whether JNDI or hand-rolled) to find shared objects
  • using factories that return an abstract type

The major benefits are:

  • mocking of objects for unit tests becomes easier
  • refactoring of code becomes easier
  • configuration of factories is compile-time checked
  • application logic is not swamped by lots of “new” operators or lookup calls
  • the objects a method depends on are explicit in the method prototypes, not buried in the method implementation.

A Recap on Object Scopes

The scope of an object describes when it is created, and when it is garbage-collected (aka its “lifecycle”).

Common scopes are:

  • No scope (or in Spring terms, “prototype” scope)
  • Singleton Scope
  • Session Scope (HTTP only)
  • Request Scope (Java Servlets only)
  • Page Scope (JSP only)

Objects with “no scope” may live for the duration of a single method call (ie are created as a local variable, and are no longer referenced by anything after the method returns). Alternatively, they are attached to some “owning” object, and die when the owning object dies (or when the owning object explicitly discards them).

Singleton scope means there is only one instance of that type; it is either created when first referenced, or simply when the application starts. These objects are generally not discarded until the application ends.

Session-scope objects die when the current user “logs out”, and are shared between objects belonging to a single user, but not between users.

You can guess the others. Custom scopes can also be defined as needed.

How an Injector works

Conceptually, a Guice Injector instance holds a table of (Class -> Provider for that class).

User code can then ask the injector to return an instance of any specified class; the injector simply finds and executes the associated Provider object.

Normally, an application has a single Injector which is configured at application startup via a list of Module objects. The Module objects just call their inherited ‘bind’ method to register appropriate providers.

There is a different type of Provider for each of the possible scopes:

  • a “no scope” provider simply calls the constructor for the class.
  • a “singleton” provider simply returns a cached instance of the class (or creates it on first call if none already exists).
  • a “session” provider looks for an instance cached in the http session, and returns that if present - else creates one and caches it.
  • etc

Actually, the key into the map held by the injector is a (Class, Annotation) pair, allowing multiple providers to be defined for a class. The caller can then say “return me the output of the provider for variant X of this class”, and the matching provider is used. The ‘default’ provider for a class has annotation=null.

By default, Guice assumes that an object has “no scope”; this is correct in 95% of cases. If a Guice Injector is asked to produce an instance of a concrete class that it does not yet know about, then it simply creates a suitable no-scope Provider object and registers it automatically. This assumption massively cuts down on the number of bindings needed to configure Guice in the Modules - the only bindings a programmer needs to explicitly declare are those for classes with scopes other than no-scope, and to map abstract classes or interfaces to the appropriate concrete classes.

How a Provider Works

Suppose class N has a constructor of form

 public N(P param1, Q param2);

Binding this class with no scope will create a Provider that works as if it were implemented like this:

public class Provider<N> {
  public N get() {
    P p = injector.get(P);
    Q q = injector.get(Q);
    return new N(p, q);
  }
}

What instances of P and Q are used depends simply upon how they are bound in the injector map. They might themselves be no-scope, or might be singletons, etc. If P and Q are concrete types which have no scope then, as noted earlier, the Guice injector will automatically create the appropriate provider for them; otherwise you will also need to declare a binding for them.

A singleton-scoped provider simply acts as if it were implemented like this (with appropriate locking added of course):

public class Provider<N> {
  private N instance;

  public N get() {
    if (instance == null) {
      P p = injector.get(P);
      Q q = injector.get(Q);
      instance = new N(p, q);
    }
    return instance;
  }
}

Where the standard providers are not appropriate for some reason, you always have the option of writing your own Provider<N> implementation instead.

It is possible for a module to declare bindings for P and Q just for the purposes of instantiating class N. These are known as “private bindings”. This is really useful when P and Q are internal implementation details that other code shouldn’t be using anyway.

Critical Observation #1: Class implies Scope (mostly)

In the vast majority of cases, all instances of a class have the same scope.

The singleton case is trivial : there is only one instance.

If an object of class N is used as a locally private “helper object” in one location (ie has “no scope”), then it is unlikely that elsewhere in the application a common instance of N is shared by two different objects.

If an object of class P is stored in a user’s HTTP session by one part of an application (ie has session scope), then it is unlikely that elsewhere in the application there will be an instance of P which is not associated with an HTTP session.

This means that the “scope” for a type can be declared once - or in other words, that the Injector map really can just have a single Class->Provider entry for the vast majority of classes. This keeps the amount of code needed for modules to declare all bindings to a very reasonable size.

Of course there is an “escape hatch” for the cases where this is not true - by having a Module explicitly define the variants needed, and then by annotating those places where the non-default variant should be supplied.

Critical Observation #2: Objects are generally created as ‘trees’

In most cases, calls to “new” are from the constructors of other objects (or their initialisation methods which are invoked from the constructor).

So in effect, there are a very few places in an application where the root of an object tree is created - which can then trigger creation of a number of other objects.

These few critical points need to explicitly use a Guice Injector or Provider. However there are really very few of them in a normal app, so the “intrusiveness” of Guice (or any other Dependency Injection framework) is not high.

Summary

Dependency injection has many useful properties (listed in the intro).

A drawback of DI is that somewhere in the application there needs to be a call to the DI framework to do this injection. The fact that objects are naturally created as “trees” makes this burden reasonable.

A drawback of DI is that the “wiring rules” must be declared by the programmer somewhere. The fact that class implies scope and the fact that most objects have “no scope” makes this burden reasonable.

The result is that Guice configuration modules are usually quite small, and only contain code that would generally have existed in the application in some form anyway (via lookups or similar).

So: little overhead, great benefits. And really quite easy to understand once you get the idea of the Injector and Provider.

Guice is a great little library.

References