An overview of Guice

Google's Guice is a Java-based dependency injection framework, which means approximately nothing to people who aren't familiar with it.

Google's Guice is a way to build a graph of dependencies so you can instantiate complex objects made of simpler parts.

Google's Guice is a replacement for the new keyword for many of your objects. Bizzarre, right?

In a more tangible sense, Guice is a tool that's used in a lot of Java development. Beyond that, it positively impacts your code resulting in a reduction of boilerplate and an increase in testability.

What is dependency injection?

Dependency injection (DI) is a way to give an object it's dependencies. There are a few flavors of this, but first, let's just get on the same page.

A dependency of a class is something it needs to operate. It will encapsulate some amount of functionality whose implementation the dependant class can ignore.

A basic example of this would be a BillingProcessor class might have a dependency on SalesTaxCalculator. It doesn't need to know how this works and the Law of Demeter (aka the priniciple of least knowledge) says that we shouldn't care how it works.

Dependency injection (the means of giving a class it's dependencies) can take two main forms: constructor injection and setter injection. They're pretty much what they sound like. Constructor injection is passing in the dependencies to the constructor. With setter injection, you create the object with an empty constructor and call setFoo to add the Foo dependency.

I personally prefer constructor injection, because you don't have an object which is in an intermediate invalid state which eliminates lots of checking to ensure the object is valid.

DIY Injection Examples

  • Constructor Injection
    public class BillingProcessor {
      private SalesTaxCalculator taxCalc;
    
      public BillingProcessor(SalesTaxCalculator taxCalc) {
        this.taxCalc = taxCalc;
      }
    
      public bill(...) { /* ... */ }
    }
    
  • Setter Injection
    public class BillingProcessor {
      private SalesTaxCalculator taxCalc;
    
      public BillingProcessor() {}
    
      public void setTaxCalculator(SalesTaxCalculator taxCalc) {
        this.taxCalc = taxCalc;
      }
    
      public bill(...) { /* ... */ }
    }
    

Doing it without Guice

You can do DI without Guice, but it eventually gets a bit complicated. For our example, we'll design an event-based notification service.

public interface EventCoordinator {
  public Boolean schedule(Event ev);
}

public interface Emailer {
  public void email(User u, String title, String body);
}

The implementation of this is pretty straight forward.

public EventScheduler implements EventCoordinator {
  private Emailer emailer;

  public EventScheduler() {
    this.emailer = new EmailService();
  }

  public Boolean schedule(Event ev) {
    try {
      for (User u : ev.recipients()) {
        this.emailer.email(u, ev.title, ev.body);
      }
    } catch (Exception exc) {
      return false;
    }
    return true;

  }
}

This suffers from some problems, in that our email service is hard coded, which means when we write our unit tests, we'll be sending real emails to people, which isn't good. Instead of this, we need to sub out the emailer implementation at test time.

public EventScheduler implements EventCoordinator {
  private Emailer emailer;

  public EventScheduler(Emailer em) {
    this.emailer = em;
  }

  public Boolean schedule(Event ev) {
    /* ... */
  }
}

This is solid. This is easily testable by creating a fake implementation of the Emailer. As this code grows beyond toy examples and the dependency graph of your dependencies becomes longer, you end up with code that looks more like a yak shave.

new EventScheduler(
  new RealEmailService(
    new EmailServiceTokenFetcher(username, password),
    new RateLimiter(maxEmailsPerHour),
    new EmailTemplate(new FileLocator("template/dir/path/"),
      new UserPreferenceSelector(new DatabaseConnection(...), ...))
  )
)

This is where Guice comes in. You tell Guice that you want an implementation of Emailer and it goes and figures out all the things it needs to do to make one.

Using Guice

Once setup, Guice is pretty straight forward to use. In each of your constructors that need to have something injected in them, you just add an @Inject annotation and that tells Guice to do it's thing.

public EventScheduler implements EventCoordinator {
  private Emailer emailer;

  @Inject
  public EventScheduler(Emailer em) {
    this.emailer = em;
  }

  public Boolean schedule(Event ev) {
    /* ... */
  }
}

Guice figures out how to give you an Emailer based on the type. If it's a simple object, it'll instantiate it and pass it in. If it has dependencies, it will resolve those dependencies, pass them into it's constructor, then pass the resulting object into your object.

When you want to actually get your instance of EventScheduler, you just need to create an injector and ask for one. This usually lives in your application's main() method. Guice will fill in this object's dependencies and any of its dependencies and so on.

Injector injector = Guice.createInjector(new MyModule()); // modules are the next section
EventScheduler scheduler = injector.getInstance(EventScheduler.class);
scheduler.schedule(...)

But wait.. emailer is an interface, not an instantiatable thing. How does Guice know how to give you one of those?

Configuring Guice

Guice is configured with one or more "modules". These inherit from AbstractModule.

public class MyModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(Emailer.class).to(RealEmailService.class);
  }
}

You can also tell Guice how to create specific object with don't have @Inject annotations (likely because they're a third party library) by adding methods to your module annotated with @Provides. These methods can also take in arguments which are things Guice knows how to build for you.

public class MyModule extends AbstractModule {
  String TEMPLATE_PATH = '/path/to/templates';

  @Provides
  RateLimiter generateRateLimiter(UserPreferenceSelector prefSel) {
    return new EmailTemplate(new FileLocator(TEMPLATE_PATH), prefSel);
  }

  /* ...configure... */
}

So this all works based on types. What happens if you have multiple versions of the same type? This happens pretty often with configuration variables which are inevitably ~String~s.

Dealing with different versions of the same type

Guice solves the problem of having two different String objects you want to inject through "named annotations".

public EventScheduler implements EventCoordinator {
  private Emailer emailer;
  private String defaultSender;

  @Inject
    public EventScheduler(Emailer em, @Named("default_from") String defaultFrom) {
    this.emailer = em;
    this.defaultSender = defaultFrom;
  }

  /* ... */
}

You can bind these manually in your module, but there are libraries which will pull these from your configuration that make use of Names.bindProperties.

public class MyModule extends AbstractModule {
  @Provides
  RateLimiter generateRateLimiter(UserPreferenceSelector prefSel,
                                  @Named("template-path") String templatePath) {
    return new EmailTemplate(new FileLocator(templatePath), prefSel);
  }

  public void configure() {
    bind(String.class).annotatedWith(Names.named("template-path")).toInstance("/path/to/template");
    /* ...more things... */
  }
}

There isn't any static analysis of Guice to see if you actually have bindings for the annotations you said you did. This means that failures in Guice happen at run-time. Given that developers can fat-finger strings not referenced as constants, Guice also allows you to use your own annotations.

package example.pizza;

import com.google.inject.BindingAnnotation;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;

@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
public @interface TemplatePath {}

Then you can use it similar to how you'd use the @Named annotation.

public class MyModule extends AbstractModule {
  @Provides
  RateLimiter generateRateLimiter(UserPreferenceSelector prefSel,
                                  @TemplatePath String templatePath) {
    return new EmailTemplate(new FileLocator(templatePath), prefSel);
  }

  public void configure() {
    bind(String.class).annotatedWith(TemplatePath.class).toInstance("/path/to/template");
    /* ...more things... */
  }
}

How instantiation works

Guice, by default, will instantiate a new version of your object every time you request it. This is perfectly fine for things with minimal configuration. Some objects, such as database connections, are limited resources. To solve this, Guice provides different Scopes. Two common ones are @Singleton and @RequestScoped.

@Singleton bindings mean there will only be one instance of the object, which is passed around to all dependants. It can be applied to the class, the @Provides method, or manually in the configure method.

// class based
@Singleton
public class InMemoryTransactionLog implements TransactionLog {
  /* everything here should be threadsafe! */
}

// within the module's configure() method
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);

// in your module
@Provides @Singleton
TransactionLog provideTransactionLog() {
  ...
}

IMPORTANT Objects which are annotated as singletons must be threadsafe. This is a transitive property, meaning that all of the dependencies of that module requires must also be threadsafe.

@RequestScoped annotations have default servlet extensions which will generate a new object on each request.

Testing

Depencency injection makes for really great test seams, where you can pass in your own mocked objects. Beyond that, you may want to test your actual Guice configuration or use your Guice configuration for integration tests.

Testing your Guice bindings

Because Guice bindings are resolved at runtime, the best mechanism for testing them is simply writing end-to-end tests. If Guice can't find the required bindings, it will crash early.

If you have multiple Guice modules with inter-relationships (yes, that's totally a thing), you can test how they work together by using requireBinding in the dependant module's configure method. Just list the classes you expect to be available and it'll error out if they're not there.

Testing modules using Guice

Sometimes, you want most of the bindings from a Guice module, but you want to override just one. A common example is the endpoint of a production server or credentials with the test version. For that, you can use Modules.override. This will take all of the items in MyProductionModule, except the ones defined in the inline-created AbstractModule we made.

Modules.override(new MyProductionModule()).with(new AbstractModule {
    @Override
    public void configure() {
      // overrides a binding for Credentials.class which was defined in MyProductionModule.
      bind(Credentials.class).toInstance(new DevelopmentCredentials());
    }
  });

Comparison vs Spring

Spring is a tool which some folks have experience with. It predates Guice by 5 years. It's a primarly XML oriented framework, but it does a lot more than Guice. It does some dependency injection, but also comes with a full MVC web framework.

Guice, while being simpler (by virtue of being focused on a small task and having hindsight), also manages to accomplish more in less code. Because it's just normal Java code, it also has good compiler checking for ensuring your constructors are well formed.

There is support for having both Guice and Spring play nicely together.

AOP - Aspect Oriented Programming

Aspect Oriented Programming (AOP) is a mechanism to wrap the functionality of existing code. It's a reasonable fit for things that have cross-cutting concerns, like authorization or metering.

In short, it allows you to intercept the call of a method and do something before it, instead of it, or use the return value of the intercepted method for some purpose. This is similar to defadvice for elisp hackers or python's decorators.

There are other AOP frameworks (AspectJ being the fastest for how it does bytecode weaving) which Guice interoperates with.

If you're curious about this and how it works, check out Guice's documentation on it.

© 2012 - 2023 · Home — Theme Simpleness