介绍
应用当中最好的类是那些实现的类,例如:BarcodeDecoder(条形码解析器),KoopaPhysicsEngine(引擎类),AudioStreamer(音频流),这些类都有其依赖的类,可能是BarcodeCameraFinder,DefaultPhysicsEngine或HttpStreamer。
相比较而言,最糟糕的是那些既占用空间却并不做多少事的类,例如:BarcodeDecoderFactory, CameraServiceLoader, MutableContextWrapper。这些类就像笨拙的胶带将这些有趣的实现类捆绑起来。
Dagger就是用来替代这些工厂类的。它让你专注于这些有趣的类。声明依赖关系,明确如何满足它们的需求,然后运行你的APP。
用标准的javax.inject注解进行构造,每个类都非常容易测试。你并不需要一大堆测试模板仅仅为了将RpcCreditCardService换成FakeCreditCardService。
依赖注入并不仅仅是为了测试。它也可以使创建可重用可替换的模块变得容易。你可以在全局分享同一个AuthenticationModule模块。然后在开发期间运行DevLoggingModule,在上线后运行ProdLoggingModule,在每种情况下都有相应的行为。
使用Dagger
我们将通过构建一个咖啡机来阐述依赖注入和dagger。
声明依赖
dagger会构造你应用里的类的实例来满足它们的依赖,它使用javax.inject.Inject注解来分辨哪些构造器和变量与之相关。
使用@Inject注册构造器,dagger会用它来创建类的实例。当新的实例被请求,dagger将会获取所需的参数值然后回调该构造器。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
dagger可以直接给变量注入。在这个例子中,它获取一个Heater实例给heater变量,获取一个Pump实例给pump变量。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
如果你的类有注解变量没有注解构造器,如果该类存在,dagger会使用无参构造器。缺少@Inject注解的类不能被dagger构造。
dagger并不支持injection方法。
满足依赖
默认情况下,dagger会想上门所描述的那样,创建所需类型的实例来满足每种依赖。当你请求一个CoffeeMaker,它会调用new CoffeeMaker()来获取该实例并将其设置到它的注解变量。
但@Inject并不是在所有地方都可用:
1 接口不能被构造
2 第三方类不能注解
3 配置对象必须已经配置好
在这些@Inject不能使用的尴尬情况下,可用使用@Provides注解方法来满足依赖,该方法的返回类型定义了应该满足哪种依赖。
例如无论何时一个Heater被请求,provideHeater()将会被调用:
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides方法允许有自己的依赖。当Pump被需要时,会返回Thermosiphon:
@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为后缀命名。
构造依赖对象图
@Inject和@Provides注解类形成了一张对象图表,由它们的依赖关系所连接。调用ObjectGraph.create()可以获取该图表,它接受一个或多个模块:
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
为了使用该图表,我们需要启动注入。这通常需要注入命令行应用的main类,或Android应用的activity类。以coffee为例,CoffeeApp类就被用来启动依赖注入。我们要求图表提供一个类的注入实例:
class CoffeeApp implements Runnable {
@Inject CoffeeMaker coffeeMaker;
@Override public void run() {
coffeeMaker.brew();
}
public static void main(String[] args) {
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
CoffeeApp coffeeApp = objectGraph.get(CoffeeApp.class);
...
}
}
唯一遗漏的是注入类CoffeeApp并不被图表所知,我们需要显式地在@Module中以一种注解类型注册。
@Module(
injects = CoffeeApp.class
)
class DripCoffeeModule {
...
}
该injects选项会让整个图表在编译时有效,越早发现问题会加速开发过程并且减少重构时的危险。现在图表已经构造完毕,根对象已经注解,我们可以运行咖啡机app了。
单例
用@Singleton注解一个 @Provides方法或可被注解的类,图表会为所有使用者提供一个单一实例。
@Provides @Singleton Heater provideHeater() {
return new ElectricHeater();
}
@Singleton 注解也可被看做一个参考,它提醒那些潜在的持有者这个类是可以被多个线程所共享的。
@Singleton
class CoffeeMaker {
...
}
懒注解
有时你需要迟一些实例化对象。对于任意类型绑定T,你可以创建Lazy<T>,它会延迟实例化直到第一次调用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();
}
}
}
提供者注解
有时你想返回多个实例而不是单一实例,你有多种选择( Factories, Builders等等 ), 其中一种是注入 Provider<T>而不仅仅是T,每次调用.get()都会创建一个新的T实例。
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>可能产生令人混淆的代码,可能在你的对象图中的设计存在范围上的缺失或者构造对象的缺失。通常你会使用Factory<T>或者Lazy<T>或者重新组织生命周期和代码结构仅仅为了能够注解一个T。然而注入Provider<T>在一些情况下会变成一个life saver(这个要自己理解了)。通常的用法是在当你使用一个遗留的架构时,它与你对象的自然生命周期不一致。(比如 servlets被设计成单例模式,但仅仅在指定请求数据的场景下有效)
限定者
有时一个类型无法满足标识一种依赖关系。比如一个成熟的咖啡机应用可能会想把加热器分成为水加热和为盘子加热两种加热器。这种情况下,我们需要添加一个限定注解,任何注解都有一个@Qualifier属性。这里是 包含在javax.inject中的限定注解 @Named属性的声明。
@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可以注解静态值,用@Inject注解的静态变量必须在模块注解里面以staticInjections列出来。
@Module(
staticInjections = LegacyCoffeeUtils.class
)
class LegacyModule {
}
用ObjectGraph.injectStatics()去填充这些静态变量
ObjectGraph objectGraph = ObjectGraph.create(new LegacyModule());
objectGraph.injectStatics();
注意:静态注解只会在即时图表中运行,如果你在使用plus()方法调用创建的图表中调用injectStatics(), 那么在继承图表的模块中静态注解不会执行。
编译时有效性
dagger包含一个注解处理器进行确认模块和注入的有效性。该处理器很严格,当任何绑定无效或不完整的时候都会引起编译错误。比如下面这个模块缺少Executor绑定。
@Module
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
编译时,javac拒绝了缺失的绑定:
[ERROR] COMPILATION ERROR :
[ERROR] error: No binding for java.util.concurrent.Executor
required by provideHeater(java.util.concurrent.Executor)
可以添加@Provides注解方法给Executor来修复这个问题, 也可以标记该模块并不完整。
@Module(complete = false)
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
在注入类列表提供未曾使用的类型的模块也将触发错误。
@Module(injects = Example.class)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
因为 Example注入到模块只用到了Heater,javac拒绝未使用的绑定:
[ERROR] COMPILATION ERROR:
[ERROR]: Graph validation failed: You have these unused @Provider methods:
1. coffee.DripCoffeeModule.provideChiller()
Set library=true in your module to disable this check.
如果你的模块绑定将会在注入列表外被使用,只需要标记该模块为一个库。
@Module(
injects = Example.class,
library = true
)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
为了得到最多运行时外的校验,只需要创建一个包含应用所有模块的模块。注解处理器将会检测所有模块的问题并报告。
@Module(
includes = {
DripCoffeeModule.class,
ExecutorModule.class
}
)
public class CoffeeAppModule {
}
当你把dagger的jar包包含进你的编译路径注解处理器会自动可用。
生成运行时代码
dagger的注解处理器也可能产生源文件,名称像CoffeeMaker$InjectAdapter.java或DripCoffeeModule$ModuleAdapter。这些文件是dagger实现的详情。你不需要直接使用它,虽然在通过注入逐步调试时它会非常方便。
模块重写
如果有多个@Provides方法竞争同一个依赖关系,dagger会抛出失败错误。但有时为了开发或测试对成品代码进行替换是有必要的。在模块注解中使用overrides = true可以让该模块比其他模块的绑定具有跟高的优先权。
下面JUnit测试使用来自mockito框架的mock对象重写DripCoffeeModule与Heater的绑定。该mock对象被注入到CoffeeMaker同时也被注入到测试:
public class CoffeeMakerTest {
@Inject CoffeeMaker coffeeMaker;
@Inject Heater heater;
@Before public void setUp() {
ObjectGraph.create(new TestModule()).inject(this);
}
@Module(
includes = DripCoffeeModule.class,
injects = CoffeeMakerTest.class,
overrides = true
)
static class TestModule {
@Provides @Singleton Heater provideHeater() {
return Mockito.mock(Heater.class);
}
}
@Test public void testHeaterIsTurnedOnAndThenOff() {
Mockito.when(heater.isHot()).thenReturn(true);
coffeeMaker.brew();
Mockito.verify(heater, Mockito.times(1)).on();
Mockito.verify(heater, Mockito.times(1)).off();
}
}
重写最适合应用中的小变化:
1 用一个单元测试的mock替换真正的实现。
2 开发中用假认证替换LDAP认证。
对于更多的潜在变化,通常使用更多的模块绑定会更简单。
应用当中最好的类是那些实现的类,例如:BarcodeDecoder(条形码解析器),KoopaPhysicsEngine(引擎类),AudioStreamer(音频流),这些类都有其依赖的类,可能是BarcodeCameraFinder,DefaultPhysicsEngine或HttpStreamer。
相比较而言,最糟糕的是那些既占用空间却并不做多少事的类,例如:BarcodeDecoderFactory, CameraServiceLoader, MutableContextWrapper。这些类就像笨拙的胶带将这些有趣的实现类捆绑起来。
Dagger就是用来替代这些工厂类的。它让你专注于这些有趣的类。声明依赖关系,明确如何满足它们的需求,然后运行你的APP。
用标准的javax.inject注解进行构造,每个类都非常容易测试。你并不需要一大堆测试模板仅仅为了将RpcCreditCardService换成FakeCreditCardService。
依赖注入并不仅仅是为了测试。它也可以使创建可重用可替换的模块变得容易。你可以在全局分享同一个AuthenticationModule模块。然后在开发期间运行DevLoggingModule,在上线后运行ProdLoggingModule,在每种情况下都有相应的行为。
使用Dagger
我们将通过构建一个咖啡机来阐述依赖注入和dagger。
声明依赖
dagger会构造你应用里的类的实例来满足它们的依赖,它使用javax.inject.Inject注解来分辨哪些构造器和变量与之相关。
使用@Inject注册构造器,dagger会用它来创建类的实例。当新的实例被请求,dagger将会获取所需的参数值然后回调该构造器。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
dagger可以直接给变量注入。在这个例子中,它获取一个Heater实例给heater变量,获取一个Pump实例给pump变量。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
如果你的类有注解变量没有注解构造器,如果该类存在,dagger会使用无参构造器。缺少@Inject注解的类不能被dagger构造。
dagger并不支持injection方法。
满足依赖
默认情况下,dagger会想上门所描述的那样,创建所需类型的实例来满足每种依赖。当你请求一个CoffeeMaker,它会调用new CoffeeMaker()来获取该实例并将其设置到它的注解变量。
但@Inject并不是在所有地方都可用:
1 接口不能被构造
2 第三方类不能注解
3 配置对象必须已经配置好
在这些@Inject不能使用的尴尬情况下,可用使用@Provides注解方法来满足依赖,该方法的返回类型定义了应该满足哪种依赖。
例如无论何时一个Heater被请求,provideHeater()将会被调用:
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides方法允许有自己的依赖。当Pump被需要时,会返回Thermosiphon:
@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为后缀命名。
构造依赖对象图
@Inject和@Provides注解类形成了一张对象图表,由它们的依赖关系所连接。调用ObjectGraph.create()可以获取该图表,它接受一个或多个模块:
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
为了使用该图表,我们需要启动注入。这通常需要注入命令行应用的main类,或Android应用的activity类。以coffee为例,CoffeeApp类就被用来启动依赖注入。我们要求图表提供一个类的注入实例:
class CoffeeApp implements Runnable {
@Inject CoffeeMaker coffeeMaker;
@Override public void run() {
coffeeMaker.brew();
}
public static void main(String[] args) {
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
CoffeeApp coffeeApp = objectGraph.get(CoffeeApp.class);
...
}
}
唯一遗漏的是注入类CoffeeApp并不被图表所知,我们需要显式地在@Module中以一种注解类型注册。
@Module(
injects = CoffeeApp.class
)
class DripCoffeeModule {
...
}
该injects选项会让整个图表在编译时有效,越早发现问题会加速开发过程并且减少重构时的危险。现在图表已经构造完毕,根对象已经注解,我们可以运行咖啡机app了。
单例
用@Singleton注解一个 @Provides方法或可被注解的类,图表会为所有使用者提供一个单一实例。
@Provides @Singleton Heater provideHeater() {
return new ElectricHeater();
}
@Singleton 注解也可被看做一个参考,它提醒那些潜在的持有者这个类是可以被多个线程所共享的。
@Singleton
class CoffeeMaker {
...
}
懒注解
有时你需要迟一些实例化对象。对于任意类型绑定T,你可以创建Lazy<T>,它会延迟实例化直到第一次调用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();
}
}
}
提供者注解
有时你想返回多个实例而不是单一实例,你有多种选择( Factories, Builders等等 ), 其中一种是注入 Provider<T>而不仅仅是T,每次调用.get()都会创建一个新的T实例。
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>可能产生令人混淆的代码,可能在你的对象图中的设计存在范围上的缺失或者构造对象的缺失。通常你会使用Factory<T>或者Lazy<T>或者重新组织生命周期和代码结构仅仅为了能够注解一个T。然而注入Provider<T>在一些情况下会变成一个life saver(这个要自己理解了)。通常的用法是在当你使用一个遗留的架构时,它与你对象的自然生命周期不一致。(比如 servlets被设计成单例模式,但仅仅在指定请求数据的场景下有效)
限定者
有时一个类型无法满足标识一种依赖关系。比如一个成熟的咖啡机应用可能会想把加热器分成为水加热和为盘子加热两种加热器。这种情况下,我们需要添加一个限定注解,任何注解都有一个@Qualifier属性。这里是 包含在javax.inject中的限定注解 @Named属性的声明。
@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可以注解静态值,用@Inject注解的静态变量必须在模块注解里面以staticInjections列出来。
@Module(
staticInjections = LegacyCoffeeUtils.class
)
class LegacyModule {
}
用ObjectGraph.injectStatics()去填充这些静态变量
ObjectGraph objectGraph = ObjectGraph.create(new LegacyModule());
objectGraph.injectStatics();
注意:静态注解只会在即时图表中运行,如果你在使用plus()方法调用创建的图表中调用injectStatics(), 那么在继承图表的模块中静态注解不会执行。
编译时有效性
dagger包含一个注解处理器进行确认模块和注入的有效性。该处理器很严格,当任何绑定无效或不完整的时候都会引起编译错误。比如下面这个模块缺少Executor绑定。
@Module
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
编译时,javac拒绝了缺失的绑定:
[ERROR] COMPILATION ERROR :
[ERROR] error: No binding for java.util.concurrent.Executor
required by provideHeater(java.util.concurrent.Executor)
可以添加@Provides注解方法给Executor来修复这个问题, 也可以标记该模块并不完整。
@Module(complete = false)
class DripCoffeeModule {
@Provides Heater provideHeater(Executor executor) {
return new CpuHeater(executor);
}
}
在注入类列表提供未曾使用的类型的模块也将触发错误。
@Module(injects = Example.class)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
因为 Example注入到模块只用到了Heater,javac拒绝未使用的绑定:
[ERROR] COMPILATION ERROR:
[ERROR]: Graph validation failed: You have these unused @Provider methods:
1. coffee.DripCoffeeModule.provideChiller()
Set library=true in your module to disable this check.
如果你的模块绑定将会在注入列表外被使用,只需要标记该模块为一个库。
@Module(
injects = Example.class,
library = true
)
class DripCoffeeModule {
@Provides Heater provideHeater() {
return new ElectricHeater();
}
@Provides Chiller provideChiller() {
return new ElectricChiller();
}
}
为了得到最多运行时外的校验,只需要创建一个包含应用所有模块的模块。注解处理器将会检测所有模块的问题并报告。
@Module(
includes = {
DripCoffeeModule.class,
ExecutorModule.class
}
)
public class CoffeeAppModule {
}
当你把dagger的jar包包含进你的编译路径注解处理器会自动可用。
生成运行时代码
dagger的注解处理器也可能产生源文件,名称像CoffeeMaker$InjectAdapter.java或DripCoffeeModule$ModuleAdapter。这些文件是dagger实现的详情。你不需要直接使用它,虽然在通过注入逐步调试时它会非常方便。
模块重写
如果有多个@Provides方法竞争同一个依赖关系,dagger会抛出失败错误。但有时为了开发或测试对成品代码进行替换是有必要的。在模块注解中使用overrides = true可以让该模块比其他模块的绑定具有跟高的优先权。
下面JUnit测试使用来自mockito框架的mock对象重写DripCoffeeModule与Heater的绑定。该mock对象被注入到CoffeeMaker同时也被注入到测试:
public class CoffeeMakerTest {
@Inject CoffeeMaker coffeeMaker;
@Inject Heater heater;
@Before public void setUp() {
ObjectGraph.create(new TestModule()).inject(this);
}
@Module(
includes = DripCoffeeModule.class,
injects = CoffeeMakerTest.class,
overrides = true
)
static class TestModule {
@Provides @Singleton Heater provideHeater() {
return Mockito.mock(Heater.class);
}
}
@Test public void testHeaterIsTurnedOnAndThenOff() {
Mockito.when(heater.isHot()).thenReturn(true);
coffeeMaker.brew();
Mockito.verify(heater, Mockito.times(1)).on();
Mockito.verify(heater, Mockito.times(1)).off();
}
}
重写最适合应用中的小变化:
1 用一个单元测试的mock替换真正的实现。
2 开发中用假认证替换LDAP认证。
对于更多的潜在变化,通常使用更多的模块绑定会更简单。