Dagger 2
Dagger是针对Android和Java平台提供的依赖注入框架。Dagger 1.*最早版本由 JakeWharton 开发,从 2.0 版本开始正式由 Google 接手。
Dagger 2 的不同之处
依赖注入 是一种设计模式,而不是一个库。框架发展至今也有很长的时间了,面对丰富的工具提供配置与注入,我们为什么要重新造轮子?Dagger 2 是首先实现了一站式的DI实现方案,它在 Dagger 1.* 版本的基础上进行优化,更加注重开发者的体验,使工具运用变得更为简单、可以追踪、性能更高。对比常用的DI框架 Spring、Guice、Dagger 1.* ,它的优势总结如下(详细比较请看 演示文档):
- 优点
- 编译时校验所有依赖配置。提早发现配置的语法及逻辑错误
- 调试简单,可追踪。运行时可以对 DI 的全过程添加断点调试,能够追踪到所有的调用堆栈
- 性能更高。相比于 Java 反射机制实现的DI框架,性能更高
- 缺点
- 不够灵活。注入依赖于 Component 作为中介,需要手动调用
- 不是动态的。子类调用 inject 方法不能注入属于父类的属性
- 不能从 Guice 自动地迁移到 Dagger
如何使用
项目配置
Maven
<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>
Gradle
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' // 3
}
}
... //略
def Dagger2_Version = '2.0'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.3'
//1. Dagger 2.0
compile com.google.dagger:dagger:$Dagger2_Version
apt com.google.dagger:dagger-compiler:$Dagger2_Version
//2. Dagger 2 中会用到 @Generated 注解,而 javax.anotation.generated 在 Java 6 及以上版本中都有,Android API 中没有,所以在Android项目中一定要加此句
provided 'org.glassfish:javax.annotation:10.0-b28'
}
语法介绍
这里演示使用的是 Google 提供的一个煮咖啡程序,你也可以下载示例代码编译和运行,参见 coffee example
声明依赖
Dagger 能够创建类的实例,并为它所需的属性提供注入。通过使用 javax.inject.Inject 指定需要实现注入的构造方法和属性。
对需要使用 Dagger 来创建对象的构造函数添加 @Inject 注解,当需要创建一个实例时,Dagger 会去获取需要的参数,并调用构造函数。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
Dagger 能够直接注入属性。在下面代码里, Header
和 Pump
可以通过 Dagger 直接注入。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
如果一个类中包含 @Inject 注解的属性,但是没有 @Inject 注解的构造函数,如果有需要 Dagger 将注入这些属性,单不会创建一个新的实例。添加一个注解了 @Inject 的无参构造函数后,Dagger可以同时创建实例。
Dagger 也支持方法注入,但是更推荐使用构造方法和属性注入。
缺少 @Inject 的类无法通过 Dagger 创建。
注入依赖
默认情况下,正如上面介绍的 Dagger 通过构造实例的方式实现注入。当需要获得一个 CoffeeMaker
的时候,Dagger会通过 new CoffeeMaker()
创建实例,并注入属性。
注意: @Inject 在以下情况下不可用:
- 接口不能构造
- 第三方类无法添加注解
- 可配置的类必须配置
由于这些缺陷,使用 @Provides 注解方法来实现注入,被注解的方法将返回说依赖的对象。
单需要注入Heater的时候, provideHeater()
将被调用:
@Provides Heater provideHeater() {
return new ElectricHeater();
}
某些情况下 @Provides 方法可能依赖自身的对象:
@Provides Pump providePump(Thermosiphon pump) {
return pump;
}
所有的 @Provides 方法必须属于一个 module
:
@Module
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Pump providePump(Thermosiphon pump) {
return pump;
}
}
通常情况下,建议所有的 @Provides 方法都以 provide
作为前缀,module 模块都是用 Module
作为后缀。
创建映射
@Inject 和 @Provides 注解将类组织成一张或多张有向无环图谱(Graph),通过在Android 的Application中使用 Dagger 根据图谱完成注入。在 Dagger 2 中,这些依赖的映射关系是通过一系列提供了无参方法的接口来实现。为接口添加 @Component 注解并传入 module 列表,Dagger 2 会为其在编译时为其提供具体的注入实现。
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
实现类使用 Dagger
作为前缀,和接口同名。调用 builder()
方法得到 builder 设置依赖并得到一个实例。
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
.build();
每一个包含默认构造函数的 module 可以再 builder()
的构建中省略,Dagger 将自动为其创建并设置实例。如果 Component 所依赖的所有 module 都包含无参构造函数,它的实现类中将提供一个 create()
可以直接创建一个 Component 的实现。
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
得到了 Dagger 生成的 Component 实现类之后,便可以使用注入后的依赖对象了。
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
注解了 @Singleton 的 @Provides 方法或可以注入的类,在图谱(Graph)中将为所有的依赖使用唯一的实例。
@Provides @Singleton Heater provideHeater() {
return new ElectricHeater();
}
如果 Component 类提供的注入使用的 scope 使用的是 @Singleton
,Component 在定义时也需要添加 @Singleton
注解。
@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
CoffeeMaker maker();
}
延时注入
有时候我们需要让依赖的属性延时注入,可以使用 Lazy<T>
完成。只有当第一次调用 Lazy<T>
的 .get()
方法时才会注入。如果 T
是一个单例,那么 Lazy<T>
也将返回同一个对象。不然,每一个注入点得到的都是不同的实例。除此之外,对于同一个 Lazy<T>
多次调用 .get()
返回的是同一个实例。
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<T>
。如果 T
中包含 @Inject 的构造方法,每次调用 .get()
将返回一个新的实例。
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();
...
}
}
}
条件注入
有时候单一的类型无法满足对同一个依赖类型的分类。例如,一个完善的 Coffee Maker 应该能提供水温加热和热盘子加热两种方式。
在这种情况下,需要添加一个条件注解(qualifier annotaion)。这个注解本身有一个 @Qualifier
注解,例如这里创建一个 @Named
注解:
@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") 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);
}
}
执行编译,Java编译器将抛出如下错误:
[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.
解决这个错误的方法是在该 Component 的任意 module 中添加一个 @Provides 方法提供 Executor
的注入。当 @Inject、@Module 和 @Component分别校验通过后,Dagger 在确保整个依赖图谱完整之后,校验才算通过。
生成代码
Dagger在执行编译时,将生成辅助类,类中包含 CoffeeMaker$$Factory.java
、CoffeeMaker$$MembersInjector.java
等属性。这些属性在 Dagger 工作中会用到,虽然在注入的过程中通过单步调试可以查看到内容,但开发者不应该在代码中直接使用。
附录
参考资料
Dagger 2 GitHub: https://github.com/google/dagger
Dagger 1 GitHub: https://github.com/square/dagger
Dagger 2 文档: http://google.github.io/dagger