Guice文檔翻譯【Part1-用戶指南】

  • 此文為本人學(xué)習(xí)guice的過程中,翻譯的官方文檔,如有不對的地方,歡迎指出。另外還有一些附件說明、吐槽、疑問點(diǎn),持續(xù)學(xué)習(xí),持續(xù)更新~
  • 有一些不太好翻譯或者沒太看懂的地方,就放原文啦。
  • 內(nèi)容比較枯燥,如果剛?cè)腴T的話,可以先了解一下什么是IoC,轉(zhuǎn)到IoC/DI?
  • Guice官方文檔(用戶指南)在此:https://github.com/google/guice/wiki/Motivation

1. User's Guide

1.1 Motivation

Motivation(動機(jī))

在應(yīng)用程序開發(fā)過程中,把所有東西都整合到一起是很討厭的一種體驗(yàn)。好在有很多方法能夠幫助你將數(shù)據(jù)、服務(wù)和展現(xiàn)層關(guān)聯(lián)起來,為了讓你更清晰的看出這些方法的區(qū)別, 我們?yōu)橐粋€披薩在線訂購網(wǎng)站編寫了“開賬單”的代碼:

public interface BillingService {
  /**
   * BillingService 接口定義了賬單服務(wù)要做的事情:它將嘗試通過信用卡支付pizza訂單,然后無論交易是否成功,都這筆交易都將被記錄下來
   * @return 返回交易賬單。若支付成功,交易賬單記錄一筆成功的交易信息,否則將記錄交易失敗原因。
   */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}

接下來,我們將實(shí)現(xiàn)這個接口,并編寫對應(yīng)的測試用例,進(jìn)行單元測試。在單元測試中,我們需要定義一個 FakeCreditCardProcessor ,以避免使用真實(shí)的信用卡進(jìn)行支付。

注:FakeCreditCardProcessor 顧名思義:“假的信用卡處理器”。如果不用 FakeCreditCardProcessor ,那么就要每次進(jìn)行單元測試時,都需要從真實(shí)的信用卡中刷錢,qa小姐姐哭暈在廁所~

Direct constructor calls(直接構(gòu)造)

首先考慮最簡單直接的實(shí)現(xiàn)方式:在 chargeOrder 中 new 一個 credit card process 和一個transaction logger 來進(jìn)行信用卡扣款和交易記錄操作,代碼大概會是下面這個樣子:

public class RealBillingService implements BillingService {
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {

    // 看這里,new 一個Processor實(shí)例,和一個 TransactionLog 實(shí)例 ,then do something
    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());
    }
  }
}

上面的代碼模塊化和可測試性都非常差。最直接的體現(xiàn)是,編譯時 chargeOrder 方法內(nèi)部直接依賴 credit card processor 實(shí)例(new 了一個 PaypalCreditCardProcessor),這意味著無論測試用例怎么寫,都會從真實(shí) PayPal 的信用卡中扣款。除此之外,對“信用卡扣款請求被拒絕”、或“信用卡扣款服務(wù)不可用”這樣的case,沒辦法測試。

Factories(工廠)

為了解決上面的問題,可以引入一個工廠類,來簡單完成“客戶端”和“實(shí)現(xiàn)類”之間的解耦。

首先定義一個簡單的工廠類,提供 setInstancegetInstance 靜態(tài)方法,允許外部調(diào)用方通過 setInstancegetInstance 方法設(shè)置或獲取一個接口實(shí)現(xiàn)類的實(shí)例,這個實(shí)現(xiàn)類可以是真實(shí)的,也可以是偽造的。
工廠定義代碼示例如下:

public class CreditCardProcessorFactory {
  private static CreditCardProcessor instance;
  // 設(shè)置一個 CreditCardProcessor 接口類型實(shí)例
  public static void setInstance(CreditCardProcessor processor) {
    instance = processor;
  }
  // 獲取 CreditCardProcessor 接口類型實(shí)例
  public static CreditCardProcessor getInstance() {
    if (instance == null) {
      return new SquareCreditCardProcessor();
    }   
    return instance;
  }
}

然后,在我們的 BillingService 里,只需要將 new 替換成工廠的 getInstance() 就行了,代碼示例如下:

public class RealBillingService implements BillingService {
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {

    // 這里,使用工廠的 getInstance() 方法分別獲取 processor 和 transactionLog 實(shí)例
    CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
    TransactionLog transactionLog = TransactionLogFactory.getInstance();

    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 class RealBillingServiceTest extends TestCase {
  // 偽造 訂單信息,和信用卡信息
  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard("1234", 11, 2010);

  // 定義 內(nèi)存交易記錄器 和 假信用卡處理器
  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();

  // 在 setUp() 方法里,通過工廠方法,將上面的兩個實(shí)例 set 到工廠里
  @Override public void setUp() {
    TransactionLogFactory.setInstance(transactionLog);
    CreditCardProcessorFactory.setInstance(processor);
  }

  // 定義 tearDown() 方法,從工廠里釋放實(shí)例。
  @Override public void tearDown() {
    TransactionLogFactory.setInstance(null);
    CreditCardProcessorFactory.setInstance(null);
  }

  // 測試
  public void testSuccessfulCharge() {
    RealBillingService billingService = new RealBillingService();
    Receipt receipt = billingService.chargeOrder(order, creditCard);
    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, processor.getCardOfOnlyCharge());
    assertEquals(100, processor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}

這段代碼看起來怎么樣呢?呃,非常的“臃腫”!全局變量持有 mock 實(shí)現(xiàn)類的管理權(quán)限,這就使得我們就必須非常小心的進(jìn)行實(shí)現(xiàn)類的 “設(shè)置” 和 “銷毀” 操作。如果 tearDown() 銷毀操作失敗,全局變量將繼續(xù)指向我們的測試實(shí)例。這可能會導(dǎo)致其他測試用例出現(xiàn)問題,還有可能會影響到多個測試用例的并行執(zhí)行。

但是最大的問題是,類之間的依賴關(guān)系是隱藏在代碼里的。如果我們需要在業(yè)務(wù)邏輯中新加一個 CreditCardFraudTracker 依賴,就不得不重新進(jìn)行測試用例的編寫來發(fā)現(xiàn)代碼中可能存在的問題。

另外,一旦我們在生產(chǎn)環(huán)境中忘記對服務(wù)中的工廠進(jìn)行初始化,這個問題會被隱藏掉,直到有收費(fèi)請求時,才會暴露出來。而隨著應(yīng)用程序業(yè)務(wù)需求的不斷增長和變更,作為“臨時保姆”角色的“工廠們”會越來越無力從心,反而成為應(yīng)用程序中沉重的負(fù)擔(dān)。

對這種代碼,盡管QA有能力通過各種手段發(fā)現(xiàn)其中的質(zhì)量問題,但實(shí)際上,我們可以做的更好。

PS:梳理一下例子中用到的接口和類:
BillingService,是登記賬單的接口。
RealBillingService,是 BillingService 的實(shí)現(xiàn)類。
CreditCardProcessor,是信用卡處理器接口。
PaypalCreditCardProcessor,是 CreditCardProcessor 的實(shí)現(xiàn)類,是真實(shí)的 Paypal 信用卡處理器,它會從真實(shí)的信用卡中扣錢。
FakeCreditCardProcessor,是 CreditCardProcessor 的實(shí)現(xiàn)類,是mock的信用卡處理器,不會使用真實(shí)的信用卡,測試使用。
TransactionLog,是交易記錄器接口。
DatabaseTransactionLog ,是 TransactionLog 接口的實(shí)現(xiàn)類,是真實(shí)的數(shù)據(jù)庫交易記錄器,會將交易信息記錄到數(shù)據(jù)庫中。
InMemoryTransactionLog ,是 TransactionLog 接口的實(shí)現(xiàn)類,是內(nèi)存交易記錄器,會將交易信息記錄到內(nèi)存中,不落庫,測試使用。
關(guān)系BillingService實(shí)現(xiàn)類chargeOrder() 方法,需要使用 CreditCardProcessor實(shí)現(xiàn)類 從信用卡中扣錢,用 TransactionLog實(shí)現(xiàn)類 記錄交易信息。

Dependency Injection(依賴注入)

依賴注入和工廠類似,也是一種設(shè)計(jì)模式。依賴注入的核心原則是:將行為與依賴解析過程分開。在我們的例子中,RealBillingService 本身不負(fù)責(zé)查找TransactionLogCreditCardProcessor ,而是通過構(gòu)造函數(shù)接受相關(guān)參數(shù),然后將其作為內(nèi)部成員變量使用,此時,依賴關(guān)系需要Service的調(diào)用方管理。

public class RealBillingService implements BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  // 帶參構(gòu)造函數(shù),接受CreditCardProcessor 和 TransactionLog 類型參數(shù)
  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());
    }
  }
}

我們不再需要任何工廠類,也可以扔掉 setUptearDown 方法來簡化測試用例。

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

現(xiàn)在,當(dāng) BillingService 的依賴項(xiàng)有增加或刪除時,編譯器都會提醒我們哪些測試用例需要fix。依賴關(guān)系向上暴露到API接口層。

不幸的是,BillingService 不再需要自己管理 TransactionLog 和 CreditCardProcessor 依賴,但 BillingService 的調(diào)用方需要卻需要管理 BillingService 依賴了。此時,可以再次應(yīng)用這個模式來解決問題,即:BillingService 調(diào)用方也定義一個帶參構(gòu)造方法,接受類型為 BillingService 的參數(shù)。

然而對頂層類來說,它可能更需要一個框架幫助它進(jìn)行依賴管理,而不是不斷的進(jìn)行 new、constructor 操作:

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

Dependency Injection with Guice(基于guice的依賴注入)
依賴注入模式使得代碼的模塊化和可測性更好,guice 能夠幫助你更容易的寫出這樣的代碼。為了在我們的記賬例子中使用 guice,首先需要學(xué)習(xí)如何將 接口 和 接口的實(shí)現(xiàn) map 起來。Guice 使用 module 進(jìn)行配置,只要定義一個類實(shí)現(xiàn) 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);
  }
}

然后,在 RealBillingService 的構(gòu)造器前面加上 @Inject 注解,這個注解用于引導(dǎo) guice 使用它。guice 會檢查所有加了 @Inject 注解的構(gòu)造方法,并為這些構(gòu)造方法中的每一個參數(shù)找到對應(yīng)類型的值。

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

最后,就可以開始客戶端代碼的編寫了。Guice.createInjector將按照Module中定義的規(guī)則生成一個 Injector,然后我們可以在Injector中獲取任何已配置的類實(shí)例。

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

1.2 Getting Started

Getting Started

使用 DI 模式,對象可以通過它們的構(gòu)造方法接受依賴項(xiàng)的注入。為了構(gòu)造某一個對象,你需要先完成它的依賴項(xiàng)們的構(gòu)造。然后,你可能還需要構(gòu)造依賴項(xiàng)的依賴項(xiàng),也就是說,你實(shí)際上要構(gòu)造的是一個 對象關(guān)系圖

手工建立對象關(guān)系圖顯然是個吃力不討好的活兒:工作量大、易出錯、難測試。相反,Guice 可以讓幫你建立對象關(guān)系圖,前提是:你需要配置好你想要個什么樣的圖。

舉個例子,BillingService 構(gòu)造方法接受它依賴的兩個接口 CreditCardProcessorTransactionLog 參數(shù)。為了讓例子看的更清楚,BillingService 構(gòu)造由guice完成,并給 BillingService 加了 @Inject 注解:

class BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;
  @Inject
  BillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    ...
  }
}

現(xiàn)在我們想用 PaypalCreditCardProcessorDatabaseTransactionLog 構(gòu)造一個 BillingService 實(shí)例。guice 使用 bindings 來映射接口及其實(shí)現(xiàn)類之間的關(guān)系,一個Module是一系列流式綁定操作語句的集合:

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {

     /*
      * 這段是告訴guice,只要看到TransactionLog依賴,就給它一個DatabaseTransactionLog實(shí)例
      */
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);

     /*
      * 類似的,只要看到CreditCardProcessor依賴,就給一個PaypalCreditCardProcessor實(shí)例
      */
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
  }
}

這些 module 構(gòu)造了injector構(gòu)造器,也就是 guice 的對象關(guān)系圖生成器。先創(chuàng)建 injector ,然后就可以使用這個 injector 生成 BillingService 實(shí)例了:

 public static void main(String[] args) {
    /*
     * Guice.createInjector() takes your Modules, and returns a new Injector
     * instance. Most applications will call this method exactly once, in their
     * main() method.
     */
    Injector injector = Guice.createInjector(new BillingModule());

    /*
     * Now that we've got the injector, we can build objects.
     */
    BillingService billingService = injector.getInstance(BillingService.class);
    ...
  }

1.3 Bindings

Bindings
注射器 injector 的工作是:生成對象關(guān)系圖。當(dāng)你請求一個指定類型的實(shí)例時, injector 會自動構(gòu)建實(shí)例、解析依賴,并進(jìn)行組裝操作。為了了解 injector 的依賴解析過程,請先使用 bindings 來配置 injector 規(guī)則。

Creating Bindings
具體做法是:繼承 AbstractModule 并實(shí)現(xiàn) configure 方法。在方法內(nèi),調(diào)用 bind() 指明每一條綁定鏈路。這些方法都是帶類型檢查的,所以如果你使用了錯誤的類型,編譯器將報(bào)錯。modules 創(chuàng)建完成后,就可以將它們作為參數(shù)傳給 Guice.createInjector ,guice 會根據(jù) modules 定義的規(guī)則生成 injector 注入器。

使用 module,可以創(chuàng)建:linked bindingsinstance bindings@Provides methodsprovider bindingsconstructor bindingsuntargetted bindings

More Bindings
除了通過 module 定義 injector 綁定規(guī)則外,injector 還有內(nèi)置綁定 build-in bindings。當(dāng)外部請求一個依賴項(xiàng),但 injector 找不到該對象的實(shí)例時,injector 會試圖創(chuàng)建一個即時綁定just-in-time binding。注入器 injector 也包括 providers 和 其他 bindings 的綁定配置(The injector also includes bindings for the providers of its other bindings.)。

1.3.1 Linked Bindings(鏈?zhǔn)浇壎ǎ?/h3>

鏈?zhǔn)浇壎?linked bindings 用于綁定一個類型/接口道它的實(shí)現(xiàn)類。下面這個例子就是把接口 TransactionLog 綁定到它的具體實(shí)現(xiàn)類 DatabaseTransactionLog

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

現(xiàn)在,當(dāng)你調(diào)用 injector.getInstance(TransactionLog.class) 或者 injector 遇到某一個依賴 TransactionLog 的類時,它都會使用 DatabaseTransactionLog 替代 TransactionLog。從類型 鏈接到任意子類型,比如接口和接口的實(shí)現(xiàn)類,或類和類的繼承子類。你也可以使用鏈?zhǔn)浇壎ǎ?DatabaseTransactionLog 綁定到它的子類上去,比如:bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);

Linked bindings 也可以串連起來:

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

在這種情況下,injector 會把所有 TransactionLog 替換為 MySqlDatabaseTransactionLog

1.3.2 Binding Annotations(綁定注解)

Binding Annotations
有的時候,你可能希望給一個類型做多個綁定。比如,你可能需要一個 Paypal credit card processor 和一個 Google Checkout processor。Bindings 提供一個可選綁定注解 ,用于實(shí)現(xiàn)該功能。注解和類型唯一標(biāo)識一個綁定規(guī)則,注解+類型,合在一起叫做key。

定義綁定注解只需要兩行代碼(附加幾個引用)。把這個放在 .java 文件里,或者放在它要注解的類型內(nèi)部。

package example.pizza;

import com.google.inject.BindingAnnotation;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;

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

你不需要完全理解上面的各個注解,如果你比較好奇的話:

  • @BindingAnnotation 告訴 Guice ,這是一個綁定注解。如果試圖對同一個對象,標(biāo)記多個注解,Guice會報(bào)錯。
  • @Target({FIELD, PARAMETER, METHOD}) 是為了讓你的注解對用戶友好。它可以防止 @PayPal 注解被意外的應(yīng)用到無法使用的地方。
  • @Retention(RUNTIME) 使得這個注解可以在運(yùn)行時被獲取。

為了使用注解綁定,需要把這個注解應(yīng)用到注入?yún)?shù)上:

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

最后,使用這個注解創(chuàng)建一個綁定。在 bind() 使用 annotatedWith 聲明:

   bind(CreditCardProcessor.class)
        .annotatedWith(PayPal.class)
        .to(PayPalCreditCardProcessor.class);
  • 同一個注解,可以標(biāo)記不同類型的參數(shù)。(@Paypal 可以標(biāo)記 CreditCardProcessor,也可以標(biāo)記TransactionLog
  • 但是,一個參數(shù)只能被一個注解標(biāo)記。(RealBillingService 構(gòu)造方法中的 CreditCardProcessor 只能被一個注解標(biāo)記,否則會編譯失敗)

@Named
Guice 有一個內(nèi)置注解 @Named 接收一個字符串參數(shù):

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

使用 Names.named() 生成一個實(shí)例,傳給 annotatedWith,這樣就綁定到一個指定的名字上了。

    bind(CreditCardProcessor.class)
        .annotatedWith(Names.named("Checkout"))
        .to(CheckoutCreditCardProcessor.class);

由于編譯器不檢查字符串,所以我們建議盡量少用 @Named 。自己定義專屬注解能夠提供更好的類型檢查安全性。

Binding Annotations with Attributes(帶參綁定注解)

Guice 支持帶參綁定注解(例如 @Named )。在極少數(shù)情況下,當(dāng)你需要這樣的注解的時候(并且不能使用 @Provides 方法),我們建議你使用 Auto/Value 項(xiàng)目中的 @AutoAnnotation 注解,因?yàn)檎_的實(shí)現(xiàn)一個帶參綁定注解是很容易出錯的。如果你決定要手動創(chuàng)建一個自定義帶參綁定注解,那么請務(wù)必正確的實(shí)現(xiàn) Annotation Javadoc 中說明的規(guī)范實(shí)現(xiàn) equals()hashCode() 。然后將對應(yīng)的實(shí)例傳給 annotatedWith() 綁定子句。

PS:Auto/Value項(xiàng)目 應(yīng)該是指google的另一個項(xiàng)目 auto

1.3.3 Instance Bindings(實(shí)例綁定)

你可以把一個類型綁定到它的具體實(shí)例上(類和類實(shí)例)。這一般適用于類型本身不依賴任何其他類型的場景,例如值對象:

    bind(String.class)
        .annotatedWith(Names.named("JDBC URL"))
        .toInstance("jdbc:mysql://localhost/pizza");
    bind(Integer.class)
        .annotatedWith(Names.named("login timeout seconds"))
        .toInstance(10);

注意不要使用 .toInstance 綁定一個復(fù)雜的對象,因?yàn)樗鼤档统绦騿铀俣取H绻斜匾脑挘埵褂?@Provides 方法來做這件事情。

  • 鏈?zhǔn)浇壎ǎ壎ǖ氖?接口 和 接口的實(shí)現(xiàn)類。
  • 實(shí)例綁定,綁定的是 類型 和 該類型的值對象(類和類實(shí)例也可以,但是這個類不要太復(fù)雜)。

1.3.4 @Provides Methods(@Provides 方法)

@Provides Methods
當(dāng)你需要代碼幫你自動創(chuàng)建對象的時候,可以使用 @Provides 方法。這個方法必須在 module 內(nèi)定義,并帶有 @Provides 注解,方法返回的類型就是綁定類型。當(dāng) injector 需要一個該類型的實(shí)例的時候,就會調(diào)用該方法。

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@Named("Checkout") 這樣的綁定注解,則 guice 以綁定注解優(yōu)先,綁定注解配置找不到的話,才找 @Provides 方法。 @Provides 方法可以以參數(shù)形式接受依賴注入,injector 會在調(diào)用 @Provides 方法之前,先解析該方法的方法的依賴。

  @Provides @PayPal
  CreditCardProcessor providePayPalCreditCardProcessor(
      @Named("PayPal API key") String apiKey) {
    PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
    processor.setApiKey(apiKey);
    return processor;
  }

Throwing Exceptions
Guice 不允許在 @Provides 方法內(nèi)部拋出異常。如果 @Provides 方法內(nèi)部有異常拋出,則該異常將被包裝成 ProvisionException 類型。在 @Provides 方法內(nèi)拋出任何類型(運(yùn)行時/檢查時)的異常都是不好的實(shí)踐。如果你有一些不得已的原因非要在 @Provides 方法中拋出異常,你可以使用 ThrowingProviders extension@CheckedProvides 方法。

1.3.5 Provider Bindings(Provider綁定)

Provider Bindings

當(dāng)你開始使用 @Provides 方法編寫更復(fù)雜的代碼時,你可以考慮把這些 @Provides方法 提到一個單獨(dú)的類里,繼承 Provider接口即可。Provider 接口是一個簡單通用的提供getter方法的接口:

public interface Provider<T> {
  T get();
}

Provider 實(shí)現(xiàn)類一般通過 @Inject 注解標(biāo)注過的構(gòu)造函數(shù)接收相關(guān)依賴,并通過實(shí)現(xiàn) Provider 接口來返回類型安全的復(fù)雜對象實(shí)例:

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

最后,通過 .toProvider 子句來完成 provider 綁定。

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

如果你的 provider 很復(fù)雜,記得進(jìn)行測試。

Throwing Exceptions
Guice 不允許在 Providers 內(nèi)部拋出異常。運(yùn)行時異常會被包裝為ProvisionExceptionCreationException,通常這種異常會影響到 injector 的創(chuàng)建。如果你因?yàn)槟撤N原因需要在 Provider 內(nèi)拋出異常,可以使用 ThrowingProviders extension

1.3.6 Untargeted Bindings(無目標(biāo)綁定)

你可以在創(chuàng)建綁定的時候不指定目標(biāo)。這對具體實(shí)現(xiàn)類,和 @ImplementedBy@ProvidedBy 標(biāo)記的接口類型非常有用。無目標(biāo)綁定講這個類型的存在告知 injector,從而使得 injector 可以對相關(guān)依賴進(jìn)行即時預(yù)加載。

無目標(biāo)綁定沒有 to 子句:

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

當(dāng)你的綁定規(guī)則帶有注解配置的時候,就算它本身就是實(shí)現(xiàn)類也必須帶 to 子句,此時 綁定關(guān)系的兩端是同一個實(shí)現(xiàn)類,舉例:

    bind(MyConcreteClass.class)
        .annotatedWith(Names.named("foo"))
        .to(MyConcreteClass.class);
    bind(AnotherConcreteClass.class)
        .annotatedWith(Names.named("foo"))
        .to(AnotherConcreteClass.class)
        .in(Singleton.class);

1.3.7 Constructor Bindings(構(gòu)造器綁定)

Constructor Bindings
Guice 3.0 新特性。

某些場景下,你可能需要把某個類型綁定到任意一個構(gòu)造函數(shù)上。以下情況會有這種需求:1、 @Inject 注解無法被應(yīng)用到目標(biāo)構(gòu)造函數(shù);2、目標(biāo)類是一個第三方類;3、目標(biāo)類有多個構(gòu)造函數(shù)參與DI。

@Provides methods @Provides 方法是解決該問題的最佳方案,直接調(diào)用你需要的目標(biāo)構(gòu)造函數(shù),不需要做反射,也不會踩到反射相關(guān)的陷阱。但是 @Provides 方法有它的局限性,手工創(chuàng)建對象不能再 AOP 中使用。

PS:不是很理解 manually constructed instances do not participate in AOP 的原因。

為了解決這個問題,guice 提供了 toConstructor() bindings ,它需要你指定要使用的確切的某個目標(biāo)構(gòu)造函數(shù),并處理 "constructor annot be found" 異常:

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

在這個例子中,DatabaseTransactionLog 必須有一個只帶1個 DatabaseConnection 類型參數(shù)的構(gòu)造函數(shù))。此時,DatabaseTransactionLog 的構(gòu)造函數(shù)不需要標(biāo)注 @Inject 注解。guice 會自動查找并調(diào)用指定的構(gòu)造函數(shù),來完成綁定。

每一個 toConstructor() 子句創(chuàng)建的綁定,其作用域是獨(dú)立的,如果你創(chuàng)建了多個單例綁定并且使用的是目標(biāo)類的同一個構(gòu)造方法,那么每一個綁定還是各自持有一個實(shí)例。

  • 當(dāng)你直接指定 injector 去調(diào)用類的某個構(gòu)造函數(shù)進(jìn)行實(shí)例獲取的時候,就不需要再在該類的構(gòu)造函數(shù)前標(biāo)記 @Inject 注解了。前面Provider的例子也是這樣。
  • 如果你需要某個類實(shí)例成為單例的話,寫一個綁定就ok了,不要做多余的事情。

1.3.8 Built-in Bindings(內(nèi)置綁定)

Built-in Bindings
除了顯示綁定和 just-in-time bindings,剩下的綁定都屬于injector的內(nèi)置綁定。這些綁定只能由injector自己創(chuàng)建,不允許外部調(diào)用。

Loggers
Guice 有一個 java.util.logging.Logger 的內(nèi)置綁定,旨在保存一些日志模板。The binding automatically sets the logger's name to the name of the class into which the Logger is being injected..

@Singleton
public class ConsoleTransactionLog implements TransactionLog {

  private final Logger logger;

  @Inject
  public ConsoleTransactionLog(Logger logger) {
    this.logger = logger;
  }

  public void logConnectException(UnreachableException e) {
    /* the message is logged to the "ConsoleTransacitonLog" logger */
    logger.warning("Connect exception failed, " + e.getMessage());
  }

The Injector
在框架代碼里,有時你可能直到運(yùn)行時才知道你需要的類型是什么。在這種特殊情況下,你需要注入injector。當(dāng)你注入injector的時候,程序不會進(jìn)行injector的依賴檢查,因此盡量不要用這種方法。

PS:沒有很懂,inject injector是怎么玩的?

Providers
For every type Guice knows about, it can also inject a Provider of that type. Injecting Providers describes this in detail.

TypeLiterals
Guice has complete type information for everything it injects. If you're injecting parameterized types, you can inject a TypeLiteral<T> to reflectively tell you the element type.

PS:這個怎么翻譯都很怪,大概意思就是說,如果你的接口/類型本身帶參,那么就可以用 TypeLiteral<T> 來描述接口/類型的構(gòu)成元數(shù)據(jù)類型。其中T是你自定義的復(fù)雜類型/接口。

The Stage
Guice 內(nèi)置一個 stage 枚舉類型,用于區(qū)分開發(fā)環(huán)境和生產(chǎn)環(huán)境。

這個枚舉類型叫做Stage,有3個枚舉值:TOOL、DEVELOPMENT、PRODUCTION。

MembersInjectors
When binding to providers or writing extensions, you may want Guice to inject dependencies into an object that you construct yourself. To do this, add a dependency on a MembersInjector<T> (where T is your object's type), and then call membersInjector.injectMembers(myNewObject).

1.3.9 Just-In-Time Bindings(即時綁定)

由Guice 自動創(chuàng)建綁定。

Just-in-time Bindings
當(dāng) injector 需要某一個類型的實(shí)例的時候,它需要獲取一個綁定。在Module類中的綁定叫做顯式綁定,只要他們可用,injector 就會在任何時候使用它們。如果需要某一類型的實(shí)例,但是又沒有顯式綁定,那么injector將會試圖創(chuàng)建一個即時綁定(Just-in-time Bindings),也被稱為JIT綁定 或 隱式綁定。

Eligible Constructors
Guice 可以使用某個具體類的 injectable constructor 可注入構(gòu)造函數(shù) 來創(chuàng)建該類的實(shí)例。這個構(gòu)造函數(shù)要么是非私有不帶參的,要么是 @Inject 標(biāo)記的:

public class PayPalCreditCardProcessor implements CreditCardProcessor {
  private final String apiKey;

  @Inject
  public PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
    this.apiKey = apiKey;
  }

Guice 不會創(chuàng)建內(nèi)部類實(shí)例,除非它有 static 修飾符。因?yàn)閮?nèi)部類含有一個指向外部類的隱式引用,而這個隱式引用無法注入。

沒有懂

@ImplementedBy
注解 @ImplementedBy 的作用是告訴 injector 它所標(biāo)記的類型對應(yīng)的默認(rèn)實(shí)現(xiàn)類型是哪一個。從行為上看,@ImplementedBy 的作用比較像鏈?zhǔn)浇壎ǎ瑸槟硞€類型綁定其子類型:

@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {
  ChargeResult charge(String amount, CreditCard creditCard)
      throws UnreachableException;
}

上面的注解和下面的 bind() 子句等價:

    bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);

如果某個類型,既有 bind() 字句(作為第一個參數(shù)),又有 @ImplementedBy 注解標(biāo)記,那么會使用 bind() 的配置。即:這個注解推薦的默認(rèn)實(shí)現(xiàn),是可以被 bind() 配置覆蓋的。請小心使用 @ImplementedBy 注解,因?yàn)樗诮涌诤蛯?shí)現(xiàn)類之間添加了編譯時依賴。

疑問:所以和鏈?zhǔn)浇壎ǖ降子猩侗举|(zhì)上的區(qū)別呢?

@ProvidedBy
@ProvidedBy 注解用于告訴 injector 它所標(biāo)記的類型要使用哪一個Provider進(jìn)行實(shí)例創(chuàng)建:

@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {
  void logConnectException(UnreachableException e);
  void logChargeResult(ChargeResult result);
}

上面的注解等價于:

    bind(TransactionLog.class)
        .toProvider(DatabaseTransactionLogProvider.class);

@ImplementedBy 相似,如果使用該注解的同時配置了 bind() ,則以 bind() 配置為準(zhǔn)。

1.4 Scopes

Scopes
默認(rèn)情況下,Guice 每次都提供一個新的對象實(shí)例給 getInstance() 調(diào)用方。這個作用域可以通過 scopes 來進(jìn)行配置。作用域允許你復(fù)用實(shí)例:應(yīng)用程序整個生命周期內(nèi)(@Singleton),一個 session 內(nèi)(@SessionScoped),一個請求內(nèi)(@RequestScoped)。Guice 包含了一個 servlet 擴(kuò)展,用于定義 web app 的作用域。Custom scopes can be written for other types of applications.

Applying Scopes
Guice 使用注解來定義作用域。為某個類型的實(shí)例定義作用域的方法是:直接將 scope 注解加在其實(shí)現(xiàn)類的類定義前。 As well as being functional, this annotation also serves as documentation.
例如, @Singleton 注解意味著必須要保證這個類是線程安全的。

@Singleton
public class InMemoryTransactionLog implements TransactionLog {
  /* everything here should be threadsafe! */
}

作用域也可以在 bind 聲明中定義:

  bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);

還可以在 @Providers 方法上加注解定義作用域:

  @Provides @Singleton
  TransactionLog provideTransactionLog() {
    ...
  }

如果某個類型的作用域配置有沖突(多種方式配置了作用域),那么以 bind() 配置為準(zhǔn)。如果某個類型上你配置了你不想要的作用域,那么你可以在 bind() 語句中配置作用域?yàn)?Scopes.NO_SCOPE 就可以覆蓋掉類型上的作用域配置。

在鏈?zhǔn)浇壎ɡ铮饔糜驊?yīng)用到 source 類型,而不是 target。假設(shè)有一個類 Applebees,同時繼承了 BarGrill 接口,此時 Applebees 可以定義兩個綁定實(shí)例,一個是綁定 Bar ,另一個是綁定 Grill

  bind(Bar.class).to(Applebees.class).in(Singleton.class);
  bind(Grill.class).to(Applebees.class).in(Singleton.class);

這兩個bind語句的作用域限定都是對綁定類型(BarGrill)的,而不是Applebees。如果你需要要 Applebees 只能創(chuàng)建一個實(shí)例,那么可以使用 @Singleton 來標(biāo)記類定義,或者再加一個綁定語句:

  bind(Applebees.class).in(Singleton.class);

有了這個綁定定義后,前面的兩個 .in(Singleton.class) 就是不必要的了。
in() 子句可以接受作用域注解(例如:RequestScoped.class)作為參數(shù),也接受 Scope 實(shí)例作為參數(shù)(例如:ServletScopes.REQUEST)。

  bind(UserPreferences.class)
      .toProvider(UserPreferencesProvider.class)
      .in(ServletScopes.REQUEST);

注解做參數(shù)是首選的,因?yàn)樗试S modules 在不同類型的應(yīng)用程序內(nèi)重用。例如:定義一個 @RequestScoped 對象,它在webapp程序中一個http請求一個實(shí)例,在api服務(wù)中一個rpc調(diào)用一個實(shí)例。

Eager Singletons

  • 單例模式有兩種:餓漢模式,懶漢模式。
    • 餓漢模式是指,在類加載時就完成初始化。這種模式下,類加載慢,但是獲取對象的速度快。(需要加同步鎖)
    • 懶漢模式是指,在類加載時不初始化,第一次請求的時候才初始化。這種模式下,類加載速度較快,但運(yùn)行時獲取對象速度慢。
  • 這個 eager-singletons 對應(yīng)的是餓漢模式; lazy-singletons 對應(yīng)的是懶漢模式。

Guice 有一個特殊的句法來定義“餓漢模式單例”:

  bind(TransactionLog.class).to(InMemoryTransactionLog.class).asEagerSingleton();

餓漢模式單例能夠更早的發(fā)現(xiàn)問題,保證終端用戶得到一致的、優(yōu)雅的使用體驗(yàn)。懶漢模式單例能夠加快 編輯-編譯-運(yùn)行 的開發(fā)周期。使用 Stage 枚舉值來指定需要使用哪種單例模式。

PRODUCTION DEVELOPMENT
.asEagerSingleton() eager eager
.in(Singleton.class) eager lazy
.in(Scopes.SINGLETON) eager lazy
@Singleton eager* lazy
  • Guice 默認(rèn)都使用餓漢模式加載單例。這些單例都是在你的 modules 中定義過的類型,還有這些類型的相關(guān)依賴。

Choosing a scope
如果某個對象是有狀態(tài)的,那么其作用域應(yīng)該是顯而易見的。單個應(yīng)用內(nèi)唯一配置 @Singleton,單個請求中唯一配置 @RequestScoped 等等。如果某個對象是無狀態(tài)的,并且創(chuàng)建開銷不那么昂貴,那么可以不必指定作用域。如果不指定作用域的話,Guice 會在每一次需要對象實(shí)例的時候都創(chuàng)建一個新的。

Singletons are popular in Java applications but they don't provide much value, especially when dependency injection is involved. Although singletons save object creation (and later garbage collection), initialization of the singleton requires synchronization; getting a handle to the single initialized instance only requires reading a volatile.

單例在以下場景最有用:

  • 有狀態(tài)的對象,例如配置或計(jì)數(shù)器。
  • 對象構(gòu)建開銷大的。
  • 對象綁定到某些資源的,比如數(shù)據(jù)庫連接池。

Scopes and Concurrency(作用域和并發(fā))
使用 @Singleton@SessionScoped 標(biāo)記的類必須是線程安全的,同時,注入到這些類中的依賴,也必須是線程安全的。
Minimize mutability to limit the amount of state that requires concurrency protection.

@RequestScoped 對象不需要關(guān)注線程安全問題。Guice不允許 @ Singleton@ SessionScoped 對象依賴 @ RequestScoped 對象,會直接報(bào)錯。如果你需要在很小的作用域內(nèi)使用某個對象,那就可以給這個對象注入一個 Provider 。

疑問:為啥?Provider注入和其他模式注入產(chǎn)生的對象作用域有啥區(qū)別嗎?

1.5 Injections

Guice 是怎么幫你完成對象初始化的呢?

Injections
DI 模式將行為與依賴解析過程分離開來。不同于直接在工廠查找依賴項(xiàng),DI 建議將依賴從外部傳遞進(jìn)來。把依賴從外部傳到對象內(nèi)部的過程就叫做 “注入 injection”。

Constructor Injection(構(gòu)造子注入)
構(gòu)造子注入將注入與實(shí)例化放到一起。為了使用構(gòu)造子注入,你需要使用 @Inject 注解標(biāo)記類的構(gòu)造方法,這個構(gòu)造方法需要接受類依賴作為參數(shù)。大多數(shù)構(gòu)造子將會把接收到的參數(shù)分派給內(nèi)部成員變量。

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

如果類定義中沒有 @Inject 標(biāo)注的構(gòu)造函數(shù),Guice 會使用一個 public 無參構(gòu)造方法(如果存在的話)。注解能夠明確指出哪些類型參與了 DI 依賴注入(原文:Prefer the annotation, which documents that the type participates in dependency injection.)。

構(gòu)造子注入方式便于進(jìn)行單元測試。如果你的類在單個構(gòu)造函數(shù)內(nèi)接受所有的依賴項(xiàng),那么你在構(gòu)建對象時不會遺漏任何一個依賴。當(dāng)類中引入了新的依賴時,相關(guān)調(diào)用鏈路會斷掉(原文:all of the calling code conveniently breaks)!修復(fù)編譯錯誤后,就能夠確認(rèn)一切都o(jì)k了。

Method Injection(方法注入)
Guice 可以向標(biāo)注了 @Inject 的方法中注入依賴。依賴項(xiàng)以參數(shù)的形式傳給方法,Guice 會在調(diào)用注入方法前完成依賴項(xiàng)的構(gòu)建。注入方法可以有任意數(shù)量的參數(shù),并且方法名對注入操作不會有任何影響(即:你不必使用setXXX的格式進(jìn)行命名,Guice 的方法注入只看注解不看方法名)。

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

Field Injection(字段注入)
Guice 支持使用 @Inject 注解標(biāo)記字段。這是最簡潔的注入方式,但是不可測試。

public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
  @Inject Connection connection;

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

不要在 final 字段上加注解,which has weak semantics

Optional Injections(可選注入)
有的時候,你可能需要一個依賴項(xiàng)存在則進(jìn)行注入,不存在則不注入,這樣是比較方便的。此時可以使用方法注入或字段注入來做這件事,當(dāng)依賴項(xiàng)不可用的時候Guice 就會忽略這些注入。如果你需要配置可選注入的話,使用 @Inject(optional = true) 注解就可以了:

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

同時使用可選注入和JIT綁定可能會產(chǎn)生意外的結(jié)果。
例如,下面的代碼,即使 Date 沒有被顯示綁定,也會總是被注入到字段中。這是因?yàn)?Date 有一個public無參構(gòu)造方法,它正好能夠被用來做JIT綁定。

  @Inject(optional=true) Date launchDate;

也就是說,即使Date是optional的,但是Date有一個public無參構(gòu)造方法,所以Guice能夠使用它JIT構(gòu)建Date實(shí)例,所以無論怎樣都會有Date注入的,那個optional=true就沒啥實(shí)際含義了。

On-demand Injection(按需注入)
方法注入和字段注入可以可以用來初始化現(xiàn)有實(shí)例,你可以使用 Injector.injectMembers API:

  public static void main(String[] args) {
    Injector injector = Guice.createInjector(...);
    
    CreditCardProcessor creditCardProcessor = new PayPalCreditCardProcessor();
    injector.injectMembers(creditCardProcessor);

疑問:什么場景需要這么干?

Static Injections(靜態(tài)注入)
When migrating an application from static factories to Guice, it is possible to change incrementally. Static injection is a helpful crutch here. It makes it possible for objects to partially participate in dependency injection, by gaining access to injected types without being injected themselves. Use requestStaticInjection() in a module to specify classes to be injected at injector-creation time:

  @Override public void configure() {
    requestStaticInjection(ProcessorFactory.class);
    ...
  }

Guice 會自動注入帶 @Inject 注解的靜態(tài)成員到類中:

class ProcessorFactory {
  @Inject static Provider<Processor> processorProvider;

  /**
   * @deprecated prefer to inject your processor instead.
   */
  @Deprecated
  public static Processor getInstance() {
    return processorProvider.get();
  }
}

Static members will not be injected at instance-injection time. This API is not recommended for general use because it suffers many of the same problems as static factories: it's clumsy to test, it makes dependencies opaque, and it relies on global state.

PS:不建議使用靜態(tài)注入,偷個懶不翻譯了

Automatic Injection
Guice 會對以下情形做自動注入:

  • 在綁定語句里,通過 toInstance() 注入實(shí)例。
  • 在綁定語句里,通過 toProvider() 注入 Provider 實(shí)例。這些對象會在注入器創(chuàng)建的時候被創(chuàng)建并注入容器。如果它們需要滿足其他啟動注入,Guice 會在它們被使用前將他們注入進(jìn)去。

1.5.1 Injecting Providers

Injecting Providers
在常規(guī)依賴注入中,每一個類型都僅僅能獲得依賴項(xiàng)的某1個實(shí)例。 RealBillingService 獲得一個 CreditCardProcessor 實(shí)例和一個 TransactionLog 實(shí)例。但有時候你可能需要某一個類型的多個實(shí)例。此時可以提供一個 provider 給 Guice。Provider 在每次 get() 方法被調(diào)起時都生成一個新的值:

public interface Provider<T> {
  T get();
}

Provider 類型是可參數(shù)話的,例如 Provider<TransactionLog>Provider<CreditCardProcessor> 。任何可以注入 value 的地方,都可以使用 provider for value 替代。


public class RealBillingService implements BillingService {
  private final Provider<CreditCardProcessor> processorProvider;
  private final Provider<TransactionLog> transactionLogProvider;

  @Inject
  public RealBillingService(Provider<CreditCardProcessor> processorProvider,
      Provider<TransactionLog> transactionLogProvider) {
    this.processorProvider = processorProvider;
    this.transactionLogProvider = transactionLogProvider;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = processorProvider.get();
    TransactionLog transactionLog = transactionLogProvider.get();

    /* use the processor and transaction log here */
  }
}

對于任何綁定、注解,或者什么否沒有,injector 注入器都會給它的 Provider 提供一個內(nèi)置綁定。

Providers for multiple instances
當(dāng)你需要同一類型的多個實(shí)例時,可以使用 Provider
。假設(shè),當(dāng)pizza 訂單支付失敗時,你需要保存一份摘要和一份明細(xì)清單,通過使用Provider,你可以在任何你需要的時候獲取一個新的記錄:

public class LogFileTransactionLog implements TransactionLog {

  private final Provider<LogFileEntry> logFileProvider;

  @Inject
  public LogFileTransactionLog(Provider<LogFileEntry> logFileProvider) {
    this.logFileProvider = logFileProvider;
  }

  public void logChargeResult(ChargeResult result) {
    LogFileEntry summaryEntry = logFileProvider.get();
    summaryEntry.setText("Charge " + (result.wasSuccessful() ? "success" : "failure"));
    summaryEntry.save();

    if (!result.wasSuccessful()) {
      LogFileEntry detailEntry = logFileProvider.get();
      detailEntry.setText("Failure result: " + result);
      detailEntry.save();
    }
  }

Providers for lazy loading
如果你需要一個構(gòu)建代價很高的依賴,那么你可以使用 provider 對這些依賴進(jìn)行懶加載。如果你不是一直都需要用到這個依賴的話,那么這種方法這會對你非常有用:

public class DatabaseTransactionLog implements TransactionLog {
  
  private final Provider<Connection> connectionProvider;

  @Inject
  public DatabaseTransactionLog(Provider<Connection> connectionProvider) {
    this.connectionProvider = connectionProvider;
  }

  public void logChargeResult(ChargeResult result) {
    /* only write failed charges to the database */
    if (!result.wasSuccessful()) {
      Connection connection = connectionProvider.get();
    }
  }

Providers for Mixing Scopes
依賴一個作用域范圍很小的對象可能會報(bào)錯。假設(shè),你有一個 @Singleton transactionLog,需要依賴一個 @ RequestScoped 的當(dāng)前用戶。加入你直接把 用戶信息注入到transactionLog,就亂套了,因?yàn)槊恳粋€請求的用戶信息是不一樣的。而 Provider可以按需注入,所以它們能夠幫你安全的將作用域不同的依賴綁定到一起:

@Singleton
public class ConsoleTransactionLog implements TransactionLog {
  
  private final AtomicInteger failureCount = new AtomicInteger();
  private final Provider<User> userProvider;

  @Inject
  public ConsoleTransactionLog(Provider<User> userProvider) {
    this.userProvider = userProvider;
  }

  public void logConnectException(UnreachableException e) {
    failureCount.incrementAndGet();
    User user = userProvider.get();
    System.out.println("Connection failed for " + user + ": " + e.getMessage());
    System.out.println("Failure count: " + failureCount.incrementAndGet());
  }

1.6 AOP

Guice 的攔截方法

Aspect Oriented Programming(面向切面編程)
除了依賴注入,Guice 也支持方法攔截。這個特性允許你編寫一段代碼,用于在某些特定方法執(zhí)行前執(zhí)行。它適用于關(guān)注切面(“aspects”)的場景,比如:事務(wù)、安全和日志記錄。攔截器從切面的角度看待問題,而不是對象,所以這種用法被稱為“面向切面編程(AOP)”。

Most developers won't write method interceptors directly; but they may see their use in integration libraries like Warp Persist. Those that do will need to select the matching methods, create an interceptor, and configure it all in a module.

Matcher 是一個簡單的接口,要么接收一個值,要么拒絕一個值。在 Guice AOP 中,你需要2個 matcher:一個用來定義哪些類參與 AOP;另一個用來定義這些類的哪些方法參與 AOP。為了簡化這項(xiàng)工作,這里有一個 factory class 可以滿足該場景:

任何時候,當(dāng)某一個匹配的方法被調(diào)用的時候,都會執(zhí)行 MethodInterceptors ,它們會檢查調(diào)用操作:方法、參數(shù)、接收的實(shí)例,然后執(zhí)行切面邏輯,然后委托到底層方法。最后,MethodInterceptors 可以檢查返回值、異常或其他返回信息。由于攔截器可能被應(yīng)用到很多方法上,并處理相關(guān)的請求,因此,攔截器必須是有效且非入侵性的。

Example: Forbidding method calls on weekends
為了演示攔截器方法在Guice中的使用,我們舉個例子,要求拒絕周末的pizza訂單。送貨員只在周一到周五工作,所以我們需要防止pizza在不能送貨時被訂購。類似這種結(jié)構(gòu)的,都可以使用AOP進(jìn)行授權(quán)管理。

為了將選擇方法標(biāo)記為工作日,我們定義一個注解:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface NotOnWeekends {}

然后將這個注解應(yīng)用到一個需要攔截的方法上:

public class RealBillingService implements BillingService {

  @NotOnWeekends
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    ...
  }
}

接下來,通過實(shí)現(xiàn) rg.aopalliance.intercept.MethodInterceptor 接口定義攔截器。當(dāng)請求通過授權(quán),可以轉(zhuǎn)到底層方法的時候,調(diào)用 invocation.proceed() 來完成請求下發(fā):

public class WeekendBlocker implements MethodInterceptor {
  public Object invoke(MethodInvocation invocation) throws Throwable {
    Calendar today = new GregorianCalendar();
    if (today.getDisplayName(DAY_OF_WEEK, LONG, ENGLISH).startsWith("S")) {
      throw new IllegalStateException(
          invocation.getMethod().getName() + " not allowed on weekends!");
    }
    return invocation.proceed();
  }
}

最后,進(jìn)行相關(guān)的配置。在這里為需要攔截的類和方法定義 matchers。在這個例子里,我們會匹配所有標(biāo)記了 @NotOnWeekends 注解的類:

public class NotOnWeekendsModule extends AbstractModule {
  protected void configure() {
    bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class), 
        new WeekendBlocker());
  }
}

把它們放到一起,(然后等到星期六),執(zhí)行這段代碼就會發(fā)現(xiàn),我們的訂單會被攔截并拒絕了:

Exception in thread "main" java.lang.IllegalStateException: chargeOrder not allowed on weekends!
    at com.publicobject.pizza.WeekendBlocker.invoke(WeekendBlocker.java:65)
    at com.google.inject.internal.InterceptorStackCallback.intercept(...)
    at com.publicobject.pizza.RealBillingService$$EnhancerByGuice$$49ed77ce.chargeOrder(<generated>)
    at com.publicobject.pizza.WeekendExample.main(WeekendExample.java:47)

** Limitations**
方法攔截是通過運(yùn)行時生成字節(jié)碼來實(shí)現(xiàn)的。Guice 通過重寫方法來動態(tài)創(chuàng)建一個子類應(yīng)用到攔截器中。如果你的代碼所應(yīng)用的平臺不支持字節(jié)碼生成(例如Android),那么 Guice 不能為你提供 AOP 支持。

這種方法對需要被攔截的類和方法提出以下要求:

  • 類必須是公共的或包內(nèi)私有的。
  • 類必須是非final的。
  • 方法必須是public, package-private or protected
  • 方法必須是非final的。
  • 實(shí)例必須是由 Guice 創(chuàng)建的(通過 @Inject 注解或無參構(gòu)造函數(shù))。Guice 的AOP 不會對 非Guice 創(chuàng)建的實(shí)例進(jìn)行攔截。

Injecting Interceptors

如果你需要向一個攔截器中注入依賴,可以使用 requestInjection API:

public class NotOnWeekendsModule extends AbstractModule {
  protected void configure() {
    WeekendBlocker weekendBlocker = new WeekendBlocker();
    requestInjection(weekendBlocker);
    bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class), 
       weekendBlocker);
  }
}

另一種方法是,使用 Binder.getProvider 然后通過構(gòu)造方法注入依賴項(xiàng)到攔截器中:

public class NotOnWeekendsModule extends AbstractModule {
  protected void configure() {
    bindInterceptor(any(),
                    annotatedWith(NotOnWeekends.class),
                    new WeekendBlocker(getProvider(Calendar.class)));
  }
}

使用攔截器時一定要格外小心。If your interceptor calls a method that it itself is intercepting, you may receive a StackOverflowException due to unending recursion.

AOP Alliance
The method interceptor API implemented by Guice is a part of a public specification called AOP Alliance. This makes it possible to use the same interceptors across a variety of frameworks.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,823評論 18 139
  • Dagger2 轉(zhuǎn)載請注明原作者,如果你覺得這篇文章對你有幫助或啟發(fā),可以關(guān)注打賞。 前言本文翻譯自Google ...
    輕云時解被占用了閱讀 6,721評論 4 31
  • 原文鏈接:https://github.com/google/guice/wiki/Bindings Bindin...
    2fc2a81494ac閱讀 2,117評論 0 0
  • Guice是谷歌推出的一個輕量級依賴注入框架,幫助我們解決Java項(xiàng)目中的依賴注入問題。如果使用過Spring的話...
    樂百川閱讀 26,629評論 6 25
  • 小組時間: 活動名稱:小手不要碰 活動目標(biāo): 1.知道保護(hù)小手,了解保護(hù)小手的方法。 2.知道生活中哪些危險的物品...
    yoniyoniyoni閱讀 319評論 0 0