前言
在為dropwizard選擇DI框架的時候考慮了很久。Guice比較成熟,Dagger2主要用於Android。雖然都是google維護的,但Dagger2遠比guice更新的頻率高。再一個是,Dagger2不同於guice的運行時注入,編譯時生成代碼的做法很好。提前發現問題,更高的效率。
還是那句話,百度到的dagger2資料看着一大堆,大都表層,而且和Android集成很深。很少有單獨講Dagger2的。不得已,去看官方文檔。
HelloWorld
官方的example是基於maven的,由於maven天然結構的約定,compile的插件生成可以和maven集成的很好。而我更喜歡gradle,gradle隨意很多,結果就是編譯結構需要自己指定。
demo source: https://github.com/Ryan-Miao/l4dagger2
結構如下:
.
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── readme.md
├── settings.gradle
└── src
└── main
├── java
│ └── com
│ └── test
│ └── l4dagger2
│ └── hello
│ ├── CoffeeApp.java
│ ├── CoffeeMaker.java
│ ├── DripCoffeeModule.java
│ ├── ElectricHeater.java
│ ├── Heater.java
│ ├── Pump.java
│ ├── PumpModule.java
│ └── Thermosiphon.java
├── resources
└── webapp
11 directories, 15 files
加載依賴
build.gradle
plugins {
id "net.ltgt.apt" version "0.12"
id "net.ltgt.apt-idea" version "0.12"
id "net.ltgt.apt-eclipse" version "0.12"
}
repositories {
mavenLocal()
maven {
url "http://maven.aliyun.com/nexus/content/groups/public/"
}
mavenCentral()
}
group 'com.test'
version '1.0-SNAPSHOT'
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'idea'
sourceCompatibility = 1.8
dependencies {
compile 'com.google.dagger:dagger:2.12'
apt 'com.google.dagger:dagger-compiler:2.12'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Note that:
plugins
插件需要放到最開頭。然后,由於設計編譯時生成sourceSet類,針對IDE需要添加對應的插件。dagger2
生成的類放在build/generated/source/apt/main
Coding Time
接下來的內容就和官方的demo一樣了。
com.test.l4dagger2.hello.CoffeeApp
public class CoffeeApp {
@Singleton
@Component(modules = { DripCoffeeModule.class })
public interface CoffeeShop {
CoffeeMaker maker();
}
public static void main(String[] args) {
CoffeeShop coffeeShop = DaggerCoffeeApp_CoffeeShop.builder().build();
coffeeShop.maker().brew();
}
}
com.test.l4dagger2.hello.DripCoffeeModule
@Module(includes = PumpModule.class)
class DripCoffeeModule {
@Provides
@Singleton
Heater provideHeater() {
return new ElectricHeater();
}
}
com.test.l4dagger2.hello.PumpModule
@Module
abstract class PumpModule {
@Binds
abstract Pump providePump(Thermosiphon pump);
}
com.test.l4dagger2.hello.Pump
interface Pump {
void pump();
}
com.test.l4dagger2.hello.Thermosiphon
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
@Override public void pump() {
if (heater.isHot()) {
System.out.println("=> => pumping => =>");
}
}
}
com.test.l4dagger2.hello.Heater
interface Heater {
void on();
void off();
boolean isHot();
}
com.test.l4dagger2.hello.ElectricHeater
class ElectricHeater implements Heater {
boolean heating;
@Override public void on() {
System.out.println("~ ~ ~ heating ~ ~ ~");
this.heating = true;
}
@Override public void off() {
this.heating = false;
}
@Override public boolean isHot() {
return heating;
}
}
com.test.l4dagger2.hello.CoffeeMaker
class CoffeeMaker {
private final Lazy<Heater> heater; // Create a possibly costly heater only when we use it.
private final Pump pump;
@Inject
CoffeeMaker(Lazy<Heater> heater, Pump pump) {
this.heater = heater;
this.pump = pump;
}
public void brew() {
heater.get().on();
pump.pump();
System.out.println(" [_]P coffee! [_]P ");
heater.get().off();
}
}
針對DaggerCoffeeApp_CoffeeShop
不識別問題,運行編譯后就可以了。
sh gradlew build
結果
Run main method
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P
用法分析
Dagger暴露的最外層為component,而Component的注入來自module。Component之間不能互相注入,module之間可以互相注入。
注入原理
編譯時掃描注解,生成對應的builder和factory。這點和spring不同,spring是運行時通過反射生成instance。另一個問題就是由於是靜態工廠,那么就不能動態綁定了。不過可以通過其他的手段彌補。
以下來自詳解Dagger2
- @Inject: 通常在需要依賴的地方使用這個注解。換句話說,你用它告訴Dagger這個類或者字段需要依賴注入。這樣,Dagger就會構造一個這個類的實例並滿足他們的依賴。
- @Module: Modules類里面的方法專門提供依賴,所以我們定義一個類,用@Module注解,這樣Dagger在構造類的實例的時候,就知道從哪里去找到需要的 依賴。modules的一個重要特征是它們設計為分區並組合在一起(比如說,在我們的app中可以有多個組成在一起的modules)。
- @Provide: 在modules中,我們定義的方法是用這個注解,以此來告訴Dagger我們想要構造對象並提供這些依賴。
@Component: Components從根本上來說就是一個注入器,也可以說是@Inject和@Module的橋梁,它的主要作用就是連接這兩個部分。 - Components可以提供所有定義了的類型的實例,比如:我們必須用@Component注解一個接口然后列出所有的@Modules組成該組件,如 果缺失了任何一塊都會在編譯的時候報錯。所有的組件都可以通過它的modules知道依賴的范圍。
- @Scope: Scopes可是非常的有用,Dagger2可以通過自定義注解限定注解作用域。后面會演示一個例子,這是一個非常強大的特點,因為就如前面說的一樣,沒 必要讓每個對象都去了解如何管理他們的實例。在scope的例子中,我們用自定義的@PerActivity注解一個類,所以這個對象存活時間就和 activity的一樣。簡單來說就是我們可以定義所有范圍的粒度(@PerFragment, @PerUser, 等等)。
- Qualifier: 當類的類型不足以鑒別一個依賴的時候,我們就可以使用這個注解標示。例如:在Android中,我們會需要不同類型的context,所以我們就可以定義 qualifier注解“@ForApplication”和“@ForActivity”,這樣當注入一個context的時候,我們就可以告訴 Dagger我們想要哪種類型的context。
1. 入口
@Singleton
@Component(modules = { DripCoffeeModule.class })
public interface CoffeeShop {
CoffeeMaker maker();
}
dagger中Component就是最頂級的入口,dagger為之生成了工廠類DaggerCoffeeApp_CoffeeShop
, 目標是構建CoffeeMaker
, 在CoffeeMaker
中使用了Injection
,那么依賴要由工廠類來提供。工廠類是根據modules
的參數來找依賴綁定的。
本例中,指向了DripCoffeeModule
,意思是CoffeeMaker
的依賴要從這個module里找。
工廠名稱生成規則
- 如果Component是接口, 則生成
Dagger
+接口名 - 如果Component是內部接口,比如本例,則生成
Dagger
+類名+_
+ 接口名
2. 依賴管理
module看起來似乎和spring里的configuration有點相似,負責聲明bean。而且同樣支持繼承,子module擁有父親的元素。 這點和spring的context也很像,子context可以從父context里獲取instance。對應的Java里的繼承也同樣,子類可以使用父類的屬性和方法。
這里可以把DripCoffeeModule
當做父類,而PumpModule
為子類。
但是, 引用注入的時候卻和spring相反,module之間 !
在spring里,子context擁有所有的bean,所以在子context里可以注入任何bean。而父context只能注入自己聲明的bean。
而在dagger2的這個module里,module可以看做是一個打包。最外層的包顯然包含了所有的bean。因此,在CoffeeShop
中引入的是父module DripCoffeeModule
。在子module PumpModule
中的Thermosiphon
可以注入聲明在DripCoffeeModule
里的Heater
實例。
當然,造成這個問題的原因是生成的時候的順序有關。調整下順序,把PumpModule
引入Component里,然后,把DripCoffeeModule
include到PumpModule
里。此時一樣沒啥問題,只是掉了個。不同的是,父子對調導致Pump變成了父親的元素,Heater成了子類的元素。然而,一樣可以將heater注入到Pump。為啥?等看了源碼再了解,這里先搞定用法scop。猜測會不會是在創建Pump的時候發現缺少Heater,然后壓棧,去子module里找聲明,找到后,彈出棧。
Anyway,demo的注入就是這么簡單。module起到定義bean的范圍的作用, module之間只要連接就是互通的,可以相互注入, 但打包bean還是要靠最外層的module。
3. 具體實現方式
簡單的說,就是一個工廠模式,由Dagger負責創建工廠,幫忙生產instance。遵從Java規范JSR 330,可以使用這些注解。現在不研究Dagger2是如何根據注解去生成工廠的,先來看看工廠是什么東西,理解為什么可以實現了DI(Dependency Injection),如何創建IoC(Inverse of Control)容器。
從入口出發。
CoffeeApp.CoffeeShop coffeeShop = DaggerCoffeeApp_CoffeeShop.builder().build();
CoffeeMaker maker = coffeeShop.maker();
DaggerCoffeeApp_CoffeeShop
是生成的工廠類,實現了我們定義Component
的接口CoffeeShop
.
針對Component上的注解
@Singleton
@Component(modules = { DripCoffeeModule.class })
首先觀察DripCoffeeModule
,里面目前聲明了一個Provider<Heater>
, 並且include
了PumpModule
。顯然,我們的Component就是由這兩個東西決定的。因此,DripCoffeeModule
把這兩個當做成員變量,這樣就有了操縱這兩個東西來生成instance的可能。
下一步,就是build()
方法了:
public CoffeeApp.CoffeeShop build() {
if (dripCoffeeModule == null) {
this.dripCoffeeModule = new DripCoffeeModule();
}
if (pumpModule == null) {
this.pumpModule = new PumpModule();
}
return new DaggerCoffeeApp_CoffeeShop(this);
}
這里顯然就是初始化這兩個成員變量。然后創建我們的工廠DaggerCoffeeApp_CoffeeShop
private void initialize(final Builder builder) {
this.provideHeaterProvider =
DoubleCheck.provider(
DripCoffeeModule_ProvideHeaterFactory.create(builder.dripCoffeeModule));
this.pumpModule = builder.pumpModule;
}
到這里才開始核心的依賴管理。
initialize分析
先看第一部分,這是關於Heater
的。由於Heater聲明了Singleton
,Dagger通過經典的double-check
來實現單例。面試必備。來看看dagger是怎么用的。這里有兩種Provider
其中,Factory
是正宗的工廠。為毛還要專門繼承出來一個接口?可以學習下這種抽象方法,雖然Factory和Provider幾乎一模一樣,但分出來是為了標記。或者說歸類。比如,區別於DoubleCheck
。看名字都能才出來,DoubleCheck
是一個代理類。
雖然簡單,但還是有好多可以學習的編程要點。
/** Returns a {@link Provider} that caches the value from the given delegate provider. */
public static <T> Provider<T> provider(Provider<T> delegate) {
checkNotNull(delegate);
if (delegate instanceof DoubleCheck) {
/* This should be a rare case, but if we have a scoped @Binds that delegates to a scoped
* binding, we shouldn't cache the value again. */
return delegate;
}
return new DoubleCheck<T>(delegate);
}
看看,同樣是創建一個新對象,比我們平時多了兩步。一是檢查Null,我表示遇到最多的生產事故是由NullPointException
造成的,然后檢查是否需要代理,如果本來就是代理類則直接返回,這里就實現了方法的冪等性,重復調用的結果一致。
接下來看我們的工廠DripCoffeeModule_ProvideHeaterFactory
, 真就是一個工廠。但也不能不看,因為這是和我們代碼關聯最緊密的一步。工廠是如何根據我們的注解生產instance的呢?后面再看。學習源碼真心提高抽象思維。
至此,initialize 方法結束。下一步就是生成我們的Component了。
Make instance
public CoffeeMaker maker() {
return new CoffeeMaker(
DoubleCheck.lazy(provideHeaterProvider),
Preconditions.checkNotNull(
pumpModule.providePump(new Thermosiphon(provideHeaterProvider.get())),
"Cannot return null from a non-@Nullable @Provides method"));
}
果然就是直接用構造函數new了一個,因此,不要以為在Component上標記了Singleton就會生產出同一個Component了,每次生產的最外一層的instance,即Component,就是new了一個。但他的依賴就不同了。看看兩個依賴的不同生命周期就能明白。
Heater
Heater做了兩個處理,一個是Singleton,一個是Lazy, 即懶漢式。Singleton和Lazy是兩種設計模式。
DoubleCheck實現了Provider和Lazy的接口,而Provider和Lazy除了名字不同以為,一模一樣。都是提供一個Get方法。再次體現了接口抽象的命名標記法。
而我們的Heater自然也是集Lazy和Singleton為一體的。這里的CoffeeMaker直接就是一個Lazy,一個代理,暫時不做任何操作。進下一步。
PumpModule
直接調用方法生產數據,因為沒有聲明為Singleton,則直接new一個就好。其實就是我們平時寫的工廠模式的get,不過我們寫的時候直接返回一個new值,人家這里幫忙new了,丟進來。沒啥大問題。真正的問題又回到了Heater,由於是單例的,必然不能直接new,需要去找持有單例的工廠類拿。而provideHeaterProvider
就是前面的DoubleCheck
代理。
private static final Object UNINITIALIZED = new Object();
private volatile Provider<T> provider;
private volatile Object instance = UNINITIALIZED;
private DoubleCheck(Provider<T> provider) {
assert provider != null;
this.provider = provider;
}
@SuppressWarnings("unchecked") // cast only happens when result comes from the provider
@Override
public T get() {
Object result = instance;
if (result == UNINITIALIZED) {
synchronized (this) {
result = instance;
if (result == UNINITIALIZED) {
result = provider.get();
/* Get the current instance and test to see if the call to provider.get() has resulted
* in a recursive call. If it returns the same instance, we'll allow it, but if the
* instances differ, throw. */
Object currentInstance = instance;
if (currentInstance != UNINITIALIZED && currentInstance != result) {
throw new IllegalStateException("Scoped provider was invoked recursively returning "
+ "different results: " + currentInstance + " & " + result + ". This is likely "
+ "due to a circular dependency.");
}
instance = result;
/* Null out the reference to the provider. We are never going to need it again, so we
* can make it eligible for GC. */
provider = null;
}
}
}
return (T) result;
}
經典的雙重檢查實現了懶漢單例模式。值得學習的是,這里並沒有將null當做初始值,而是給了一個Object。然后把真正的生產數據的功能抽象,提出來稱為Provider。這個Provider就是前面提到的真正干事情的工廠DripCoffeeModule_ProvideHeaterFactory
。負責new一個instance出來。然后,值得學習的地方來了。因為單例模式已經不再需要工廠了,那么這個工廠類可以回收了。我們自己的編程習慣是扔着不管,請保姆(垃圾收集器)來干活。這里直接設置為null,值得注意,雖然大家都懂但不一定都會這樣寫。
至此,全部分析結束。生成的代碼不復雜,但抽象度極高,雖然看的容易,但想象出並設計成這樣就很難了。百度里一堆自己實現一個DI啥的,說起來簡單,DI就是一個工廠模式。但你設計的DI有考慮這么多東西嗎。如果沒有這么高度的抽象,你如何才能少量的代碼實現如此眾多高效的功能?是時候學習源碼了。
Lazy and Singleton
上面的例子,使用DoubleCheck實現了單例模式的懶漢式。同時,又是懶加載Lazy。讓人以為,Lazy和Singleton是一回事。但並不是這樣。Lazy的javac注釋中有:
Note that each injected {@code Lazy} is independent, and remembers its value in isolation of other {@code Lazy} instances.
Lazy是一種延遲加載手段,其實就是在真實instance外面增加了一層包裹,只有當需要調用的時候才會啟用get
方法創建一個instance。而DoubleCheck同時繼承了Provider和Lazy,因此看着像是單例和延遲加載同體了。
4. SubComponent
事實上,到這里dagger的用法對於服務端來說已經足夠了。通過module的連接特性可以定義IoC容器范圍,再結合dropwizard,就和springboot一樣了。然而,畢竟dagger2是為了Android而打造的,為了適應其復雜的繼承體系和生命周期的限制,dagger提供了SubComponent模型。也就是子組件。
剛看到這里會好奇,module已經可以把bean提供出來注入了,為啥還需要子組件?
我並沒有真實的在生產環境中使用過dagger,全部認知也就來自對官方文檔里的理解。對於Subcomponent的作用,大概有兩點: 1)繼承擴展功能並綁定生命周期,2)封裝。
繼承體現在subcomponent可以使用parent的module,共享其生命周期。
封裝則是因為但其他人都不可以使用subcomponent的依賴,只能使用subcomponent本身。也就是parent里的Component不能調用subcomponent里的module。
暫時沒能理解subcomponent和scope的使用,感覺有些復雜。將在項目中簡單使用Module,因為期待得到的DI是最小侵入性的提供inject功能,而考慮這些層次關系以及作用范圍,會導致耦合性增強,偏離了最初引入DI的意願。目前掌握:我需要一個instance,dagger給一個instance給我injec。不需要考慮任何其他問題。
用法總結
@Component
用來標注Component,最外層,the bean could only be exposed@Module
負責管理依賴- 使用
@Provides
可以提供instance,當無法自動綁定的時候,比如接口和實現類 - 使用
@Inject
可以讓IoC容器負責生成instance,如果沒有這個注解,dagger將不認識,當做普通類,無法代理 - 在使用
@Component
的時候必須要提供scope范圍,標准范圍是@Singleton
@Component
在使用@Module
的時候必須匹配相同的scope- 通過
@Component.modules
或者@Module.includes
可以把依賴連接成一個圖,可以互相inject - 能使用Singleton的時候,要注意標注,否則默認多例
命名規約
- @Provides方法用provide前綴命名
- @Module 用Module后綴命名
- @Component 以Component作為后綴
此文為官方文檔讀后感,至於生產環境的應用問題,將在后面使用后補充。
<未完待續>