Dagger 2
01 Jul 2015Dagger2是Dagger1的分支,由谷歌公司接手开发,是针对android和java的一个依赖注入框架。
最理想的类应该是这样的: BarcodeDecoder
, KoopaPhysicsEngine
, AudioStreamer
。这些类依赖其它模块,可能是BarcodeCameraFinder
,DefaultPhysicsEngine
,或HttpStreamer
。
作为对比,不好的类是那些占用空间却没有做多少事情的:BarcodeDecoderFactory
,CameraServiceLoader
和MutableContextWrapper
。 这些类只是很笨拙地起到连接作用而已。
Dagger就是这些工厂类的替代品,它使用dependency injection 设计模式, 你不再需要写这些模板。它让你可以专注于功能开发。声明依赖关系,指定需要什么,就可以了。
基于标准javax.inject注解(JSR 330),每个类都易于测试。你不用仅仅为了交换RpcCreditCardService
和FakeCreditCardService
而写一大堆模板。
依赖注入不只是用于测试。它也可以方便地用来创建可重用、可互换的模块。你可以在各模块间共用同一个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注解。 被注解的方法的返回值表示它可以满足哪些依赖。
例如,无论何时需要一个Heater
,provideHeater()
都会被调用:
@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
你可以创建一个LazyLazy<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等等),有一个方式是注入ProviderT
。Provider<T>
在每次.get()
调用的时候调用T
的binding 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
你会想要使用factory或Lazy<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.java
或CoffeeMaker$$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.