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