Guice Concise Tutorial

Keywords: Google JDBC MySQL Attribute

Guice It's an open source dependency injection library for Google, smaller and faster than Spring IoC. Elastic search uses Guice extensively. This article briefly introduces the basic concepts and usage of Guice.

Learning objectives

  • Summary: Understand what Guice is and what its characteristics are.
  • Quick Start: Learn about Guice through examples;
  • Core concepts: Understanding Guice's core concepts, such as Binding, Scope and Injection;
  • Best practices: Officially recommended best practices;

Overview of Guice

  • Guice is an open source dependency injection Library of Google. It reduces the use of factory methods and new through Guice, making the code easier to deliver, test and reuse.
  • Guice can help us better design API s, which are lightweight, non-intrusive libraries.
  • Guice is friendly to development and can provide more useful information for analysis when an exception occurs.

Quick Start

Suppose an online booking Pizza website has a billing service interface:

public interface BillingService {
 /**
  * Pay by credit card. Whether the payment is successful or not, transaction information needs to be recorded.
  *
  * @return Transaction receipt. Return success information when payment is successful, otherwise record the cause of failure.
  */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

Get the credit card payment processor and the database transaction logger in the new way:

public class RealBillingService implements BillingService {
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();

    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

The problem with using new is that it makes code coupled and difficult to maintain and test. For example, it is impossible to pay directly with a real credit card in UT. Mock needs a CreditCard Processor. Compared with new, it is easier to think of improvements using factory methods, but factory methods still have problems in testing (because global variables are usually used to save instances, and if not reset in use cases, other use cases may be affected). A better way is to inject dependencies by constructing methods:

public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  public RealBillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

For real web applications, real business processing services can be injected:

public static void main(String[] args) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();
    BillingService billingService
        = new RealBillingService(processor, transactionLog);
    ...
  }

Mock classes can be injected into test cases:

public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard("1234", 11, 2010);

  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService
        = new RealBillingService(processor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, processor.getCardOfOnlyCharge());
    assertEquals(100, processor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

So how do you implement dependency injection through Guice? First, we need to tell Guice that if we find the implementation class of the interface, this can be achieved through modules:

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
    bind(BillingService.class).to(RealBillingService.class);
  }
}

The module here only needs to implement the Module interface or inherit from AbstractModule, and then set the binding in the configure method (which will be described later). Then just add @Inject annotation to the original construction method to inject:

public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  public RealBillingService(CreditCardProcessor processor,
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}

Finally, let's look at how the main method is called:

public static void main(String[] args) {
    Injector injector = Guice.createInjector(new BillingModule());
    BillingService billingService = injector.getInstance(BillingService.class);
    ...
  }

binding

Connection binding

Connection binding is the most commonly used binding method, which maps a type to its implementation. In the following example, the TransactionLog interface is mapped to its implementation class DatabaseTransactionLog.

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
  }
}

Connection binding also supports chains, such as the following example that finally maps the TransactionLog interface to the implementation class MySqlDatabaseTransactionLog.

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
  }
}

Annotation binding

There may be multiple implementations of one type, such as PayPal and Google Payments in the credit card payment processor, which makes connection binding impossible. At this point, we can achieve this by annotation binding:

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

public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@PayPal CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }
}

// When the injected method parameter has @PayPal annotation, the injected PayPal CreditCardProcessor implementation
bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);

You can see that the annotated With method is used to specify specific annotations for module binding. One problem with this method is that we have to add custom annotations to bind. Based on this Guice, a @Named annotation is built in to satisfy this scenario:

public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }
}

// Implement Checkout CreditCardProcessor when the injected method parameter has @Named annotation and the value is Checkout
bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);

Instance binding

Binding a type to a specific instance rather than an implementation class is done by using unrelated objects such as value objects. If toInstance contains complex logic that results in startup speed, then it should be bound through the @Provides method.

bind(String.class).annotatedWith(Names.named("JDBC URL")).toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class).annotatedWith(Names.named("login timeout seconds")).toInstance(10);

@ Provides method binding

The method return value defined in the module with @Provides annotation is the type of binding mapping.

public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    ...
  }

  @Provides
  TransactionLog provideTransactionLog() {
    DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
    transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
    transactionLog.setThreadPoolSize(30);
    return transactionLog;
  }

  @Provides @PayPal
  CreditCardProcessor providePayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
    PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
    processor.setApiKey(apiKey);
    return processor;
  }
}

Provider binding

If the binding logic becomes more and more complex using the @Provides method, it can be implemented through Provider binding, an implementation class that implements the Provider interface.

public interface Provider<T> {
  T get();
}

public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
  private final Connection connection;

  @Inject
  public DatabaseTransactionLogProvider(Connection connection) {
    this.connection = connection;
  }

  public TransactionLog get() {
    DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
    transactionLog.setConnection(connection);
    return transactionLog;
  }
}

public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(TransactionLog.class).toProvider(DatabaseTransactionLogProvider.class);
  }
}

Targetless binding

Targetless binding can be used when we want to provide a specific class to the injector.

bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);

Constructor binding

3.0 Additional bindings for classes provided by third parties or for multiple constructors participating in dependency injection. The constructor can be explicitly invoked through the @Provides method, but there is a limitation to this approach: AOP cannot be applied to these instances.

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {
    try {
      bind(TransactionLog.class).toConstructor(DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
    } catch (NoSuchMethodException e) {
      addError(e);
    }
  }
}

Range

By default, Guice returns a new instance each time, which can be configured by Scope. Common scopes are singletons (@Singleton), sessions (@SessionScoped) and requests (@RequestScoped), and can be extended through custom scopes.

Scope annotations can be specified in the implementation class, @Provides method, or at binding time (with the highest priority):

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

// scopes apply to the binding source, not the binding target
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);

@Provides @Singleton
TransactionLog provideTransactionLog() {
    ...
}

In addition, Guice has a special singleton pattern called hungry singleton (as opposed to lazy loading singleton):

// Eager singletons reveal initialization problems sooner, 
// and ensure end-users get a consistent, snappy experience. 
bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();

injection

The requirement of dependency injection is to separate behavior from dependency. It recommends that dependency injection be used instead of finding it through factory class methods. Injection methods usually include constructor injection, method injection, attribute injection and so on.

// Constructor Injection
public class RealBillingService implements BillingService {
  private final CreditCardProcessor processorProvider;
  private final TransactionLog transactionLogProvider;

  @Inject
  public RealBillingService(CreditCardProcessor processorProvider,
      TransactionLog transactionLogProvider) {
    this.processorProvider = processorProvider;
    this.transactionLogProvider = transactionLogProvider;
  }
}

// Method injection
public class PayPalCreditCardProcessor implements CreditCardProcessor {
  private static final String DEFAULT_API_KEY = "development-use-only";
  private String apiKey = DEFAULT_API_KEY;

  @Inject
  public void setApiKey(@Named("PayPal API key") String apiKey) {
    this.apiKey = apiKey;
  }
}

// Attribute injection
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
  @Inject Connection connection;

  public TransactionLog get() {
    return new DatabaseTransactionLog(connection);
  }
}

// Optional injection: no error when no mapping is found
public class PayPalCreditCardProcessor implements CreditCardProcessor {
  private static final String SANDBOX_API_KEY = "development-use-only";
  private String apiKey = SANDBOX_API_KEY;

  @Inject(optional=true)
  public void setApiKey(@Named("PayPal API key") String apiKey) {
    this.apiKey = apiKey;
  }
}

Auxiliary injection

Assisted Inject is part of the Guice extension, which enhances the use of non-injection parameters by automatically generating factories with @Assisted annotations.

// There are two parameters in RealPayment, startDate and amount, that cannot be injected directly
public class RealPayment implements Payment {
  public RealPayment(
        CreditService creditService,  // from the Injector
        AuthService authService,  // from the Injector
        Date startDate, // from the instance's creator
        Money amount); // from the instance's creator
  }
  ...
}

// One way is to add a factory to construct it.
public interface PaymentFactory {
  public Payment create(Date startDate, Money amount);
}

public class RealPaymentFactory implements PaymentFactory {
  private final Provider<CreditService> creditServiceProvider;
  private final Provider<AuthService> authServiceProvider;

  @Inject
  public RealPaymentFactory(Provider<CreditService> creditServiceProvider,
      Provider<AuthService> authServiceProvider) {
    this.creditServiceProvider = creditServiceProvider;
    this.authServiceProvider = authServiceProvider;
  }

  public Payment create(Date startDate, Money amount) {
    return new RealPayment(creditServiceProvider.get(),
      authServiceProvider.get(), startDate, amount);
  }
}

bind(PaymentFactory.class).to(RealPaymentFactory.class);

// Real PaymentFactory can be reduced through the @Assisted annotation
public class RealPayment implements Payment {
  @Inject
  public RealPayment(
        CreditService creditService,
        AuthService authService,
        @Assisted Date startDate,
        @Assisted Money amount);
  }
  ...
}

// Guice 2.0
// bind(PaymentFactory.class).toProvider(FactoryProvider.newFactory(PaymentFactory.class, RealPayment.class));
// Guice 3.0
install(new FactoryModuleBuilder().implement(Payment.class, RealPayment.class).build(PaymentFactory.class));

Best Practices

  • Minimizing variability: As far as possible, immutable objects are injected;
  • Injecting only direct dependencies: no need to inject an instance to get the real needed instances, which increases complexity and is not easy to test;
  • Avoiding cyclic dependency
  • Avoid static state: static state and testability are natural enemies;
  • By default, @Nullable: Guice is prohibited from injecting null objects.
  • Module processing must be fast and without side effects
  • Be careful about IO issues in Providers binding: Providers do not check exceptions, do not support timeouts, and do not support retries;
  • No branching logic in modules
  • Do not expose constructors as much as possible

Reference material

Posted by blkrt10 on Sun, 14 Jul 2019 17:09:13 -0700