(译) Dagger 2 用户指南

原文链接

任何应用中的最好的类都是那些干实事的:BarcodeDecoder, KoopaPhysicsEngine,以及 AudioStreamer。这些类会有依赖,可能是 BarcodeCameraFinder,DefaultPhysicsEngine 以及 HttpStreamer。

相反的,最坏的类是那些占着位置而根本没有做什么事情的:BarcodeDecoderFactory,CameraServiceLoader,以及 MutableContextWrapper。这些类就像是笨重的强力胶带把有意思的东西连接起来。

Dagger 是这些工厂类的替代品,Dagger 实现了依赖注入模式,却没有编写样板代码的负担。它允许你专注于有意思的类。声明依赖,具体说明如何满足这些依赖,然后推出你的应用。

基于标准的 javax.inject 注解 (JSR 330)进行构建,每个类都是易于测试的。你不需要一大堆的样板代码仅仅去从 RpcCreaditCardService 交换出一个 FakeCreditCardService。

依赖注入不仅仅是为了测试的。它也使创建可重用,可交换模块变得简单。你可以在你的应用中共享相同的 AuthenticationModule。并且你可以在开发期间运行 DevLoggingModule ,产品中使用 ProdLoggingModule ,从而在每个环境中做正确的事情。

为什么 Dagger 2 是不同的

依赖注入框架已经存在多年了,有大量的 API 来进行配置和注入。那么,为什么要重新造轮子呢?Dagger 2 是首个用生成的代码来实现整套功能的。生成代码的指导原则是生成的代码模仿用户可能手写的代码,来确保依赖注入尽可能地简单,可追踪和高性能。想要更多地了解设计的背景,请观看这个 Gregory Kick 的演讲

使用 Dagger

我们将通过构建一个咖啡机来演示依赖注入和 Dagger。至于完整的你可以编译和运行的样例代码,你可以查看 Dagger 的 coffe example

声明依赖

Dagger 构建了你应用的类的实例并满足它们的依赖。它使用 javax.inject.Inject 注解来标识它对哪个构造器和字段感兴趣。

使用 @Inject 来注解构造函数,Dagger 应该使用它来创建新的类实例。当一个新的实例被请求的时候,Dagger 会获取所需的参数值并调用这个构造函数。

1
2
3
4
5
6
7
8
9
10
class Thermosiphon implements Pump {
private final Heater heater;

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

...
}

Dagger 能给直接注入字段。在这个例子中它为 heater 字段获取了一个 Heater 实例以及为 pump 字段获取了 Pump 实例。

1
2
3
4
5
6
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;

...
}

如果你的类有着 @Inject 注解过的字段但是没有 @Inject 注解过的构造函数,如果需要的话 Dagger 会注入这些字段,但是不会创建新的实例,使用 @Inject 添加一个无参的构造函数会意味着 Dagger 可能也会创建实例。

Dagger 也支持方法注入,虽然构造函数或字段注入是首选的。

缺少 @Inject 注解的类,不能够被 Dagger 构建。

满足依赖

默认情况下,Dagger 通过按所以上所描述地构建所请求类型的实例满足每个依赖。当你请求一个 CoffeeMaker, 它会通过调用 new CoffeeMaker() 获取一个然后设置它可注入的字段。

但是 @Inject 不是每个地方都适用的:

  • 接口不能被构造
  • 第三方类不能添加注解
  • 可配置的对象必须是可配置的!

对于这些情形来说,@Inject 是不足的或者说是使用不便的,使用一个添加了 @Provides 注解方法来满足依赖。这个方法的返回类型定义了满足它的依赖。

对于例子来说,当 Heater 被需要的时候 provideHeater() 被调用了:

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

@Provider 方法可能有着它们自己的依赖。当 Pump 被需要的时候,这个方法返回了 Thermosiphon 。

所有 @Provides 方法必须属于一个 module。它们仅仅是有着一个 @Module 注解的类。

1
2
3
4
5
6
7
8
9
10
@Module
class DripCoffeeModule{
@Provides static Heater provideHeater(){
return new ElectricHeater();
}
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}

}

按照惯例,@Provides 方法被被命名的时候加上了一个 provide 前缀并且 module 类命名的时候加上了 Module 前缀。

构建图

加了 @Inject 和 @Provides 注解的类形成了一个由它们的依赖链接而成对象的图。
调用代码(如应用程序的主方法或Android应用程序),这些代码通过明确定义的根集合来访问该图。在 Dagger 2中,这个集合由一个接口定义,并由无参的方法返回想要的类型。通过将@Component 注解应用到这个的一个接口然后传递 module 类型到 modules参数,Dagger 2 然后就能完整地生成协议的实现。

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

实现有着与接口一样的名字并以 Dagger 为前缀,通过调用实现上的 builder() 方法并使用返回的 builder 设置依赖并 build() 一个新的实例。

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

注意:如果你的 @Component 不是一个顶层类型,生成的组件的名词会包含它的围绕类型的名称,并以下滑线连接。例如,这个代码:

1
2
3
4
5
6
class Foo {
static class Bar {
@Component
interface BazComponent {}
}
}

会生成一个叫做 DaggerFoo_Bar_BazComponent.

任何具有可访问的默认构造函数的模块都可以省略,因为如果没有设置,构建器将自动构造实例。对于任何 @Provides 方法都是静态的模块,实现根本不需要实例。如果可以在没有用户创建依赖实例的情况下构造所有依赖,那么生成的实现也将具有 create() 方法,该方法可用于获取新实例而无需处理构造器。

1
CoffeeShop coffeeShop = DaggerCoffeeShop.create();

现在,我们的 CoffeeApp 能够简单地使用 Dagger 生成的 CoffeeShop 实现来获取一个注入完全的 CoffeeMaker。

1
2
3
4
5
6
public class CoffeeApp {
public static void main(String[] args) {
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
coffeeShop.maker().brew();
}
}

现在图构建完成了,入口被注入了,我们可以运行我们的咖啡机应用了。

1
2
3
4
$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P

图中的绑定

上面的示例显示了如何使用一些更典型的绑定构建组件,但是有多种机制可以为图提供绑定。以下可用作依赖,可用于生成良好的组件:

  • 由 @Component.modules 直接引用的 @Module 中的 @Provides 方法声明的那些或者通过 @Module.includes 传递的
  • 具有 @Inject 构造函数的任何类型,该构造函数无范围的,或者具有与组件范围之一匹配的 @Scope 注解
  • 组件依赖的 component provision methods
  • 组件本身
  • 任何包含的子组件的无限定构建器
  • 任何上述绑定的 Provider 或 Lazy 包装器
  • 任何上述绑定的 Lazy Provider(例如,Provider<Lazy>)
  • 适用于任何类型的 MembersInjector

单例和限定范围的绑定

使用 @Singleton 注解 @Provides 注解的方法或可注入类。该图将为其所有客户端使用该值的单个实例。

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

注入类上的 @Singleton 注解也可用作文档。它提醒潜在的维护者这个类可能被多个线程共享。

1
2
3
4
@Singleton
class CoffeeMaker {
...
}

由于 Dagger 2 将图中的范围实例与组件实现的实例相关联,因此组件本身需要声明它们要表示的范围。例如,在同一组件中使用 @Singleton 绑定和 @RequestScoped 绑定没有任何意义,因为这些范围具有不同的生命周期,因此必须存在于具有不同生命周期的组件中。要声明组件与给定范围相关联,只需将范围注解应用于组件接口即可。

1
2
3
4
5
@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
CoffeeMaker maker();
}

组件可能应用了多个范围注解。这声明它们都是同一范围的别名,因此该组件可能包含与其声明的任何范围的范围绑定。

可重用范围

有时你希望限制实例化 @Inject 构造的类或调用 @Provides 方法的次数,但你不需要保证在任何特定组件或子组件的生命周期中使用完全相同的实例。这在 Android 等环境中很有用,因为分配可能很昂贵。

对于这些绑定,您可以应用 @Reusable 范围。 @Reusable-scoped 绑定与其他范围不同,不与任何单个组件关联;相反,实际使用绑定的每个组件将缓存返回或实例化的对象。

这意味着如果在组件中安装带有 @Reusable 绑定的模块,但只有子组件实际使用绑定,则只有该子组件将缓存绑定的对象。如果两个不共享祖先的子组件各自使用绑定,则每个子组件将缓存其自己的对象。如果组件的祖先已经缓存了对象,则子组件将重用它。

无法保证组件只调用绑定一次,因此将 @Reusable 应用于返回可变对象的绑定,或者引用相同实例的对象是危险的。将 @Reusable 用于不可变对象是安全的,如果你不关心它们分配了多少次,你可以保留无范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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() {}
}

懒注入

有时你需要一个懒惰地实例化的对象。对于任何绑定 T,你可以创建一个延迟实例化的Lazy ,直到第一次调用 Lazy 的 get() 方法。如果 T 是单例,则对于 ObjectGraph 中的所有注入,Lazy 将是相同的实例。否则,每个注入站点将获得自己的 Lazy实例。无论如何,对任何给定的 Lazy 实例的后续调用将返回相同的底层 T 实例。

1
2
3
4
5
6
7
8
9
10
class GrindingCoffeeMaker {
@Inject Lazy<Grinder> lazyGrinder;

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

Provider 注入

有时你需要返回多个实例而不是仅注入单个值。虽然你有几个选项(工厂,构建器等),但是一个选项是注入 Provider 而不仅仅是 T.Anvery 每次调用.get()时调用T的绑定逻辑。如果该绑定逻辑是 @Inject 构造函数,则将创建一个新实例,但 @Provides 方法没有这样的保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
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>但在某些情况下可以节省生命。一个常见的用途是当你必须使用不符合对象自然生命周期的遗留架构时(例如,servlet是设计中的单例,但仅在请求特定数据的上下文中有效)。

限定词(Qualifiers)

有时单独的类型不足以识别依赖关系。例如,一个复杂的咖啡机应用程序可能需要单独的加热器用于水和热板。

在这种情况下,我们添加限定词注解。这是任何本身都有 @Qualifier 注解的注解。这是 @Named 的声明,javax.inject 中包含的限定词注解:

1
2
3
4
5
6
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}

你可以创建自己的限定词注释,或者只使用 @Named。通过注解感兴趣的字段或参数来应用限定词。类型和限定词注解都将用于标识依赖。

通过注解相应的 @Provides 方法来提供限定值。

1
2
3
4
5
6
7
@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
return new ElectricHeater(70);
}

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

依赖可能没有多个限定符注解。

可选的绑定

如果你想要在一些依赖没有绑定到组件的时候,绑定仍然能工作,你可以添加 @BindOptionalof 方法到 module。

1
@BindsOptionalOf abstract CoffeeCozy optionalCozy();

这意味着 @Inject 构造函数、成员变量以及 @Provides 方法能够依赖于 Optional 对象。如果组件中有针对 CoffeeCozy 的绑定, 可选的就会呈现出来,如果没有有针对 CoffeeCozy 的绑定,可选的就不会被呈现出来。

特别的,你可以注入以下的所有东西:

  • Optional<CoffeeCozy> (除非有针对 CoffeeCozy 的 @Nullable 绑定)
  • Optional<Provider<CoffeeCozy>>
  • Optional<Lazy<CoffeeCozy>>
  • Optional<Provider<Lazy<CoffeeCozy>>>

(你也可以注入其中任何一个的提供者或Lazy或Lazy提供者,但这不是很有用。)

如果 CoffeeCozy 有绑定,并且该绑定是 @Nullable,那么注入 Optional是一个编译时错误,因为 Optional 不能包含 null。你始终可以注入其他表单,因为 Provider和 Lazy 始终可以从其 get() 方法返回null。

如果子组件包含底层类型的绑定,则一个组件中不存在的可选绑定可以存在于子组件中。

你可以使用 Guava 的 Optional 或 Java 8 的 Optional。

绑定实例

通常,在构建组件时,你可以获得数据。例如,假设你有一个使用命令行参数的应用程序;你可能希望在组件中绑定这些参数。

也许你的应用程序只需要一个参数来表示你想要注入的用户名 @UserName String。你可以将注解 @BindsInstance 的方法添加到组件构建器,以允许将该实例注入组件中。

1
2
3
4
5
6
7
8
9
10
@Component(modules = AppModule.class)
interface AppComponent {
App app();

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

你的应用可能看起来像这样

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
if (args.length > 1) { exit(1); }
App app = DaggerAppComponent
.builder()
.userName(args[0])
.build()
.app();
app.run();
}

在上面的示例中,在组件中注入 @UserName String 将在调用此方法时使用提供给 Builder 的实例。在构建组件之前,必须调用所有 @BindsInstance 方法,传递非 null 值(下面的@Nullable 绑定除外)。

如果 @BindsInstance 方法的参数标记为 @Nullable,那么绑定将被视为“可空”,就像@Provides 方法可以为空一样:注入站点也必须将其标记为 @Nullable,而null是可接受的值绑定。此外,Builder 的用户可能省略调用该方法,并且该组件将该实例视为 null。

@BindsInstance 方法应该优先使用构造函数参数编写 @Module 并立即提供这些值。

编译时验证

Dagger 注解处理器是严格的,如果任何绑定无效或不完整,将导致编译器错误。例如,此模块安装在组件中,该组件缺少 Executor 的绑定:

1
2
3
4
5
6
@Module
class DripCoffeeModule {
@Provides static Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}

编译时,javac 弹出绑定缺失的错误:

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

通过向组件中的任何模块添加 Executor的 @Provide-annotated 方法来解决此问题。虽然@Inject,@Module 和 @Provides 注解是单独验证的,但绑定之间关系的所有验证都发生在@Component 级别。 Dagger 1 严格依赖于 @Module 级验证(可能或可能没有反映运行时行为),但是 Dagger 2 省略了这种验证(以及@Module上的配置参数),支持完整图验证。

编译时代码生成

Dagger 的注解处理器也可以生成名称为 CoffeeMaker_Factory.java 或CoffeeMaker_MembersInjector.java的源文件。这些文件是 Dagger 的实现细节。你不应该直接使用它们,尽管它们在通过注人进行步骤调试时非常方便。你应该在代码中引用的唯一生成类型是你的组件的前缀为 Dagger 的类型。

在你的构建中使用 Dagger

你需要在应用程序的运行时中包含 dagger-2.X.jar。为了激活代码生成,你需要在编译时在构建中包含 dagger-compiler-2.X.jar。有关更多信息,请参阅自述文件。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号