Dagger2 入門解析

前言

在爲dropwizard選擇DI框架的時候考慮了很久。Guice比較成熟,Dagger2主要用於Android。雖然都是google維護的,但Dagger2遠比guice更新的頻率高。再一個是,Dagger2不同於guice的運行時注入,編譯時生成代碼的做法很好。提前發現問題,更高的效率。

作者:@Ryan-Miao
本文爲作者原創,轉載請註明出處:http://www.cnblogs.com/woshimrf/p/hello-dagger.html

還是那句話,百度到的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>, 並且includePumpModule。顯然,我們的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作爲後綴

此文爲官方文檔讀後感,至於生產環境的應用問題,將在後面使用後補充。
<未完待續>

參考

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