Google - Guice 入門介紹

感謝本文的原作者讓我對Guice可以初步的瞭解一點,出於項目的考慮,有時候Spring並不能作爲一個通用的框架,尤其需要一個輕量級的純DI框架時,個人認爲Guice是個不錯的選擇。在文章最後還附了一段關於Spring和Guice的比較,有興趣的童鞋可以看看

本文出處:http://blog.csdn.net/derekjiang/article/details/7231490



一. 概述

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就會共享同樣的對象了。


附:關於選擇Spring還是Google-Guice的一些想法


Spring已經出來好多年了,當年是作爲輕量級J2EE容器和EJB抗衡的,不過隨着技術和時間的發展,Spring越來越全面,越來越強大,也就越來越Heavy了。而且,在使用Spring的過程中,因爲所有Bean直接的關聯都是在XML配置文件中完成的,於是當系統變大之後,XML配置中的內容會非常的多,感覺會很亂。

Google-Guice是最近幾年剛剛出來的一種DI框架,它的好處就是簡單,輕量級,快。Guice中的Bean之間的關聯都是在Module中定義的,如果需要定義不同的關聯,則需要定義不同的Module,這點相對來說會麻煩一些,但是這種使用Annotation和Module的方式,對於程序員來說,可能會感覺更爲直接。另外,Guice啓動的時候比Spring要快很多,據說要快100倍,這主要是因爲Spring要去讀配置文件,要Parse XML文件。但是個人感覺這種快,其實作用有限,因爲這種速度的差異,只是在load Bean的時候,當Bean都已經被load到內存之後,其實就沒什麼差別了,所以Guice快只是快在啓動階段。


關於在做項目時,到底選擇Spring還是選擇Guice呢? 個人感覺,如果你原來一直在使用Spring或者Guice,那麼其實完全沒有必要換,因爲這樣會帶來額外的複雜度。原來熟悉那個用那個就OK了。 如果Spring和Guice都沒用過,或者說都用過,那麼我覺得就看項目需要了,如果項目需要的僅僅是個DI容器,那麼我覺得Guice就足夠了,好處就是簡單,上手快,啓動快;如果項目需要更多功能,要和其他的框架進行兼容,那麼個人感覺選擇Spring仍然是不錯的選擇,比較Spring出道時間長,和很多成熟框架之間都有很好的兼容,雖然Guice新的版本已經可以支持AOP功能,並且也可以支持和一些其他框架進行兼容,但是Guice想要在短時間內取代Spring,個人感覺還是非常難的。

Guice與Spring的對比
  Spring Guice
使用XML 使用將類與類之間的關係隔離到xml中,由容器負責注入被調用的對象,因此叫做依賴注入 不使用xml,將類與類之間的關係隔離到Module中,聲名何處需要注入,由容器根據Module裏的描述,注入被調用的對象。
使用Annotation   使用
支持自定義Annotation標註,對於相同的接口定義的對象引用,爲它們標註上不同的自定義Annotation註釋,就可以達到同一個類裏邊的同一個接口的引用,注射給不同的實現,在Module裏用標註做區分,靈活性大大增加。
使用Annotation也未必是好事,範型等新特性也未必是好事,目前大多的服務器均不支持jdk1.5,wls要9以前才支持,而目前的客戶由於價格原因也很少選用wls9的,至少我們做過的項目中都沒有。功能再強,客戶不需要,何用?
運行效率 裝載spring配置文件時,需解析xml,效率低,getBean效率也不高,不過使用環境不會涉及到getBean,只有生產環境的時候會用到getBean,在裝載spring應用程序的時候,已經完成全部的注射,所以這個低效率的問題不是問題。 使用Annotation,cglib, 效率高與spring最明顯的一個區別,spring是在裝載spring配置文件的時候把該注入的地方都注入完,而Guice呢,則是在使用的時候去注射,運行效率和靈活性高。
類耦合度 耦合度低,強調類非侵入,以外部化的方式處理依賴關係,類裏邊是很乾淨的,在配置文件裏做文章,對類的依賴性極低。 高,代碼級的標註,DI標記@inject侵入代碼中,耦合到了類層面上來,何止侵入,簡直侵略,代碼耦合了過多guice的東西,大大背離了依賴注入的初衷,對於代碼的可維護性,可讀性均不利
類編寫時 需要編寫xml,配置Bean,配置注入 只需聲明爲@inject,等着被注入,
最後在統一的Module裏聲明注入方式
僅支持IOC 否,spring目前已經涉獵很多部分 是,目前僅僅是個DI容器
是否易於代碼重構 統一的xml配置入口,更改容易 配置工作是在Module裏進行,和spring異曲同功
支持多種注入方式 構造器,setter方法 Field,構造器,setter方法
靈活性  

1,如果同一個接口定義的引用需要注入不同的實現,就要編寫不同的Module,煩瑣

2,動態注入

如果你想注射的一個實現,你還未知呢,怎麼辦呢,spring是沒辦法,事先在配置文件裏寫死的,而Guice就可以做到,就是說我想注射的這個對象我還不知道注射給誰呢,是在運行時才能得到的的這個接口的實現,所以這就大大提高了依賴注射的靈活性,動態注射。

與現有框架集成度 1, 高,衆多現有優秀的框架(如struts1.x等)均提供了spring的集成入口,而且spring已經不僅僅是依賴注入,包括衆多方面。
2, Spring也提供了對Hibernate等的集成,可大大簡化開發難度。
3, 提供對於orm,rmi,webservice等等接口衆多,體系龐大。
1,可以與現有框架集成,不過僅僅依靠一個效率稍高的DI,就想取代spring的地位,有點難度。
配置複雜度 在xml中定位類與類之間的關係,難度低 代碼級定位類與類之間的關係,難度稍高


發佈了2 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章