自举(Bootstrapping)你的应用
自举(
bootstrapping)对于依赖注入非常重要。总是显式地向
Injector 索要依赖,这就将 Guice 用作了服务定位器,而不是一个依赖注入框架。
你的代码应该尽量少地和
Injector 直接打交道。相反,你应该通过注入一个根对象来自举你的应用。容器可以更进一步地将依赖注入根对象所依赖的对象,并如此迭代下去。最终,在理想情况下,你的应用中应该只有一个类知道
Injector,每个其他类都应该使用
注入的依赖关系。
例如,一个诸如 Struts 2 的 Web 应用框架通过注入你的所有动作类来自举你的应用。你可以通过注入你的服务实现类来自举一个 Web 服务框架。
依赖注入是传染性的。如果你重构一个有大量静态方法的已有代码,你可能会觉得你正在试图拉扯一根没有尽头的线。这是好事情。它表明依赖注入正在帮助你改进代码的灵活性和可测试性。
如果重构工作太复杂,你不想一次性地整理完所有代码,你可以
暂时将一个
Injector 的引用存入某个类的一个静态的字段,或是使用静态注入。这时,请清楚地命名包含该字段的类:比如
Injector
Hack 和
GodKillsAKittenEveryTimeYouUseMe。记住你将来可能不得不为这些类提供
伪测试类,你的单元测试则不得不手工安装一个注入器。记住,你将来需要清理这些代码。
绑定依赖关系
Guice 是如何知道要注入什么东西的呢?对启动器来说,一个包含了类型和可选的标注的 Key 唯一地指明了一个依赖关系。Guice 将 key 和实现之间的映射
标记为一个
Binding。一个实现可以包含一个单独的对象,一个需要由 Guice 注入的类,或一个定制的 provider。
当注入依赖关系时,Guice 首先寻找显式绑定,即你通过绑定器 Binder 指明的绑定。Binder API 使用生成器(Builder)模式来创建一种领域相关的描述语言。根据约束适用方法的上下文的不同,不同方法返回不同的对象。
例如,为了将接口 Service 绑定到一个具体的实现 ServiceImpl,调用:
binder.bind(Service.class).to(ServiceImpl.class);
该绑定与下面的方法匹配:
@Inject
void injectService(Service service) {
...
}
注: 与某些其他的框架相反,Guice 并没有给 "setter" 方法任何特殊待遇。不管方法有几个参数,只要该方法含有
@Inject 标注,Guice 就会实施注入,甚至对基类中实现的方法也不例外。
不要重复自己
对每个绑定不断地重复调用 "
binder" 似乎有些乏味。Guice 提供了一个支持
Module 的类,名为 AbstractModule,它隐含地赋予你访问
Binder 的方法的权力。例如,我们可以用扩展
AbstractModule 类的方式改写上述绑定:
bind(Service.class)
.to(ServiceImpl.class);
在本手册的余下部分中我们会一直使用这样的语法。
标注绑定
如果你需要指向同一类型的多个绑定,你可以用标注来区分这些绑定。例如,将接口
Service 和标注
@Blue 绑定到具体的实现类
BlueService 的代码如下:
bind(Service.class)
.annotatedWith(Blue.class)
.to(BlueService.class);
这个绑定会匹配以下方法:
@Inject
void injectService(
@Blue Service service) {
...
}
注意,标注
@Inject 出现在方法前,而绑定标注(如
@Blue)则出现在参数前。对构造
函数也是如此。使用字段注入时,两种标注都直接应用于字段,如以下代码:
@Inject @Blue Service service;
创建绑定标注
刚才提到的标注
@Blue 是从哪里来的?你可以很容易地创建这种标注,但不幸的是,你必须使用略显复杂的标准语法:
/**
* Indicates we want the blue version of a binding.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface Blue {}
幸运的是,我们不需要理解这些代码,只要会用就可以了。对于好奇心强的朋友,下面是这些程式化代码的含义:
- @Retention(RUNTIME) 使得你的标注在运行时可见。
- @Target({FIELD, PARAMETER}) 是对用户使用的说明;它不允许 @Blue 被用于方法、类型、局部变量和其他标注。
- @BindingAnnotation 是 Guice 特定的信号,表示你希望该标注被用于绑定标注。当用户将多于一个的绑定标注应用于同一个可注入元素时,Guice 会报错。
有属性的标注
如果你已经会写有属性的标注了,请跳到下一节。
你也可以绑定到标注实例,即,你可以有多个绑定指向同样的类型和标注类型,但每个绑定拥有不同的标注属性值。如果 Guice 找不到拥有特定属性值的标注实例,它会去找一个绑定到该标注类型的绑定。
例如,我们有一个绑定标注
@Named,它有一个字符
串
属性值。
@Retention(RUNTIME)
@Target({ FIELD, PARAMETER })
@BindingAnnotation
public @interface Named {
String value();
}
如果我们希望绑定到
@Named("Bob"),我们首先需要一个
Named 的实现。我们的实现必须遵守关于
Annotation 的约定,特别是
hashCode() 和
equals() 的实现。
class NamedAnnotation implements Named {
final String value;
public NamedAnnotation(String value) {
this.value = value;
}
public String value() {
return this.value;
}
public int hashCode() {
// This is specified in java.lang.Annotation.
return 127 * "value".hashCode() ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Named))
return false;
Named other = (Named) o;
return value.equals(other.value());
}
public String toString() {
return "@" + Named.class.getName() + "(value=" + value + ")";
}
public Class<? extends Annotation> annotationType() {
return Named.class;
}
}
现在我们可以使用这个标注实现来创建一个指向
@Named 的绑定。
bind(Person.class)
.annotatedWith(new NamedAnnotation("Bob"))
.to(Bob.class);
与其它框架使用基于字符串的标识符相比,这显得有些繁琐,但记住,使用基于字符串的标识符,你根本无法这样做。而且,你会发现你可以大量复用已有的绑定标注。
因为通过名字标记一个绑定非常普遍,以至于 Guice 在
com.google.inject.name 中提供了一个十分有用的
@Named 的实现。
隐式绑定
正如我们在简介中看到的那样,你并不总
是需要显式
地声明
绑定。如果缺少显式绑定,Guice 会试图注入并创建一个你所依赖的类的新实例。如果你依赖于一个
接口,Guice 会寻找一个指向具体实现的
@ImplementedBy 标注。例如,下例中的代码显式绑定到一个具体的、可注入的名为
Concrete 的类。它的含义是,将
Concrete 绑定到
Concrete。这是显式的声明方式,但也有些冗余。
bind(Concrete.class);
删除上述绑定语句不会影响下面这个类的行为:
class Mixer {
@Inject
Mixer(Concrete concrete) {
...
}
}
好吧,你自己来选择:显式的或简略的。无论何种方式,Guice 在遇到错误时都会生成有用的信息。
注入提供者
有时对于每次注入,客户代码需要
某个依赖的多个实例。其它时候,客户可能不想在一开始就真地获取对象,而是等到注入后的某个时候再获取。对于任意绑定类型
T,你可以不直接注入
T 的实例,而是注入一个
Provider<T>,然后在需要的时候调用
Provider<T>
.get(),例如:
@Inject
void injectAtm(Provider<Money> atm) {
Money one = atm.get();
Money two = atm.get();
...
}
正如你所看到的那样, Provider 接口简单得不能再简单了,它不会为简单的单元测试添加任何麻烦。
注入常数值
对于常数值,Guice 对以下几种类型做了特殊处理:
- 基本类型(int, char, ...)
- 基本封装类型(Integer, Character, ...)
- Strings
- Enums
- Classes
首先,当绑定到这些类型的常数值的时候,你不需要指定你要绑定到的类型。Guice 可以根据值判断类型。例如,一个绑定标注名为
TheAnswer:
bindConstant().annotatedWith(TheAnswer.class)
.to(42);
它的效果等价于:
bind(int.class).annotatedWith(TheAnswer.class).toInstance(42);
当需要注入这些类型的数值时,如果 Guice 找不到指向基本数据类型的显式绑定,它会找一个指向相应的
封装类型的绑定,反之亦然。
转换字符串
如果 Guice 仍然无法找到一个上述类型的显式绑定,它会去找一个拥有相同绑定标
注的常量
String 绑定,并试图将字符串转换到相应的值。例如:
bindConstant().annotatedWith(TheAnswer.class)
.to("42"); // String!
会匹配:
@Inject @TheAnswer int answer;
转换时,Guice 会用名字去查找枚举和类。Guice 在启动时转换一次,这意味着它提前做了类型检查。这个特性特别有用,例如,当绑定值来自一个属性文件的时候。
定制的提供者
有时你需要手工创建你自己的对象,而不是让 Guice 创建它们。例如,你可能不能为来自第三方的实现类添加
@Inject 标注。在这种情况下,你可以实现一个定制的
Provider。Guice 甚至可以注入你的提供者类。例如:
class WidgetProvider implements Provider<Widget> {
final Service service;
@Inject
WidgetProvider(Service service) {
this.service = service;
}
public Widget get() {
return new Widget(service);
}
}
你可以
像这样把
Widget 绑定到
WidgetProvider:
bind(Widget.class).toProvider(WidgetProvider.class);
注入定制的提供者可以使 Guice 提前检查类型和依赖关系。定制的提供者可以在任意作用域中使用,而不依赖于他们所创建的类的作用域。缺省情况下,Guice 为每一次注入创建一个新的提供者实例。在上例中,如果每个
Widget 需要它自己的
Service 实例,我们的代码也没有问题。通过在工厂类上使用作用域标注,或为工厂类创建单独的绑定,你可以为定制的工厂指定不同的作用域。
示例:与 JNDI 集成
例如我们需要绑定从 JNDI 得到的对象。我们可以仿照下面的代码实现一个可复用的定制的提供者。注意我们注入了 JNDI
Context:
package mypackage;
import com.google.inject.*;
import javax.naming.*;
class JndiProvider<T> implements Provider<T> {
@Inject Context context;
final String name;
final Class<T> type;
JndiProvider(Class<T> type, String name) {
this.name = name;
this.type = type;
}
public T get() {
try {
return type.cast(context.lookup(name));
}
catch (NamingException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a JNDI provider for the given
* type and name.
*/
static <T> Provider<T> fromJndi(
Class<T> type, String name) {
return new JndiProvider<T>(type, name);
}
}
感谢
泛型擦除(generic type erasure)技术。我们必须在运行时将依赖传入类中。你可以省略这一步,但在今后跟踪类型转换错误会比较棘手(当 JNDI 返回错误类型
的对象的时候)。
我们可以使用定制的
JndiProvider 来将
DataSource 绑定到来自 JNDI 的一个对象:
import com.google.inject.*;
import static mypackage.JndiProvider.fromJndi;
import javax.naming.*;
import javax.sql.DataSource;
...
// Bind Context to the default InitialContext.
bind(Context.class).to(InitialContext.class);
// Bind to DataSource from JNDI.
bind(DataSource.class)
.toProvider(fromJndi(DataSource.class, "..."));
限制绑定的作用域
缺省情况下,Guice 为每次注入创建一个新的对象。我们把它称为“无作用域”。你可以在配制绑定时指明作用域。例如,每次注入相同的实例:
bind(MySingleton.class).in(Scopes.SINGLETON);
另一种做法是,你可以在实现类中使用标注来指明作用域。Guice 缺省支持
@Singleton:
@Singleton
class MySingleton {
...
}
使用标注的方法对于隐式绑定也同样有效,但需要 Guice 来创建你的对象。另一方面,调用
in() 适用于几乎所有绑定类型(显然,绑定到一个单独的实例是个例外)并且会忽略已有的作用域标注。如果你不希望引入对于作用域实现的编译时依赖,
in() 还可以接受标注。
可以使用 Binder.bindScope() 为定制的作用域指定标注。例如,对于标注 @SessionScoped 和一个 Scope 的实现 ServletScopes.SESSION:
binder.bindScope(SessionScoped.class, ServletScopes.SESSION);
创建作用域标注
用于指定作用域的标注必须:
- 有一个 @Retention(RUNTIME) 标注,从而使我们可以在运行时看到该标注。
- 有一个 @Target({TYPE}) 标注。作用域标注只用于实现类。
- 有一个 @ScopeAnnotation 元标注。一个类只能使用一个此类标注。
例如:
/**
* Scopes bindings to the current transaction.
*/
@Retention(RUNTIME)
@Target({TYPE})
@ScopeAnnotation
public @interface TransactionScoped {}
尽早加载绑定
Guice 可以等到你实际使用对象时再加载单件对象。这有助于开发,因为你的应用程序可以快速启动,只初始化你需要的对象。但是,有时你总是希望在启动时加载一个对象。你可以告诉 Guice,让它总是尽早加载一个单件对象,例如:
bind(StartupTask.class).asEagerSingleton();
我们经常在我们的
应用程序中使用这个方法实现初始化逻辑。你可以通过在 Guice 必须首先初始化的单件
对象上创建依赖关系来控制初始化顺序。