zoukankan      html  css  js  c++  java
  • Dagger2 入门解析

    前言

    在为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>, 并且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作为后缀

    此文为官方文档读后感,至于生产环境的应用问题,将在后面使用后补充。
    <未完待续>

    参考

  • 相关阅读:
    各国语言缩写列表,各国语言缩写-各国语言简称,世界各国域名缩写
    How to see log files in MySQL?
    git 设置和取消代理
    使用本地下载和管理的免费 Windows 10 虚拟机测试 IE11 和旧版 Microsoft Edge
    在Microsoft SQL SERVER Management Studio下如何完整输出NVARCHAR(MAX)字段或变量的内容
    windows 10 x64系统下在vmware workstation pro 15安装macOS 10.15 Catelina, 并设置分辨率为3840x2160
    在Windows 10系统下将Git项目签出到磁盘分区根目录的方法
    群晖NAS(Synology NAS)环境下安装GitLab, 并在Windows 10环境下使用Git
    使用V-2ray和V-2rayN搭建本地代理服务器供局域网用户连接
    windows 10 专业版安装VMware虚拟机碰到的坑
  • 原文地址:https://www.cnblogs.com/woshimrf/p/hello-dagger.html
Copyright © 2011-2022 走看看