Google’s open source dependency injection is smaller and faster than spring!

Time:2022-7-27

Guice is an open source dependency injection Library of Google, which is smaller and faster than spring IOC. Elasticsearch uses Guice a lot. This article briefly introduces the basic concepts and usage of Guice.

Learning objectives

Overview: understand what Guice is and what its characteristics are;
Get started quickly: learn about Guice through examples;
Core concepts: understand the core concepts involved in Guice, such as binding, scope, and injection;
Best practices: Official recommended best practices;

Guice overview

Guice is Google’s open source dependency injection class library. Guice reduces the use of factory methods and new, making the code easier to deliver, test and reuse;
Guice can help us better design API, which is a lightweight non intrusive class library;
Guice is friendly to development and can provide more useful information for analysis when exceptions occur;

Quick start
Suppose a website that subscribes to pizza online has a billing service interface:

public interface BillingService {
 /**
* payment by credit card. Transaction information needs to be recorded whether the payment is successful or not.
  *
* @return transaction receipt. The success information will be returned when the payment is successful, otherwise, the failure reason will be recorded.
  */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

Use new to obtain the credit card payment processor and database transaction logger:

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 the code coupling difficult to maintain and test. For example, in UT, it is impossible to pay directly with a real credit card. A creditcardprocessor is required for mock. Compared with new, it is easier to think of the improvement of using factory method, but factory method still has problems in testing (because global variables are usually used to save instances, which may affect other use cases if they are not reset in use cases). A better way is to inject dependencies through construction 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 website applications, real business processing service classes can be injected:

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

The mock class can be injected into the test case:

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());
  }
}

How to implement dependency injection through Guice? First of all, we need to tell Guice that if we find the implementation class corresponding to the interface, this can be implemented through the module:

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 introduced later). Then just add the @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 see 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. The following example maps the transactionlog interface to its implementation class databasetransactionlog.

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

Connection binding also supports chaining. For example, the following example finally maps the transactionlog interface to the implementation class MySQL database transactionlog.

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 through one type, such as PayPal payment and Google payment in the credit card payment processor, so it is impossible to bind through connection. At this time, we can implement it through 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 the @paypal annotation, inject the paypalcreditcardprocessor implementation
bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);

We can see that when binding modules, we use the annotatedwith method to specify specific annotations for binding. One problem with this method is that we must add custom annotations for binding. Based on this, Guice has built-in a @named annotation to meet this scenario:

public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@Named(“Checkout”) CreditCardProcessor processor,
      TransactionLog transactionLog) {
    …
  }
}

//When the injected method parameter has the @named annotation and the value is checkout, inject the checkoutcreditcardprocessor implementation
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 used in independent objects (such as value objects). If toinstance contains complex logic that will cause startup speed, it should be bound through the @provides method at this time.

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 using the @provides method becomes more and more complex, 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);
  }
}

No target binding
When we want to provide a specific class to the injector, we can use targetless binding.

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

Constructor binding
The binding added in 3.0 is applicable to classes provided by third parties or multiple constructors participating in dependency injection. Constructors can be explicitly called through the @provides method, but this method has one limitation: 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 will return a new instance every time, which can be configured through the scope. Common scopes include singleton (@single), session (@sessionscoped) and request (@requestscoped). In addition, they can be extended through custom scopes.

The annotation of the scope can be specified in the implementation class, @provides method, or when Binding (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 mode called hungry singleton (compared with 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 is suggested to inject dependency instead of finding it through the method of factory class. Injection methods usually include constructor injection, method injection, attribute injection, etc.

//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 will be reported when the mapping cannot be 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 injection is a part of Guice extension. It enhances the use of non injection parameters through the automatic generation factory of @assisted annotations.

//There are two parameters in realpayment, StartDate and amount, which 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
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);

//Realpaymentfactory 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

Minimize variability: inject immutable objects as much as possible;
Only inject direct dependency: you don’t need to inject an instance to get the instance you really need, which increases complexity and is not easy to test;
Avoid circular dependencies
Avoid static state: static state and testability are natural enemies;
Use @nullable:guice. By default, it is forbidden to inject null objects;
The processing of the module must be fast and free of side effects
Beware of IO problems in providers binding: because providers do not check exceptions, do not support timeout, and do not support retry;
No branching logic in the module
Try not to expose the constructor

Source: zhuanlan.zhihu.com/p/24924391

Recommended Today

Laravel Tutorial – Practical Jam Community Open Source E-commerce API System

Important notice: The open source e-commerce version source code of Laravel + applet has been pulled on github, welcome to submit issue and star 🙂Open source e-commerce server:Laravel API source code Open source e-commerce client:applet source code Introduction to Jam Community IYOYO company was founded in Shanghai in 2011. After 8 years of industry accumulation, […]