翻译Dagger 2使用指南

官方文档链接:https://google.github.io/dagger/users-guide.html

1.前言


应用中最好的类都是那些干实事的(业务类),例如,BarcodeDecoder、KoopaPhysicsEngine和AudioStreamer。它们会与别的类产生依赖关系,也许是BarcodeCameraFinder、DefaultPhysicsEngine和HttpStreamer。相比之下,应用中最糟糕的类是那些占用空间却不太干事的(垃圾类),例如,BarcodeDecoderFactory,CameraServiceLoader和MutableContextWrapper。这些类臃肿,像胶带一样连接着真正干活的类。

Dagger通过实现依赖注入设计模式,摆脱写样板代码的负担,从而取代上述的垃圾类,将精力放在业务类上。只需声明依赖关系,提供所需的依赖项,然后运行应用。因为是按标准的javax.inject注解(JSR 330)构建,每个类都很容易测试。不需要改一堆样板代码,仅仅将RpcCreditCardService换成FakeCreditCardService即可满足测试。

依赖注入不仅仅是为了测试,还能容易地创建可复用的、可替换的模块。既可以在整个应用中共享同一个的AuthenticationModule;也可以在开发环境中运行DevLoggingModule,到生产环境中换成ProdLoggingModule,保证不同的环境下都执行正确的行为。

2.Dagger 2的特点


依赖注入框架已经出现好多年了,并且有一整套用于配置和注入的API。那么,为什么要重复造轮子?因为Dagger 2是首个用编译生成的代码实现完整逻辑的框架,要求是:模仿人工编写的风格生成代码,并保证依赖注入尽可能的简单、可追踪和高性能。

3.使用Dagger


下面通过构建咖啡机这个实例来演示依赖注入和Dagger。

3.1.声明依赖关系

Dagger为应用构建实例并提供它们所需的依赖项,使用javax.inject.Inject注解来标识要关注的构造函数和属性。

@Inject可以注解Dagger想要创建实例的那个类的构造方法。当需要它新的实例时,Dagger会获取必需的参数,然后调用这个构造方法。

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }

  ...
}

Dagger可以直接注解属性。下面的例子,它将会获取实例赋值给对应的属性。

class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;

  ...
}

如果某个类有 @Inject注解的属性但没有它注解的构造方法,Dagger会给这些属性赋值,而不会创建这个类新的实例。若加上 @Inject注解的无参构造方法,则能让Dagger创建对应的实例。虽然构造方法和属性注入是首选,但Dagger也支持方法注入。没有 @Inject注解的类,Dagger将不会采取措施。

3.2.提供所需依赖

默认情况下,Dagger通过构造上面描述的所需类型的实例来提供依赖。当你需要一个CoffeeMaker时,它将会调用new CoffeeMaker()方法获取实例,并设置给被注入的属性。@Inject在以下场景将无法正常工作:

  • 接口不能被构造。
  • 第三方类不能被注解。
  • 需通过配置才能使用的对象(例如Builder模式)。

对于这些@Inject无能为力的场景,可使用@Provides注解的方法提供依赖,返回的类型决定提供什么样的依赖。下面例子中,当Heater对象被需要时,provideHeater()方法将被调用。

@Provides static Heater provideHeater() {
  return new ElectricHeater();
}

@Provides注解的方法也会需要依赖,当被调用时,Dagger需能提供必要的参数。

@Provides static Pump providePump(Thermosiphon pump) {
  return pump;
}

所有@Provides注解的方法必须在一个模块中,即@Module注解的类。

@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater() {
    return new ElectricHeater();
  }

  @Provides static Pump providePump(Thermosiphon pump) {
    return pump;
  }
}

为了方便起见,@Provides注解的方法以provide做前缀命名,而@Module注解的类以Module做后缀命名。

3.3.构建依赖图

@Inject@Provides注解相关的对象,通过彼此间的依赖,形成一张关系图。当被应用的main()方法或安卓的Application调用时,将通过一个规范定义的根节点集合访问这张图。在Dagger 2 中,每个根节点是通过对应接口的无参方法定义的,返回的就是所需类型。通过给这样的接口使用@Component注解,并向该注解的modules 参数传递所需的module类型,Dagger 2就会完整地生成该关联的实现。

@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
  CoffeeMaker maker();
}

组件构造类与接口有着相同的名字,不过加了Dagger作为前缀。调用构造类的builder()方法获取builder对象,然后给它设置依赖,最后调用build()方法产生所需Component实例。

CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();

如果@Component注解的接口不在最外层,那么编译生成的组件构造类的名字将由下划线连接那些包含它的类的名字组成。如下面的例子所示,生成的组件构造类的名字为DaggerFoo_Bar_BazComponent

class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}

若所有的依赖项不需要用户手动创建,那么生成的组件构造类将拥有create()方法,可不通过builder对象获取所需Component实例。这种情况有两种:

  • 任一拥有可访问的默认构造函数的Module,若不需要额外设置,可免去手动构建,编译时自动创建实例。
  • 任一Module的@Provides注解的方法都是静态的,则组件构造类不需要它的实例来设置依赖。
CoffeeShop coffeeShop = DaggerCoffeeShop.create();

现在,CoffeeApp可以方便地使用Dagger生成的CoffeeShop对象来获取注入完成的CoffeeMaker。

public class CoffeeApp {
  public static void main(String[] args) {
    CoffeeShop coffeeShop = DaggerCoffeeShop.create();
    coffeeShop.maker().brew();
  }
}

到此,依赖图构建好了,使用的入口点也已经生成了,开始运行程序。

$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
 [_]P coffee! [_]P
3.4.构图最佳实践

上面的例子展示了如何使用那些典型的规则构建一个Component,但是还有一些其它方法为依赖图添加关系。以下是有效的依赖方式,可用于生成格式良好的Component:

  • Module中那些被@Provides注解声明的方法可被@Component.modules直接引用或者通过@Module.includes传递
  • 任何被@Inject注解构造方法的类都没有作用域,可通过@Scope注解来匹配某个Component的作用域
  • 在Component中,通过无参的方法返回依赖图提供的类型
  • Component自己
  • 通过@Subcomponent.Builder提供所需类型
  • 用Provider或Lazy包装上述任意依赖项
  • 用Provider包装Lazy再包装上述任意依赖项(例如,Provider<Lazy<CoffeeMaker>>
  • 在第一次执行构造函数注入之后执行MembersInjector中你希望的注入

若上述的内容不太理解,又等不及看后期写的文章,可以看这个系列的文章。它是翻译国外开发者写的博客,内容挺全的。

4.单例和作用域


@Singleton注解已被@Provides注解的方法或是已被@Inject注解构造方法的,依赖图将提供唯一对象的值给所有使用者。

@Provides @Singleton static Heater provideHeater() {
  return new ElectricHeater();
}

上述的@Singleton注解第二种用法,带有@Documentation注解的作用。它提醒潜在维护者,这个类可能会被多个线程共享。

@Singleton
class CoffeeMaker {
  ...
}

既然Dagger 2将依赖图中被指定作用域的实例与Component中的实例相关联,那么Component也得声明自己想要表现的作用域。举个例子,在一个Component上同时使用@Singleton@RequestScoped注解是不合理的,因为这些作用域有不同的生命周期,所以作用域之间是互斥的。在Component的接口上直接使用作用域注解,来将Component与被给定的作用域关联。有种情况,Component可能会使用多个作用域注解,即当它们都是同一个作用域的不同别名。

@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
  CoffeeMaker maker();
}

5.可复用的作用域


有时候希望限制@Inject注解构造函数的类初始化的次数或者是被@Provides注解的方法调用的次数,但又不需要保证在特定Component或Subcomponent的生命周期内是同一个实例,这对安卓之类资源使用紧张的环境是有用的。

此时可以使用@Reusable作用域,不像其它作用域,不会与某一个Component关联;相反,实际使用的是来自于缓存或创建的。意味着,在Component中使用@Reusable注解的Module,但又只是一个Subcomponent在用,那么仅仅这个Subcomponent会缓存。若两个非同一父Component的Subcomponent分别使用此作用域,它们会各自缓存自己的。如果父Component已经缓存了,Subcomponent可直接使用。

在不保证Component仅调用注解对象一次的情况下,要求使用@Reusable作用域返回不同的对象或同一对象是不可靠的。应该在只关心对象本身而不关心是否是同一对象的情况下使用。

@Reusable // It doesn't matter how many scoopers we use, but don't waste them.
class CoffeeScooper {
  @Inject CoffeeScooper() {}
}

@Module
class CashRegisterModule {
  @Provides
  @Reusable // DON'T DO THIS! You do care which register you put your cash in.
            // Use a specific scope instead.
  static CashRegister badIdeaCashRegister() {
    return new CashRegister();
  }
}

@Reusable // DON'T DO THIS! You really do want a new filter each time, so this
          // should be unscoped.
class CoffeeFilter {
  @Inject CoffeeFilter() {}
}

6.可释放的引用


当使用作用域注解,就意味着Component对象持有被注解对象的引用,直到Component对象自己被垃圾回收。像安卓这种内存有限的环境,可能希望当前不用的作用域对象能在应用内存不足需垃圾回收时删除。

这种情况下,可以先定义一个作用域并添加@CanReleaseReferences注解。

@Documented
@Retention(RUNTIME)
@CanReleaseReferences
@Scope
public @interface MyScope {}

若确定允许作用域内的对象,如果当前未被其它对象使用,在垃圾回收时被删除。那么可以为上面的作用域注入ReleasableReferenceManager对象,并调用它的releaseStrongReferences()方法,使Component持有注解对象的弱引用而不是强引用。

@Inject @ForReleasableReferences(MyScope.class)
ReleasableReferenceManager myScopeReferenceManager;

void lowMemory() {
  myScopeReferenceManager.releaseStrongReferences();
}

当内存空间又足够了,可以调用restoreStrongReferences()方法为已经缓存但在垃圾回收时未被删除的对象恢复强引用。

void highMemory() {
  myScopeReferenceManager.restoreStrongReferences();
}

7.懒注入


有时候需要某个对象晚点初始化。任意对象T都可以创建相应的Lazy<T>对象,推迟T的实例化直到首次调用它的get()方法。如果T是单例,那么Lazy<T>在依赖图提供的所有注入都将是同一个实例;否则,Lazy<T>将是自己独有的对象。无论后来怎么调用Lazy<T>对象,都将返回同一对象T。

class GrindingCoffeeMaker {
  @Inject Lazy<Grinder> lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder created once on first call to .get() and cached.
      lazyGrinder.get().grind();
    }
  }
}

8.多次注入


有时候需要一次返回多个实例而不是仅仅注入一个值。虽然有几种选择(Factories,Builders等),这里提供注入Provider<T>代替T。每当调用Provider<T>get()方法,将调用T的依赖逻辑。若依赖逻辑是@Inject注解的构造函数,则创建一个新的实例T;但@Provides注解的方法没这个能力。

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>有可能创建混乱的代码,而且会使依赖图中的对象缺乏作用域或缺乏结构性。常见的使用场景,通过工厂模式或Lazy<T>或重组作用域或重构代码来仅仅注入一个对象T。不到万不得已,不要注入Provider<T>,通常是必须使用老的架构,不能管理对象的原始作用域(例如,Servlet被设计成单例,只在特定数据请求场景下才有效)。

9.修饰


有时候单独的类型不足以确定一个依赖。例如,一个复杂的咖啡机可能希望区分水加热和电加热。那么,任意创建一个注解@Named,给它添加来自于javax.inject@Qualifier注解。修饰注解的使用同样是互斥的。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}

可以对属性或感兴趣的参数使用修饰注解,它将和类型共同确定依赖项。

class ExpensiveCoffeeMaker {
  @Inject @Named("water") Heater waterHeater;
  @Inject @Named("hot plate") Heater hotPlateHeater;
  ...
}

提供方根据修饰注解调用对应的@Provides注解的方法。

@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named("water") static Heater provideWaterHeater() {
  return new ElectricHeater(93);
}

10.可选依赖


如果希望Dagger在Component缺少一些依赖的情况下正常工作,可为某Module添加@BindsOptionalOf注解的方法。

@BindsOptionalOf abstract CoffeeCozy optionalCozy();

这意味着@Inject注解的构造方法和属性与@Provides注解的方法可以依赖于Optional<CoffeeCozy>对象。如果Component中存在对CoffeeCozy的依赖,那么Optional将起作用;如果Component中不存在对CoffeeCozy的依赖,那么Optional将忽略。可以注入以下任何类型:

  • Optional<CoffeeCozy>
  • Optional<Provider<CoffeeCozy>>
  • Optional<Lazy<CoffeeCozy>>
  • Optional<Provider<Lazy<CoffeeCozy>>>

如果Subcomponent包含相关类型的依赖,可选依赖可以不在Component中,而在Subcomponent中。

11.依赖实例


通常正在构建Component时,便有了可用数据。举个例子,想象一个应用使用命令行参数;可能希望将这些参数关联到Component中。也许应用接收单个参数表示用户的名字,注入到@UserName注解的字符串中。给Component.Builder添加@BindsInstance注解的方法来允许实例注入到Component中。

@Component(modules = AppModule.class)
interface AppComponent {
  App app();

  @Component.Builder
  interface Builder {
    @BindsInstance Builder userName(@UserName String userName);
    AppComponent build();
  }
}

使用的方法如下:

public static void main(String[] args) {
  if (args.length > 1) { exit(1); }
  App app = DaggerAppComponent
      .builder()
      .userName(args[0])
      .build()
      .app();
  app.run();
}

在上面的例子中,调用注解的方法将@UserName注解的字符串提供给Builder,从而实现命令行参数注入到Component中。在构建Component(即调用build()方法)之前,所有@BindsInstance注解的方法必须被调用,传入非空的值(除了@Nullable注解的)。

如果@BindsInstance注解的方法的参数被注解为@Nullable,那么依赖图中的对象也将认为可空,同样地认为@Provides注解的方法返回可空;被注入的地方也必须用@Nullable注解,而且null是一个可接受的值。此外,用户可以省略@BindsInstance注解的方法的调用,component默认传入的实例为null。

@BindsInstance注解的方法应该优先传给@Module注解的类的构造函数参数,以便最快提供值。

12.编译时验证


Dagger的注解处理器是很严格的,如果任何依赖是无效的或不完整的,将会导致编译时错误。举个例子,下面的Module缺少对Executor的依赖:

@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}

当编译它时,javac拒绝缺失的依赖:

[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.

在Component的任何Module中,为Executor添加@Provides注解的方法来解决问题。然而@Inject@Module@Provides这些注解都是单独验证的,所有对依赖之间关系的验证发生在@Component这一层。

Dagger 1依靠严格的@Module层验证(不能完全反映运行时行为),但是Dagger 2使用依赖图完整性验证来取代这样的验证(和对Module相应的配置参数)。

13.编译时代码生成


Dagger的注解处理器生成的源文件命名如下:CoffeeMaker_Factory.javaCoffeeMaker_MembersInjector.java,它们反映Dagger实现的详情。不应该直接使用它们,即使它们能在逐步调试中通过注入追踪到。编码中应该使用到的唯一生成类型是以Dagger为前缀的组件构造类。

14.总结


关于Dagger 2的使用,网上能搜到很多,但是却很难看到有对官网的翻译。本着官网文档的内容最全最新的思想,我还是尝试着翻译和学习。由于英语水平有限,翻译起来不是很顺畅,望大家见谅,并指出错误。这是系列文章,我会接着翻译官网后续的文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270