Ryan Cheng Android Develop Blog

Dagger 2

Dagger2是Dagger1的分支,由谷歌公司接手开发,是针对android和java的一个依赖注入框架。

最理想的类应该是这样的: BarcodeDecoderKoopaPhysicsEngineAudioStreamer。这些类依赖其它模块,可能是BarcodeCameraFinderDefaultPhysicsEngine,或HttpStreamer

作为对比,不好的类是那些占用空间却没有做多少事情的:BarcodeDecoderFactoryCameraServiceLoaderMutableContextWrapper。 这些类只是很笨拙地起到连接作用而已。

Dagger就是这些工厂类的替代品,它使用dependency injection 设计模式, 你不再需要写这些模板。它让你可以专注于功能开发。声明依赖关系,指定需要什么,就可以了。

基于标准javax.inject注解(JSR 330),每个类都易于测试。你不用仅仅为了交换RpcCreditCardServiceFakeCreditCardService而写一大堆模板。

依赖注入不只是用于测试。它也可以方便地用来创建可重用、可互换的模块。你可以在各模块间共用同一个AuthenticationModule;你也可以选择在开发环境运行DevLoggingModule,在生产环境运行ProdLoggingModule

为何Dagger 2与众不同

Dependency injection框架已出现多年,有一系列用于注解和配置的API。 So,为啥要重新发明轮子?Dagger 2是第一个有代码的完整实现。其指导原则是生成代码减少用户手写,确保dependency injection是简单的,可追溯的,高性能的。想要了解更多的设计背景,点这里 (by +Gregory Kick)。

使用Dagger

我们接下来通过一个coffee maker来解释dependency injection和 Dagger。完整代码见Dagger示例coffee example

声明依赖

Dagger会创建你的类的实例,并且满足它们的依赖。它使用javax.inject.Inject注解来指定要用到的构造方法和字段。

使用@Inject来注解构造方法,Dagger将会用它来创建该类的实例。当需要一个新的实例的时候,Dagger会提供所需参数并调用该构造方法。

class Thermosiphon implements Pump {
  private final Heater heater;

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

  ...
}

Dagger也可以直接inject字段。下面这个例子,它为heater字段提供一个Heater 实例,为pump字段提供一个Pump实例。

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

  ...
}

如果你的类有@Inject注解的字段,但是没有@Inject声明的构造方法,当需要的时候Dagger会注入这些字段,但不会创建新的实例。添加一个@Inject注解的无参数的构造方法,Dagger就会创建实例。

Dagger同样支持方法注入,但是更推荐方法和字段注入。

没有@Inject注解的类不会被Dagger创建。

满足依赖

通常,如上面描述的,Dagger会构建一个需要的类的实例来满足依赖。当你需要一个CoffeeMaker,它会通过调用new CoffeeMaker()提供一个,并且设置好它的可注入字段。

但是@Inject并非所有情况下通用:

  • Interface不可以被创建
  • 第三方类不能被注解
  • Configurable objects must be configured!

对于那些@Inject不满足或不合适的情况,使用@Provides注解。 被注解的方法的返回值表示它可以满足哪些依赖。

例如,无论何时需要一个HeaterprovideHeater()都会被调用:

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

@Provides也可以对自己依赖。下面这个例子,无论何时需要Pump,都会返回一个Thermosiphon

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

@Provides注解的方法必须属于一个module,就是那些用@Module注解的类。

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

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

通常,@Provides注解的方法使用provide做前缀,module 类名使用Module做前缀。

构建图表

@Inject@Provides注解的类形成了一系列对象的图表,通过依赖关系连接。调用代码如application的main方法或Android中的Application通过一个封装良好的集合来调用这个图表。在Dagger 2,这个集合定义为一个interface,包含无参数的方法,返回值为所需对象。通过对该interface添加@Component注解,并且给module参数传递module类,Dagger 2完整实现整个结构。

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

该实现名字以Dagger为前缀加上interface的名字。通过调用该实现的builder()方法,并且使用返回的builder对象来设置依赖然后调用build()来生成一个实例。

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

有默认构造方法的module可以被省略,因为builder会自动创建一个实例。如果所有的依赖都可以用这种方式构建,你也可以直接使用create()方法来代替builder。

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

单例和作用域绑定

@Singleton注解@Provides方法或可注入类,就可以使用单例模式。

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

@Singleton注解可注入类也可以作为文档,提醒维护者这个类可能会被多个线程共用。

@Singleton
class CoffeeMaker {
  ...
}

Since Dagger 2 associates scoped instances in the graph with instances of component implementations, the components themselves need to declare which scope they intend to represent. For example, it wouldn't make any sense to have a @Singleton binding and a @RequestScoped binding in the same component because those scopes have different lifecycles and thus must live in components with different lifecycles. Declaring that a component is associated with a given scope, simply apply the scope annotation to the component interface.

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

延迟注入

有时你需要一个对象延迟初始化。对于任何类T你可以创建一个Lazy,它会推迟初始化直到初次调用Lazy<T>get()方法。如果 T是单例,Lazy<T>将会对所有的注入维持同一个对象。反之,每一个注入都会获得它自己的Lazy<T>实例。无论哪样,对一个Lazy<T>实例的后续调用都返回它包含的T对象。

class GridingCoffeeMaker {
  @Inject Lazy<Grinder> lazyGrinder;

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

Provider注入

有时候你需要返回多个实例,而不只是注入单个值。当然你有许多选择(Factories,Builders等等),有一个方式是注入Provider代替注入TProvider<T>在每次.get()调用的时候调用Tbinding logic,如果该binding logic是一个@Inject构造方法,一个新的实例会被创建。但@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();
      ...
    }
  }
}

Note: 注入Provider<T>可能产生让人迷惑的代码。通常为了注入一个T你会想要使用factoryLazy<T>或重新组织你的代码结构,而注入Provider<T>在某些情况下会更加省事。一个通常的使用场景是你必须使用一个与你的对象的自然生命周期并不一致的遗留的结构(比如,servlets被设计为单例模式,但仅在request-specfic data上下文中有效)。

限定符

有时候声明依赖关系光类本身是不足的。例如,一个高级的coffee maker可能需要单独的热水器和加热板。

在这个案例中,我们添加一个限定符注解。这是一个注解,本身拥有一个@Qualifier注解。下面是@Named的定义,存在于javax.inject中的一个限定符注解:

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

你可以创建你自己的限定符注解,或直接使用@Named,注解成员或参数。类型和限定符注解将会被用来识别一个依赖。

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

提供限定符注解相应的@Provides方法:

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

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

依赖关系不可以有多个限定符注解。

编译时确认

Dagger的注解处理器是严格的,一旦发现无效或未完成的绑定,会抛出一个编译错误。例如,下面这个module安装在一个component中, 它缺少对Executor的绑定:

@Module
class DripCoffeeModule {
  @Provides 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中为Executor添加一个@Provides注解的方法。由于@Inject@Module@Provides注解都是独立验证的,所有绑定关系的验证发生在@Component层。Dagger 1严格依赖@Module层验证,但Dagger 2省去这个验证以及伴随之的参数配置。

编译时代码生成

Dagger的注解处理器会生成源文件,命名类似于CoffeeMaker$$Factory.javaCoffeeMaker$$MembersInjector.java。这些文件是Dagger实现细节。你并不用直接使用它们,尽管它们可以方便的用于单步调试。

在你的构建中使用Dagger

你要在你项目运行时引入dagger-2.0.jar。你还要在编译时引入 dagger-compiler-2.0.jar以生成代码。

对于Maven工程,在pom.xml的dependencies中添加:

<dependencies>
  <dependency>
    <groupId>com.google.dagger</groupId>
    <artifactId>dagger</artifactId>
    <version>2.0</version>
  </dependency>
  <dependency>
    <groupId>com.google.dagger</groupId>
    <artifactId>dagger-compiler</artifactId>
    <version>2.0</version>
    <optional>true</optional>
  </dependency>
</dependencies>

License

Copyright 2014 Google, Inc.
Copyright 2012 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.