Google's open source dependency on injection and storage is smaller and faster than Spring!

Keywords: Java MySQL Google Spring JDBC

Guice, an open-source dependency injection Library of Google, compares with Spring IoC Smaller and faster. Guice is widely used in elastic search. This paper briefly introduces the basic concept and usage of Guice.

Learning objectives

 

  • Overview: understand what Guice is and what characteristics it has;
  • Quick start: learn Guice through examples;
  • Core concepts: understand the core concepts involved in Guice, such as Binding, Scope and Injection;
  • Best practices: officially recommended best practices;

Guice overview

 

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

Quick start

 

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

 

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

 

 

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

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 of using new is to make the code coupling, not easy to maintain and test. For example, in UT, it is impossible to pay directly with a real credit card. You need Mock a credit card processor.

Compared with new, it is easier to think of an improvement to use factory method, but factory method still has problems in testing (because global variables are usually used to save instances, if not reset in use cases, it may affect other use cases).

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 website 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);
    ...
}
 

 

The Mock class can be injected into:

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 the implementation class corresponding to the interface is found, it 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 (described later). Spring Boot constructor parameter binding, Let's take a look at this recommendation.

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 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 bindings

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 uncertain to bind through the connection.

In this case, we can use annotation binding to implement:

@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 exists@PayPal Inject when annotating PayPalCreditCardProcessor Realization
bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);

 

 

You 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, we built 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 exists@Named Note and value is Checkout Time injection CheckoutCreditCardProcessor Realization
bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);
 

 

Instance binding

Bind a type to a concrete instance rather than an implementation class, by using it in a non dependent object, such as a value object. 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 return value of the method defined in the module with the @ supplies 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 of @ Provides method is 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 an injector with a specific class, we can use the aimless binding.

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

 

Constructor binding

The new binding in 3.0 is applicable to classes provided by third parties or multiple constructors participating in dependency injection. Constructors can be called explicitly 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 every time, which can be configured through Scope. Common scopes include Singleton (@ Singleton), session (@ sessionscope) and request (@ requestscope). 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 single instance mode called hungry single instance (compared with lazy loading single instance):

// 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 the behavior and dependency. It suggests that dependency injection should be used instead of 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 is reported 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 injection is a part of Guice extension. It enhances the use of non injection parameters through the @ assisted annotation auto generation factory.

 

// RealPayment There are two parameters in startDate and amount Unable to inject 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 build
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);

// adopt@Assisted Annotations can be reduced RealPaymentFactory
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: immutable objects are injected as much as possible;
  • Direct dependency injection only: it does not need to inject an instance to obtain the real needed instance, which increases the complexity and is not easy to test;
  • Avoid circular dependence
  • Avoid static state: static state and testability are natural enemies;
  • Adopt @ Nullable: Guice to disable injecting null objects by default;
  • Module processing must be fast and have no side effects
  • Beware of IO problems in Providers binding: Provider does not check exceptions, does not support timeout, does not support retry;
  • Do not process branch logic in modules
  • Try not to expose the constructor

I collated Java advanced materials for free, including Java, Redis, MongoDB, MySQL, Zookeeper, Spring Cloud, Dubbo high concurrency distributed and other tutorials, a total of 30G, which needs to be collected by myself.
Portal: https://mp.weixin.qq.com/s/igMojff-bbmQ6irCGO3mqA

Posted by darkhorn on Wed, 25 Dec 2019 01:48:02 -0800