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

2022-10-11,,,,

google开源的一个依赖注入类库guice,相比于spring ioc来说更小更快。elasticsearch大量使用了guice,本文简单的介绍下guice的基本概念和使用方式。

学习目标

 

  • 概述:了解guice是什么,有什么特点;
  • 快速开始:通过实例了解guice;
  • 核心概念:了解guice涉及的核心概念,如绑定(binding)、范围(scope)和注入(injection);
  • 最佳实践:官方推荐的最佳实践;

guice概述

 

  • guice是google开源的依赖注入类库,通过guice减少了对工厂方法和new的使用,使得代码更易交付、测试和重用;
  • guice可以帮助我们更好地设计api,它是个轻量级非侵入式的类库;
  • 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", 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());
  }
}
 

 

那通过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方法中设置绑定(后面会继续介绍)即可。spring boot 构造器参数绑定,这篇推荐大家看下。

然后只需在原有的构造方法中增加@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.class, realpayment.class).build(paymentfactory.class));
 

 

 

最佳实践

  • 最小化可变性:尽可能注入的是不可变对象;
  • 只注入直接依赖:不用注入一个实例来获取真正需要的实例,增加复杂性且不易测试;
  • 避免循环依赖
  • 避免静态状态:静态状态和可测试性就是天敌;
  • 采用@nullable:guice默认情况下禁止注入null对象;
  • 模块的处理必须要快并且无副作用
  • 在providers绑定中当心io问题:因为provider不检查异常、不支持超时、不支持重试;
  • 不用在模块中处理分支逻辑
  • 尽可能不要暴露构造器

本人免费整理了java高级资料,涵盖了java、redis、mongodb、mysql、zookeeper、spring cloud、dubbo高并发分布式等教程,一共30g,需要自己领取。
传送门:https://mp.weixin.qq.com/s/igmojff-bbmq6ircgo3mqa

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

下载本文的Word格式文档,以方便收藏与打印。