首页 Java知识正文

Google 开源的依赖注入库,比 Spring 更小更快!

admin Java知识 2021-01-17 66 0

来源:zhuanlan.zhihu.com/p/24924391


  • Guice是Google开源的一个依赖注入类库,相比于Spring IoC来说更小更快。Elasticsearch大量使用了Guice,本文简单的介绍下Guice的基本概念和使用方式。

    学习目标

    Guice概述

    快速开始

    假设一个在线预订Pizza的网站,其有一个计费服务接口:

    public interface BillingService {
    /**
    * 通过信用卡支付。无论支付成功与否都需要记录交易信息。
    *
    * @return 交易回执。支付成功时返回成功信息,否则记录失败原因。
    */
    Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
    }

    使用new的方式获取信用卡支付处理器和数据库交易日志记录器:

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

    使用new的问题是使得代码耦合,不易维护和测试。比如在UT里不可能直接用真实的信用卡支付,需要Mock一个CreditCardProcessor。相比于new,更容易想到的改进是使用工厂方法,但是工厂方法在测试中仍存在问题(因为通常使用全局变量来保存实例,如果在用例中未重置可能会影响其他用例)。更好的方式是通过构造方法注入依赖:

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

    对于真实的网站应用可以注入真正的业务处理服务类:

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

    而在测试用例中可以注入Mock类:

    public class RealBillingServiceTest extends TestCase {

      private final PizzaOrder order = new PizzaOrder(100);
      private final CreditCard creditCard = new CreditCard("1234"112010);

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

    那通过Guice怎么实现依赖注入呢?首先我们需要告诉Guice如果找到接口对应的实现类,这个可以通过模块 来实现:

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

    这里的模块只需要实现Module接口或继承自AbstractModule,然后在configure方法中设置绑定 (后面会继续介绍)即可。然后只需在原有的构造方法中增加@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());
        }
      }
    }

    最后,再看看main方法中是如何调用的:

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

    绑定

    连接绑定

    连接绑定是最常用的绑定方式,它将一个类型和它的实现进行映射。下面的例子中将TransactionLog接口映射到它的实现类DatabaseTransactionLog。

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

    连接绑定还支持链式,比如下面的例子最终将TransactionLog接口映射到实现类MySqlDatabaseTransactionLog。

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

    注解绑定

    通过一个类型可能存在多个实现,比如在信用卡支付处理器中存在PayPal的支付和Google支付,这样通过连接绑定就搞不定。这时我们可以通过注解绑定来实现:

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

    public class RealBillingService implements BillingService {

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

    // 当注入的方法参数存在@PayPal注解时注入PayPalCreditCardProcessor实现
    bind(CreditCardProcessor.class).annotatedWith(PayPal.class).to(PayPalCreditCardProcessor.class);

    可以看到在模块的绑定时用annotatedWith方法指定具体的注解来进行绑定,这种方式有一个问题就是我们必须增加自定义的注解来绑定,基于此Guice内置了一个@Named注解满足该场景:

    public class RealBillingService implements BillingService {

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

    // 当注入的方法参数存在@Named注解且值为Checkout时注入CheckoutCreditCardProcessor实现
    bind(CreditCardProcessor.class).annotatedWith(Names.named("Checkout")).to(CheckoutCreditCardProcessor.class);

    实例绑定

    将一个类型绑定到一个具体的实例而非实现类,这个通过是在无依赖的对象(比如值对象)中使用。如果toInstance包含复杂的逻辑会导致启动速度,此时应该通过@Provides方法绑定。

    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方法绑定

    模块中定义的、带有@Provides注解的、方法返回值即为绑定映射的类型。

    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绑定

    如果使用@Provides方法绑定逻辑越来越复杂时就可以通过Provider绑定(一个实现了Provider接口的实现类)来实现。

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

    无目标绑定

    当我们想提供对一个具体的类给注入器时就可以采用无目标绑定。

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

    构造器绑定

    3.0新增的绑定,适用于第三方提供的类或者是有多个构造器参与依赖注入。通过@Provides方法可以显式调用构造器,但是这种方式有一个限制:无法给这些实例应用AOP。

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

    范围

    默认情况下,Guice每次都会返回一个新的实例,这个可以通过范围(Scope)来配置。常见的范围有单例(@Singleton)、会话(@SessionScoped)和请求(@RequestScoped),另外还可以通过自定义的范围来扩展。

    范围的注解可以应该在实现类、@Provides方法中,或在绑定的时候指定(优先级最高):

    @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() {
        ...
    }

    另外,Guice还有一种特殊的单例模式叫饥饿单例(相对于懒加载单例来说):

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

    注入

    依赖注入的要求就是将行为和依赖分离,它建议将依赖注入而非通过工厂类的方法去查找。注入的方式通常有构造器注入、方法注入、属性注入等。

    // 构造器注入
    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;
      }
    }

    // 方法注入
    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;
      }
    }

    // 属性注入
    public class DatabaseTransactionLogProvider implements Provider<TransactionLog{
      @Inject Connection connection;

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

    // 可选注入:当找不到映射时不报错
    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;
      }
    }

    辅助注入

    辅助注入(Assisted Inject)属于Guice扩展的一部分,它通过@Assisted注解自动生成工厂来加强非注入参数的使用。

    // RealPayment中有两个参数startDate和amount无法直接注入
    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
      }
      ...
    }

    // 一种方式是增加一个工厂来构造
    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);

    // 通过@Assisted注解可以减少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.classRealPayment.class).build(PaymentFactory.class));

    最佳实践



    版权声明

    本文仅代表作者观点,不代表本站立场。
    本文系作者授权发表,未经许可,不得转载。

    评论