譯者注
Dagger2是在Dagger1的基礎上升級開發的,所以要學習Dagger2,先了解Dagger1。下文是由Dagger1的官方文檔翻譯而來。
參考:
原文鏈接
Dagger1項目鏈接
介紹
在任何應用中最好的類是那些“幹活賣力”的:如BarcodeDecoder
,KoopaPhysicsEngine
和AudioStreamer
.這些類持有依賴,可能是一個BarcodeCameraFinder
,DefaultPhysicEngine
和HttpStreamer
.
相反的,應用中最不好的類是那些“尸位素餐”的傢伙:BarcodeDecoderFactory
,CameraServiceLoader
和MutableContextWrapper
.這些類是和好東西纏繞在一塊的管道膠帶(These classes are the clumsy duct tape that wires the interesting stuff together)。
Dagger是工廠類的替代者(譯者注:工廠類指的是負責創建對象的一些類)。它讓你將精力花在感興趣的類上面(譯者注:即”關注點分離”)。聲明依賴,指定如何去滿足他們,然後主導(ship)你的app.
在javax.inject註解標準上構建,每個類都很容易測試。你不會因爲一個FakeCreditCardService
要用一堆樣板文件去替換RpcCreditCardService
.
依賴注入不僅僅是爲了測試。它也會使得創建可複用、可替換的模塊變得容易。你可以在你的app的所有地方共享一個AuthenticationModule
。並且你可以在開發期間運行DevLoggingModule
,而在產品階段運行ProdLoggingModule
,從而在每個場景下獲得正確的行爲。
要了解更多信息,請觀看QCon公司的Jesse Wilson 2012年的介紹性演講。
使用Dagger
我們將通過構建一個咖啡機的例子來演示依賴注入和Dagger.完整的可編譯可執行代碼,請參見Dagger的咖啡案例
聲明依賴
Dagger構造你應用類的實例,並滿足(satisfy)他們的依賴關係。它使用javax.inject.Inject
註解識別它感興趣的構造方法或者字段(fields)。
使用@Inject
註解Dagger要用來創建類實例的構造方法。需要一個新實例時,Dagger會獲取需要的參數值並調用構造器。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
Dagger可以直接注入字段。例中,對於heater
字段和pump
字段它分別獲取一個Heater
實例和一個Pump
實例。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
如果你的類中有@Inject
標註的字段,但沒有@Inject
標註的構造器,Dagger會試圖使用一個無參構造器(如果存在)。缺少@Inject
標註的類不能使用Dagger構造。
Dagger不支持方法注入。
滿足依賴
默認情況下,如上文描述那樣,Dagger通過構造請求類型的實例滿足每個依賴。當你請求一個CoffeeMaker
時,它會通過調用new CoffeeMaker()
獲取一個,然後設置它的可注入字段。
但是,@Inject
並不是在任何地方都有效:
- 接口不能構造。
- 第三方類不能標註。
- 可配置的對象必須配置!
對於這些@Inject
不足或不合適的地方,我們使用一個由@Provides
標註的方法去滿足依賴。該方法的返回類型定義了它要滿足的依賴。
例如,在任何需要Heater
的時候調用provideHeater()
:
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides
方法可能有他們自己的依賴。下面這個方法在任何需要一個Pump
的時候返回一個Thermosiphon
:
@Provides Pump providePump(Thermosiphon pump) {
return pump;
}
所有的@Provides
方法必須在模塊裏。這只是一些有@Module
標註的類。
@Module
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Pump providePump(Thermosiphon pump) {
return pump;
}
}
按照慣例,@Provides
方法名以provide
爲前綴,模塊類名以module
爲後綴。
建圖(Building the Graph)
@Inject
和@Provides
標註的類形成了一張通過依賴連接的對象圖。通過調用ObjectGraph.create()
獲取這張圖,它可以接受一到多個模塊(作爲參數):
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
爲了將這張圖用起來,我們需要引導注入。這通常需要注入到命令行APP的主類,或者Android App的Activity類中。在我們的咖啡案例中,使用CoffeeApp
類啓動依賴注入。我們請求這張圖提供一個類的注入實例:
class CoffeeApp implements Runnable {
@Inject CoffeeMaker coffeeMaker;
@Override public void run() {
coffeeMaker.brew();
}
public static void main(String[] args) {
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
CoffeeApp coffeeApp = objectGraph.get(CoffeeApp.class);
...
}
}
唯一漏掉的是這張圖還不知道可注入類CoffeeApp
的存在。我們需要在@Module
註解中將它註冊爲被注入類型。
@Module(
injects = CoffeeApp.class
)
class DripCoffeeModule {
...
}
這些injects
項允許在編譯時驗證整張圖。提早檢測問題提高了開發速度,並在重構中提出了一些危險源。
既然圖已經創建好了,根對象已經注入,我們可以運行我們的咖啡機app了
$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P
單例(Singletons)
使用@Singleton
標註一個@Provides
方法或者可注入類(injectable class)。對象圖將會爲其所有使用者提供該值的一個單例。
@Provides @Singleton Heater provideHeater() {
return new ElectricHeater();
}
可注入類的@Singleton
註解也用作文檔。它提示潛在的維護者該類可以在多線程中共享。
@Singleton
class CoffeeMaker {
...
}
懶註解(Lazy Injections)
有時候我們需要對象在使用時實例化。對於任何綁定T
,你可以創建一個Lazy<T>
,它將實例化推遲到Lazy<T>
的get()
方法的第一次調用。如果T
是一個單例,Lazy<T>
則爲ObjectGraph
範圍內所有註解提供相同實例。否則,每個注入位(injection site)將得到其專屬的Lazy<T>
實例。無論如何,任何給定的Lazy<T>
實例的調用將返回相同的T
底層實例。
class GridingCoffeeMaker {
@Inject Lazy<Grinder> lazyGrinder;
public void brew() {
while (needsGrinding()) {
// Grinder created once on first call to .get() and cached.
lazyGrinder.get().grind();
}
}
}
提供者註解(Providing Injections)
有時候你需要返回多個實例,而不僅僅是注入的那單獨的一個值。當你需要多項(Factories,Builders等等)時,有一個辦法是注入一個Provider<T>
,而不是T
.一個Provider<T>
每次調用.get()
都會創建一個新實例。
class BigCoffeeMaker {
@Inject Provider<Filter> filterProvider;
public void brew(int numberOfPots) {
...
for (int p = 0; p < numberOfPots; p++) {
maker.addFilter(filterProvider.get()); //new filter every time.
maker.addCoffee(...);
maker.percolate();
...
}
}
}
==注意:== 注入Provider<T>
創建的代碼可能會讓人費解,而且這種設計可能讓你的對象圖中充滿了無範圍、無組織的對象( Injecting Provider<T>
has the possibility of creating confusing code, and may be a design smell of mis-scoped or mis-structured objects in your graph. 譯者注:smell of 應該是一種形容,像空氣一樣無處不在)。通常你要使用一個Factory<T>
或一個Lazy<T>
,或者重組你的代碼的生命期(lifetimes)和結構,使之能夠僅僅注入一個T
.但是在某些情況下,注入Provider<T>
可以很省事( Injecting Provider<T>
can, however, be a life saver in some cases )。常見用法是當你必須使用一個遺留架構(legacy architecture),它與你的對象的自然生命期不同步(例如servlets 設計成單例,但只在請求指定數據的環境中有效)。
限定符(Qualifiers)
有時候單純的類型不能確定一個依賴。比如一個複雜的咖啡機應用可能需要爲水和加熱板分別提供加熱器。
在這種情況下,我們添加一個qualifier annotation。任何使用@Qualifier
(譯者注:元註解)註解的註解都屬於這類。這裏有一個@Named
的聲明,就是javax.inject
內置的限定符註解。
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
你可以創建你自己的限定符註解,或者直接使用@Named
.通過註解關注的字段或參數應用限定符。他們的類型和限定符註解都將用來標識(identify)依賴。
class ExpensiveCoffeeMaker {
@Inject @Named("water") Heater waterHeater;
@Inject @Named("hot plate") Heater hotPlateHeater;
...
}
通過註解相應的@Provides
方法提供合格值(qualified values)。
@Provides @Named("hot plate") Heater provideHotPlateHeater() {
return new ElectricHeater(70);
}
@Provides @Named("water") Heater provideWaterHeater() {
return new ElectricHeater(93);
}
依賴不可以同時使用多個限定符註解(Dependencies may not have multiple qualifier annotations)。
靜態註解(Static Injection)
==警告:== 此功能應該有限使用,因爲靜態依賴難以測試和複用。
Dagger 可以注入靜態字段。聲明瞭使用@Inject
註解的靜態字段的類必須列入module註解的staticInjections
.
@Module(
staticInjections = LegacyCoffeeUtils.class
)
class LegacyModule {
}
使用ObjectGraph.injectStatics()
給這些靜態字段填充值:
ObjectGraph objectGraph = ObjectGraph.create(new LegacyModule());
objectGraph.injectStatics();
==注意:== 靜態注入只適用於直接圖中的模塊。如果從一個plus()
調用圖中的injectStatics()
,模塊上的靜態注入在擴展圖中將不會發生作用(Static Injection only operates for modules in the immediate graph. If you call injectStatics()
on a graph created from a call to plus()
, static injections on modules inthe extended graph will not be performed)。
編譯時驗證
Dagger包含一個驗證(validates)模塊和注入的註解處理器(annotaion processor)。這個處理器。這個處理器非常嚴格,一旦出現任何無效或不完整的綁定都會引發編譯錯誤。例如下面這個模塊缺少對Executor
的綁定。
@Module
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
編譯它的時候,javac
會指出缺失的那處綁定:
[ERROR] COMPILATION ERROR :
[ERROR] error: No binding for java.util.concurrent.Executor
required by provideHeater(java.util.concurrent.Executor)
爲Executor
添加一個@Provide
方法或者將該模塊標記爲未完成(incomplete)可以修復這個問題。我們允許未完成模塊缺失依賴項。
@Module(complete = false)
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
模塊提供了被注入類沒有使用的類型也會引發錯誤。
@Module(injects = Example.class)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
因爲模塊注入的Example
只是用了Heater
,javac
拒絕了沒有使用到的綁定:
[ERROR] COMPILATION ERROR:
[ERROR]: Graph validation failed: You have these unused @Provider methods:
1. coffee.DripCoffeeModule.provideChiller()
Set library=true in your module to disable this check.
如果你的模塊的綁定在注入類以外會被使用到,就請將該module標記爲library
@Module(
injects = Example.class,
library = true
)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
爲了充分利用編譯時驗證,創建一個包含應用所有模塊的模塊。註解處理器會檢測並報告各個模塊的問題。
@Module(
includes = {
DripCoffeeModule.class,
ExecutorModule.class
}
)
public class CoffeeAppModule {
}
當你在編譯路徑中包含了Dagger的jar文件,註解處理器自動生效。
編譯時代碼生成(Compile-time Code Generation)
Dagger的註解處理器可以生成像CoffeeMaker$InjectAdapter.java
或者DripCoffeeModule$ModuleAdapter
這樣的源文件。這些文件是Dagger的實現細節。你不需要直接使用它們,儘管通過注入進行步驟調試時會接觸到它們(though they can be handy when step-debugging through an injection)。
模塊重寫(Module Overrides)
如果多個有競爭關係的提供相同依賴的@Provides
方法,Dagger會因爲錯誤導致失敗。但是有時候使用開發或測試代碼替代產品代碼是必要的。在模塊註解中使用override = true
讓你獲得相對於其它模塊綁定的優先權。
這裏Junit測試使用一個來自Mockito的模擬對象覆蓋了DripCoffeeModule
對Heater
的綁定。這個模擬對象注入到CoffeeMaker
,然後進入測試。
public class CoffeeMakerTest {
@Inject CoffeeMaker coffeeMaker;
@Inject Heater heater;
@Before public void setUp() {
ObjectGraph.create(new TestModule()).inject(this);
}
@Module(
includes = DripCoffeeModule.class,
injects = CoffeeMakerTest.class,
overrides = true
)
static class TestModule {
@Provides @Singleton Heater provideHeater() {
return Mockito.mock(Heater.class);
}
}
@Test public void testHeaterIsTurnedOnAndThenOff() {
Mockito.when(heater.isHot()).thenReturn(true);
coffeeMaker.brew();
Mockito.verify(heater, Mockito.times(1)).on();
Mockito.verify(heater, Mockito.times(1)).off();
}
}
重寫最適合應用中的小變動:
- 在單元測試中使用一個模擬對象代替真實實現。
- 在開發中用一個假認證代替LDAP認證。
對於更大的變動,通常使用不同模塊的組合更簡單。
下載(略)
來自Guice的升級(Upgrading from Guice)
Dagger不支持Guice某些引人注意的特性:
- 注入
final
字段和private
成員。爲了Dagger生成代碼的最佳性能。可使用構造函數注入來解決這一問題。 - 餓漢單例(Eager Singletons)。爲每個餓漢單例聲明靜態字段創建一個
EagerSingletons
類可解決這一問題。 - 方法注入。
- Dagger不能構造缺少
@Inject
註解的類,即使他們有一個無參構造函數。