天天看點

Google-Guice入門介紹

一. 概述

Guice是一個輕量級的DI架構。本文對Guice的基本用法作以介紹。

本文的所有例子基于Guice 3.0

本文的很多代碼來源于Guice首頁:http://code.google.com/p/google-guice/wiki/GettingStarted

考慮到是入門介紹,本文中并未涉及到AOP相關内容,如有需要還請參考上面連結。

二. 舉例說明Guice的用法

Guice本身隻是一個輕量級的DI架構,首先我們通過一個例子來看看怎麼使用Guice。

首先有一個需要被實作的接口:

public interface BillingService {

  /**
   * Attempts to charge the order to the credit card. Both successful and
   * failed transactions will be recorded.
   *
   * @return a receipt of the transaction. If the charge was successful, the
   *      receipt will be successful. Otherwise, the receipt will contain a
   *      decline note describing why the charge failed.
   */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}
           

然後,有一個實作該接口的實作類:

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

  @Inject
  RealBillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

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

現在接口有了,實作類也有了,接下來就是如何将接口和實作類關聯的問題了,在Guice中需要定義Module來進行關聯

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

     /*
      * This tells Guice that whenever it sees a dependency on a TransactionLog,
      * it should satisfy the dependency using a DatabaseTransactionLog.
      */
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);

     /*
      * Similarly, this binding tells Guice that when CreditCardProcessor is used in
      * a dependency, that should be satisfied with a PaypalCreditCardProcessor.
      */
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
  }
}
           

好了,現在萬事俱備,就讓我們一起看看怎麼使用Guice進行依賴注入吧:

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.
     */
    RealBillingService billingService = injector.getInstance(RealBillingService.class);
    ...
  }
           

以上就是使用Guice的一個完整的例子,很簡單吧,不需要繁瑣的配置,隻需要定義一個Module來表述接口和實作類,以及父類和子類之間的關聯關系的綁定。本文不對比guice和spring,隻是單純介紹Guice的用法。

三. 綁定方式的介紹

從上面我們可以看出,其實對于Guice而言,程式員所要做的,隻是建立一個代表關聯關系的Module,然後使用這個Module即可得到對應關聯的對象。是以,主要的問題其實就是在如何關聯實作類和接口(子類和父類)。

1. 在自定義的Module類中進行綁定

1.1 在configure方法中綁定

1.1.1 鍊式綁定

鍊式綁定是最簡單,最直接,也是使用最多的綁定方式。

protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
  }
           

就是直接把一種類型的class對象綁定到另外一種類型的class對象,這樣,當外界擷取TransactionLog時,其實傳回的就是一個DatabaseTransactionLog對象。當然,鍊式綁定也可以串起來,如:

protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
  }
           

這樣,當外界請求TransactionLog時,其實傳回的就會是一個MySqlDatabaseTransactionLog對象。

1.1.2 注解(Annotations)綁定

鍊式綁定針對于同樣的類型都綁定到同一種目标類型時,非常好用,但是對于一個接口有多種實作的時候,鍊式綁定就不好區分該用哪種實作了。可以把Annotations綁定方式看作是鍊式綁定的一種擴充,專門用來解決這種同一個接口有多種實作的問題。Annotations綁定又可以分為兩種,一種是需要自己寫Annotations,另外一種則簡化了一些。

1.1.2.1 自己寫Annotations的方式 首先,寫一個注解

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

然後,使用這個注解去修飾目标字段或參數,如:

public class RealBillingService implements BillingService {

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

public class RealBillingService implements BillingService {
  @Inject
  @Www
  private CreditCardProcessor processor;
  ...
}
           

最後,在我們進行鍊式綁定時,就可以區分一個接口的不同實作了,如:

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

這樣,被Annotations PayPal?修飾的CreditCardProcessor就會被綁定到目标實作類PayPalCreditCardProcessor。如果有其他的實作類,則可把用不同Annotations修飾的CreditCardProcessor綁定到不同的實作類

1.1.2.2 使用@Named的方式 使用@Named的方式和上面自己寫Annotation的方式很類似,隻不過做了相應的簡化,不再需要自己去寫Annotation了。

public class RealBillingService implements BillingService {

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

直接使用@Named修飾要注入的目标,并起個名字,下面就可以把用這個名字的注解修飾的接口綁定到目标實作類了

bind(CreditCardProcessor.class)
        .annotatedWith(Names.named("Checkout"))
        .to(CheckoutCreditCardProcessor.class);
           
1.1.3 執行個體綁定

上面介紹的鍊式綁定是把接口的class對象綁定到實作類的class對象,而執行個體綁定則可以看作是鍊式綁定的一種特例,它直接把一個執行個體對象綁定到它的class對象上。

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.1.4 Provider綁定

在下面會介紹基于@Provides方法的綁定。其實Provider綁定是基于@Provides方法綁定的後續發展,是以應該在介紹完基于@Provides方法綁定之後再來介紹,不過因為Provider綁定也是在configure方法中完成的,而本文又是按照綁定的位置來組織的,因為就把Provider綁定放在這了,希望大家先跳到後面看過基于@Provides方法的綁定再回來看這段。

在使用基于@Provides方法綁定的過程中,如果方法中建立對象的過程很複雜,我們就會考慮,是不是可以把它獨立出來,形成一個專門作用的類。Guice提供了一個接口:

public interface Provider {
  T get();
}
           

實作這個接口,我們就會得到專門為了建立相應類型對象所需的類:

public class DatabaseTransactionLogProvider implements Provider {
  private final Connection connection;

  @Inject
  public DatabaseTransactionLogProvider(Connection connection) {
    this.connection = connection;
  }

  public TransactionLog get() {
    DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
    transactionLog.setConnection(connection);
    return transactionLog;
  }
}
           

這樣以來,我們就可以在configure方法中,使用toProvider方法來把一種類型綁定到具體的Provider類。當需要相應類型的對象時,Provider類就會調用其get方法擷取所需的對象。

其實,個人感覺在configure方法中使用Provider綁定和直接寫@Provides方法所實作的功能是沒有差别的,不過使用Provider綁定會使代碼更清晰。而且當提供對象的方法中也需要有其他類型的依賴注入時,使用Provider綁定會是更好的選擇。

1.1.5 無目标綁定

無目标綁定是連結綁定的一種特例,在綁定的過程中不指明目标,如:

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

如果使用注解綁定的話,就不能用無目标綁定,必須指定目标,即使目标是它自己。如:

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

無目标綁定,主要是用于與被@ImplementedBy 或者 @ProvidedBy修飾的類型一起用。如果無目标綁定的類型不是被@ImplementedBy 或者 @ProvidedBy修飾的話,該類型一定不能隻提供有參數的構造函數,要麼不提供構造函數,要麼提供的構造函數中必須有無參構造函數。因為guice會預設去調用該類型的無參構造函數。

1.1.6 指定構造函數綁定

在configure方法中,将一種類型綁定到另外一種類型的過程中,指定目标類型用那種構造函數生成對象。

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

這種綁定方式主要用于不友善用注解@Inject修飾目标類型的構造函數的時候。比如說目标類型是第三方提供的類型,或者說目标類型中有多個構造函數,并且可能會在不同情況采用不同的構造函數。

1.1.7 Built-in綁定

Built-in綁定指的是不用程式員去指定,Guice會自動去做的綁定。目前,Guice所支援的Built-in綁定隻有對java.util.logging.Logger的綁定。個人感覺,所謂的Built-in綁定,隻是在比較普遍的東西上為大家帶來友善的一種做法。

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

1.2 在@Provides方法中進行綁定

當你隻是需要在需要的時候,産生相應類型的對象的話,@Provides Methods是個不錯的選擇。方法傳回的類型就是要綁定的類型。這樣當需要建立一個該類型的對象時,該provide方法會被調用,進而得到一個該類型的對象。

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

需要注意的是Provide方法必須被@Provides所修飾。同時,@Provides方法綁定方式是可以和上面提到的注解綁定混合使用的,如:

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

這樣一來,隻有被@PayPal修飾的CreditCardProcessor對象才會使用provide方法來建立對象,同時

2. 在父類型中進行綁定

2.1 @ImplementedBy

在定義父類型的時候,直接指定子類型的方式。

這種方式其實和鍊式綁定的效果是完全一樣的,隻是聲明綁定的位置不同。

和鍊式綁定不同的是它們的優先級,@ImplementedBy實作的是一種default綁定,當同時存在@ImplementedBy和鍊式綁定時,鍊式綁定起作用。

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

等價于:

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

2.2 @ProvidedBy

在定義類型的時候直接指定子類型的Provider類。

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

等價于:

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

并且,和@ImplementedBy類似,@ProvidedBy的優先級也比較低,是一種預設實作,當@ProvidedBy和toProvider函數兩種綁定方式并存時,後者有效。

3. 在子類型中進行注入

3.1 構造函數注入

在構造函數綁定中,Guice要求目标類型要麼有無參構造函數,要麼有被@Inject注解修飾的構造函數。這樣,當需要建立該類型的對象時,Guice可以幫助進行相應的綁定,進而生成對象。在Guice建立對象的過程中,其實就是調用該類型被@Inject注解修飾的構造函數,如果沒要@Inject注解修飾的構造函數,則調用無參構造函數。在使用構造函數綁定時,無需再在Module中定義任何綁定關系。

這裡需要注意的是,Guice在建立對象的過程中,無法初始化該類型的内部類(除非内部類有static修飾符),因為内部類會有隐含的對外部類的引用,Guice無法處理。

public class PayPalCreditCardProcessor implements CreditCardProcessor {
  private final String apiKey;

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

3.2 屬性注入

屬性綁定的目的是告訴Guice,當建立該類型的對象時,哪些屬性也需要進行依賴注入。用一個例子來看:

首先,有一個接口:

package guice.test;

import com.google.inject.ImplementedBy;;

@ImplementedBy (SayHello.class)
public interface Talk {
	public void sayHello();
}
           

該接口指明了它的實作類SayHello

package guice.test;

public class SayHello implements Talk{
	@Override
	public void sayHello() {
		System.out.println("Say Hello!");
		
	}
}
           

接下來就是屬性注入的例子:

package guice.test;

import com.google.inject.Inject;

public class FieldDI {
	@Inject
	private Talk bs;

	public Talk getBs() {
		return bs;
	}
}
           

這裡面,指明熟悉Talk類型的bs将會被注入。使用的例子是:

package guice.test;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class Test {
	 public static void main(String[] args) {

		    Injector injector = Guice.createInjector(new BillingModule());

		    
		    FieldDI fdi = injector.getInstance(FieldDI.class);
		    fdi.getBs().sayHello();
		  }
}
           

如果我們沒有用@Inject修飾Talk bs的話,就會得到如下錯誤:

Exception in thread "main" java.lang.NullPointerException
      

如果我們用@Inject修飾Talk bs了,但是Talk本身沒有被@ImplementedBy修飾的話,會得到如下錯誤:

Exception in thread "main" com.google.inject.ConfigurationException: Guice configuration errors:

1) No implementation for guice.test.Talk was bound.
  while locating guice.test.Talk
    for field at guice.test.FieldDI.bs(FieldDI.java:5)
  while locating guice.test.FieldDI

1 error
	at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1004)
	at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:961)
	at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1013)
	at guice.test.Test.main(Test.java:24)
           

另外,屬性注入的一種特例是注入provider。如:

public class RealBillingService implements BillingService {
  private final Provider processorProvider;
  private final Provider transactionLogProvider;

  @Inject
  public RealBillingService(Provider processorProvider,
      Provider 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 */
  }
}
           

其中,Provider的定義如下:

public interface Provider {
  T get();
}
           

3.3 Setter方法注入

有了上面的基礎我們再來看Setter注入就非常簡單了,隻不過在setter方法上增加一個@Inject注解而已。 還是上面的例子,隻是有一點修改:

package guice.test;

import com.google.inject.Inject;

public class FieldDI {

	@Inject
	public void setBs(Talk bs) {
		this.bs = bs;
	}

	private Talk bs;

	public Talk getBs() {
		return bs;
	}
}
           

四. 對象産生的Scopes

在預設情況下,每次通過Guice去請求對象時,都會得到一個新的對象,這種行為是通過Scopes去配置的。

Scope的配置使得我們重用對象的需求變得可能。目前,Guice支援的Scope有@Singleton, @SessionScoped, @RequestScoped。

同樣,程式員也可以自定義自己的Scope,本文不涉及自定義scope,如果有興趣,請參考:http://code.google.com/p/google-guice/wiki/CustomScopes

下面我們就以@Singleton舉例說明怎麼來告訴Guice我們要以@Singleton的方式産生對象:1. 在定義子類型時聲明

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

2.在module的configure方法中做綁定時聲明

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

3.在module的Provides方法裡聲明

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

這裡有一點需要注意的是,如果發生下面的情況:

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

這樣一共會生成2個Applebees對象,一個給Bar用,一個給Grill用。如果在上面的配置的情況下,還有下面的配置,

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

這樣一來,Bar和Grill就會共享同樣的對象了。