Google Guice 依賴注入框架:簡明教程

Guice是一個輕量級的依賴注入框架。用於對象之間的依賴的注入。
本文的所有例子基於Guice 4.0

Guice的使用:

Guice的使用涉及接口,接口的實現類,接口與其實現類的綁定關係三個方面。
定義一個接口:
 
public interface Log {
    void log();
}

定義一個實現類:

public class DbLog implements Log {
    public void log() {
        System.out.println("dblog");
 }
}

定義一個Module管理接口與實現類的綁定關係:

public class LinkedBindingModule extends AbstractModule {
    @Override
 protected void configure() {
       bind(Log.class).to(DbLog.class);
 }
}

Module(模塊)是Guice用來管理一組綁定關係的地方。自定義模塊需要繼承AbstractModule類並覆蓋configure方法,在configure方法中設置綁定關係。

Guice創建時可使用多個模塊,則注入器可以注入多個模塊中指定的綁定關係。

如何使用:

使用@Inject註解標註需要注入的屬性,Guice會根據綁定關係將接口的具體實現類注入。

開啓Guice並獲取Log接口的實現類:

輸出結果:


Guice支持的注入方式

Guice支持3種注入方式:屬性注入,方法注入,構造器注入

上文給出了屬性注入的方式,下面看一下方法注入和構造器注入。

方法注入:

方法注入支持任意方法名,但是建議使用setter方法。

方法注入支持多個參數的注入。

構造器注入:

構造器注入是推薦的注入方式,使用一個構造器注入所有依賴,可以避免忘記注入某個依賴。

儘量使用final修飾屬性,可以避免屬性引用被重新賦值導致的問題,這種問題一般很難被發現。

注意:

1.如果類中有構造器但是沒有標註@Inject,需要保證類中有一個public的無參構造器,Guice使用反射機制調用這個構造器創建實例。

2.如果類中有多個構造器,只能有一個構造器使用@Inject標註。Guice不支持多個構造器標註@Inject。如果有多個構造器都需要注入依賴,則使用下文中的Provider綁定或構造器綁定。

Guice的綁定方式

Guice提供了多種綁定方式,用來解決綁定依賴,以及創建綁定過程中碰到的問題。

1.最簡單的綁定:鏈接綁定

語法:bind(接口類.class).to(實現類.class)

鏈接綁定綁定接口到實現類。任何使用接口的地方都可以得到自動注入的實現類。

鏈接綁定可以被傳遞。上面這種情況,所有請求TransactionLog 的地方,注入器(injector)會注入MySqlDatabaseTransactionLog,而不是DatabaseTransactionLog。

2.一個接口有多個實現類的綁定:註解綁定

藉助註解區分接口的不同實現類。兩種方式:自定義註解或使用Guice提供的@Named註解

自定義註解:
@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME)
public @interface PayPal {}
  • @BindingAnnotation 告訴Guice這是一個綁定註解。如果在一個位置(FIELDPARAMETERMETHOD),使用多個綁定註解,Guice會報錯。
  • @Target({FIELD, PARAMETER, METHOD})註解生效的位置
  • @Retention(RUNTIME) 這個註解在運行時起作用

 使用自定義註解:

public class RealBillingService implements BillingService {

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

Module中綁定註解和特定實現類:

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

在Module中聯繫註解和特定實現類

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

 

3.綁定接口的一個具體實例:實例綁定

一般的綁定都是class到class的綁定。Guice提供對綁定具體實例的支持,結合Names註解,可以方便的注入某些參數。

 

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

注意:

1.實例綁定僅用於自身不包含依賴需要綁定的場景。

2.實例綁定中的實例不應該包含複雜耗時的操作,那樣會使系統啓動變慢。如果有複雜的操作,可以使用下面的綁定:Provides綁定

4.需要代碼方式創建對象,使用:Provide綁定

接口的具體實現類,需要在代碼中new出來或者其他方式創建了對象。在Module中創建一個方法並標註@Provides註解即可。

每當需要注入TransactionLog,注入器就會調用這個方法provideTransactionLog.

考慮另外一種情況:@Provides所在的方法中也需要注入參數怎麼辦?Guice支持!直接在方法參數中標註即可。Guice會在調用@Provides方法前,注入對應依賴。

再考慮另外一種情況:當前方法返回值只是接口的某一個特定實現怎麼辦?Guice支持!直接在方法上標註@PayPal或@Named("PayPal")即可。

注意:@Provides方法不支持拋出異常,所有異常都會被包裝在ProvisionException中。

如果需要拋出特定異常,可以使用Guice的擴展:ThrowingProviders extension @CheckedProvides標註的方法。

@Provides方法只能在Module中定義,如果方法比較複雜,比較多則可以考慮分解到單獨的類中。

Guice提供Provider接口實現這個功能。

接口定義如下:

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

Provide類通過泛型支持特定的接口。

這種Provide需要在Module中使用.toProvider 方法綁定聯繫。

注意:使用Provider綁定,無法應用Guice提供的AOP功能。

5.構造器綁定

適用於以下情況:

1.綁定依賴到第三方等無法添加@Inject註解的類

2.類中有多個構造器參與依賴注入,且不使用屬性注入/方法注入時。

以上2種情況都可以用provider,直接使用java代碼創建對象的方式注入。

但是provider直接創建對象無法應用Guice提供的AOP功能.

使用構造器綁定非常簡單,在Module中調用toConstructor方法即可。

TransactionLog綁定的實現類爲DatabaseTransactionLog,且Guice會調用對應的含有一個DatabaseConnection參數的構造器,構造實現類。

DatabaseConnection將根據綁定關係注入正確的實現類,完成依賴注入。

6.內建綁定

有一些綁定是Guice已經準備好的,不需要在Module中指定綁定關係。

  • java.util.logging.Logger,Guice自動注入合適的實現類。這個我們一般不用,我們一般用slf4j,log4j等。
  • Injector 注入器

  • Providers 實例提供者

  • TypeLiterals 

  • Stage 舞臺,環境 TOOL,DEVELOPMENT,PRODUCTION

  • MembersInjectors 註冊實例的依賴

7.沒有目標(target)的綁定

除了在Module中綁定接口的實現類之外,還可以使用@ImplementedBy or @ProvidedBy標記接口,提供實現類。

這種情況下,可以不用在Module中指定綁定關係,也可以在Module中指定沒有目標的綁定,此時綁定關係中沒有to語句:

注意:如果帶有標註,則必須指定具體實現類。

 8.自動被Guice注入的綁定:實時綁定(Just-in-time Bindings)

在Module中指定的綁定稱爲顯式綁定,如果沒有指定顯式綁定,Guice會嘗試實時綁定(Just-in-time Bindings)。

實時綁定包括3種:除了@ImplementedBy和@ProvidedBy這2種之外,還有一種是通過合適的構造器綁定。

  • @ImplementedBy

等同於:

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

 

注意:使用這個標註不是一個很好的設計。一個接口不應該知道它的實現類。

  • @ProvidedBy

等同於:

 

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

注意:如果顯式綁定和實時綁定同時存在,則以顯式綁定爲準。

合適的構造器

合適的構造器是指:類中的構造器必須有@Inject標註

實例的作用範圍(Scope)

幾種常見的作用範圍

  1. No Scope 每次創建新實例
  2. Singleton  整個應用中只有一個
  3. RequestScoped  每個servlet請求創建一個實例
  4. SessionScoped   每個Servlet會話創建一個實例
  5. 自定義範圍   自定義

應用作用範圍,使其生效

Guice默認指定No Scope.

下面看下如何指定單例:

1.使用@Singleton註解

2.顯式指定

3.使用@Provides標註的方法

注意:實現Provider接口的提供者,指定Singleton無效。

如果一個實現類繼承2個接口,且應用中只需要一個,如何綁定?

在實現類中指定@Singleton註解或指定實現類爲單例:bind(Applebees.class).in(Singleton.class);

 

作用範圍爲RequestScoped  ,SessionScoped   比較簡單,直接指定即可:

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

 

“飢渴”的單例(延遲加載)

延遲加載會加快開發,測試等進度,但是也容易隱藏問題。

使用asEagerSingleton方法,可以強制Guice立即實例化類並注入:

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

在不同的環境下,延遲加載的作用機制:

選擇一個作用範圍

如果是一個有狀態的對象,必須明顯的指出它的作用範圍,比如整個應用有效,則標註@Singleton,每個請求有效則標註@RequestScoped等等。

如果一個對象是無狀態的,並且創建新對象開銷不大,那麼就讓Guice創建新的。

 

單例模式比較重要,可以減低對象的創建減少垃圾回收,但是單例模式的類的初始化需要線程同步。所以非單例的對象引用單例的對象,需要添加volatile參數,保證初始化完成後對所有線程可見,且不會被多次執行。

單例模式常在以下情況下使用:

  • 有狀態對象,比如配置信息
  • 創建比較昂貴的對象
  • 連接資源的對象,比如數據庫連接

作用範圍與線程安全性

單例和SessionScoped的對象必須是線程安全的。所有注入到線程安全的對象的其他實例也必須是線程安全的。

RequestScoped的對象不是線程安全的,比較容易犯的錯誤是單例對象注入了非線程安全的對象,比如SessionScoped的對象注入了RequestScoped的對象。

好的解決方案是範圍寬的對象需要注入範圍窄的對象時,注入Provider,祥見下面注入Provider.

注入Provider

Provider可以通過get方法創建自定義的對象,Guice可以通過實時綁定,自動注入Provider。

注入Provider一般用來解決幾種問題:需要一個對象的多個實例時,按條件延遲加載時,混合多個作用範圍時。

需要一個對象的多個實例時:

有時候在一個方法中需要一個對象的多個實例,比如一次收費的摘要和詳情應該保存在不同的記錄中。

按條件延遲加載時:

有時候需要在某些條件下才需要獲取對象:比如收費失敗時才獲取數據庫連接並記錄。

這種情況一般用於比較昂貴的對象或連接資源的對象。

混合多個作用範圍時:

一個對象注入了範圍比自己窄的對象是錯誤的!

比如一個單例的對象需要一個請求範圍的當前用戶對象,Guice注入的對象只是當前用戶的,這個注入不變,下一個用戶使用的就不是自己的用戶信息了。

使用provider可以安全的解決這個問題。

Guice對AOP的支持

Guice通過支持方法攔截器來支持AOP.

爲了支持方法攔截,必須要有2個組件支持:匹配器(Matcher)和攔截器(Interceptors)

注意:爲了攔截到方法層面,匹配器需要2個配合,一個用來匹配類層次,另外一個用來匹配要攔截的那個方法。

明白了以上2個組件之後,使用AOP就非常簡單了:

定義一個類實現org.aopalliance.intercept.MethodInterceptor攔截器接口,然後在Module中綁定攔截器即可。

Module中指定綁定關係和匹配器:


匹配器支持的幾種匹配:

小結:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章