Guice是一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(IOC)。Guice非常小而且快。Guice是类型安全的,它能够 对构造函数,属性,方法(包含任意个参数的任意方法,而不仅仅是setter方法)进行注入。Guice还具有一些可选的特性比如:自定义scopes, 传递依赖,静态属性注入,与Spring集成和AOP联盟方法注入等。
Java企业应用开发社区在连接对象方面花了很大功夫。你的Web应用如何访问中间层服务?你的服务如何连接到登录用户和事务管 理器?关于这个问题你会发现很多通用的和特定的解决方案。有一些方案依赖于模式,另一些则使用框架。所有这些方案都会不同程度地引入一些难于测试或者程式 化代码重复的问题。你马上就会看到,Guice 在这方面是全世界做得最好的:非常容易进行单元测试,最大程度的灵活性和可维护性,以及最少的代码重复。
我们使用一个假想的、简单的例子来展示 Guice 优于其他一些你可能已经熟悉的经典方法的地方。下面的例子过于简单,尽管它展示了许多显而易见的优点,但其实它还远没有发挥出 Guice 的全部潜能。我们希望,随着你的应用开发的深入,Guice 的优越性也会更多地展现出来。
在这个例子中,一个客户对象依赖于一个服务接口。该服务接口可以提供任何服务,我们把它称为Service。
public interface Service {
void go();
}
对于这个服务接口,我们有一个缺省的实现,但客户对象不应该直接依赖于这个缺省实现。如果我们将来打算使用一个不同的服务实现,我们不希望回过头来修改所有的客户代码。
public class ServiceImpl implements Service {
public void go() {
...
}
}
我们还有一个可用于单元测试的伪服务对象。
public class MockService implements Service {
private boolean gone = false;
public void go() {
gone = true;
}
public boolean isGone() {
return gone;
}
}
简单工厂模式
在发现依赖注入之前,最常用的是工厂模式。除了服务接口之外,你还有一个既可以向客户提供服务对象,也可以向测试程序
传递
伪服务对象的工厂
类。在这里我们会将服务实现为一个单件
对象,以便让示例尽量简化。
public class ServiceFactory {
private ServiceFactory() {}
private static Service instance = new ServiceImpl();
public static Service getInstance() {
return instance;
}
public static void setInstance(Service service) {
instance = service;
}
}
客户程序每次需要服务对象时就直接从工厂获取。
public class Client {
public void go() {
Service service = ServiceFactory.getInstance();
service.go();
}
}
客户程序足够简单。但客户程序的单元测试代码必须将一个伪服务对象传入工厂,同时要记得在测试后清理。在我们这个简单的例子里,这不算什么难事儿。但当你增加了越来越多的客户和服务代码后,所有这些伪代码和清理代码会让单元测试的开发一团糟。此外,如果你忘记在测试后清理,其他测试可能会得到与预期不符的结果。更糟的是,测试的成功与失败可能取决于他们被执行的顺序。
public void testClient() {
Service previous = ServiceFactory.getInstance();
try {
final MockService mock = new MockService();
ServiceFactory.setInstance(mock);
Client client = new Client();
client.go();
assertTrue(mock.isGone());
}
finally {
ServiceFactory.setInstance(previous);
}
}
最后,注意服务工厂的API把我们限制在了单件这一种应用模式上。即便 getInstance() 可以返回多个实例, setInstance() 也会束缚我们的手脚。转换到非单件模式也意味着转换到了一套更复杂的API。
手工依赖注入
依赖注入模式的目标之一是使单元测试更简单。我们不需要特殊的框架就可以实践依赖注入模式。依靠手工编写代码,你可以得到该模式大约80%的好处。
当上例中的客户代码向工厂对象请求一个服务时,根据依赖注入模式,客户代码希望它所依赖的对象实例可以被传入自己。也就是说:不要调用我,我会调用你。
public class Client {
private final Service service;
public Client(Service service) {
this.service = service;
}
public void go() {
service.go();
}
}
这让我们的单元测试简化了不少。我们可以只传入一个
伪服务对象,在结束后也不需要多做什么。
public void testClient() {
MockService mock = new MockService();
Client client = new Client(mock);
client.go();
assertTrue(mock.isGone());
}
我们也可以精确地
区分出客户代码依赖
的API。
现在,我们如何连接
客户
和服务对象呢?手工实现依赖注入的时候,我们可以将所有依赖逻辑都移动到工厂类中。也就是说,我们还需要有一个工厂类来创建客户对象。
public static class ClientFactory {
private ClientFactory() {}
public static Client getInstance() {
Service service = ServiceFactory.getInstance();
return new Client(service);
}
}
手工实现依赖注入需要的代码行数和简单工厂模式差不多。
用 Guice 实现依赖注入
手工为每一个服务与客户实现工厂类和依赖注入逻辑是一件很麻烦的事情。其他一些依赖注入框架甚至需要你显式将服务映射到每一个需要注入的地方。
Guice 希望在不牺牲可维护性的情况下去除所有这些程式化的代码。
使用 Guice,你只需要实现模块类。Guice 将一个绑定器传入你的模块,你的模块使用绑定器来连接接口和实现。以下模块代码告诉 Guice 将
Service 映射到单件模式的
ServiceImpl:
public class MyModule implements Module {
public void configure(Binder binder) {
binder.bind(Service.class)
.to(ServiceImpl.class)
.in(Scopes.SINGLETON);
}
}
模块类告诉 Guice 我们想注入什么东西。那么,我们该如何告诉 Guice 我们想
把它注入到哪里呢?使用 Guice,你可以使用
@Inject 标注你的构造器,方法或字段:
public class Client {
private final Service service;
@Inject
public Client(Service service) {
this.service = service;
}
public void go() {
service.go();
}
}
@Inject 标注
可以
清楚地告诉其他程序员
你的类中哪些
成员是被注入
的。
为了让 Guice 向
Client 中注入,我们必须直接让 Guice 帮我们创建
Client 的实例,或者,其他类必须包含被注入的
Client 实例。
Guice vs. 手工依赖注入
如你所见,Guice 省去了写工厂类的麻烦。你不需要编写
代码将客户连接到它们所依赖的对象。如果你忘了提供一个依赖关系,Guice 在启动时就会失败。Guice 也会自动处理循环依赖关系。
Guice 允许你通过声明指定对象的作用域。例如,你
需要
编写
相
同的代码将对象
反复存入
HttpSession。
实际情况通常是,
只有到了运行时,你
才能知道具体要使用哪一个实现类。
因此你需要元工厂类或服务定位器来增强你的工厂模式。Guice 用最少的代价解决了这些问题。
手工实现依赖注入时,你很容易退回到使用直接依赖的旧习惯,特别是当你对依赖注入的概念还不那么熟悉的时候。使用 Guice 可以避免这种问题,可以让你更容易地把事情做对。Guice 使你保持正确的方向。
更多的标注
只要有可能,Guice 就允许你使用标注来
替代显式地绑定对象,
以减少
更多的程式化代码。回到我们的例子,如果你需要一个接口来简化单元测试,
而你又不介意编译时的依赖,你可以直接从你的接口指向一个缺省的实现。
@ImplementedBy(ServiceImpl.class)
public interface Service {
void go();
}
这时,如果客户需要一个
Service 对象,且 Guice 无法找到显式绑定,Guice 就会注入一个
ServiceImpl 的实例。
缺省情况下,Guice 每次都注入一个新的实例。如果你想指定不同的作用域规则,你也可以对实现类进行标注。
@Singleton
public class ServiceImpl implements Service {
public void go() {
...
}
}
架构概览
我们可以将 Guice 的架构分成两个不同的阶段:启动和运行。你在启动时创建一个注入器
Injector,在运行时用它来注入对象。
启动
你通过实现
Module 来配置 Guice。你传给 Guice 一个模块对象,Guice 则将一个绑定器
Binder 对象传入你的模块,然后,你的模块使用绑定器来配置绑定。一个绑定通常包含一个从接口到具体实现的映射。例如:
public class MyModule implements Module {
public void configure(Binder binder) {
// Bind Foo to FooImpl. Guice will create a new
// instance of FooImpl for every injection.
binder.bind(Foo.class)
.to(FooImpl.class);
// Bind Bar to an instance of Bar.
Bar bar = new Bar();
binder.bind(Bar.class).toInstance(bar);
}
}
在这个阶段,Guice 会察看你告诉它的所有类,以及
任何与这些类有关系的类,然后通知你是否有依赖关系的缺失。例如,在一个 Struts 2 应用中,Guice 知道你所有的动作类。Guice 会检查你的动作类以及它们依赖的所有类,如果有问题会及早报错。
创建一个
Injector 涉及以下步骤:
- 首先创建你的模块类实例,并将其传入 Guice.createInjector().
- Guice 创建一个绑定器 Binder 并将其传入你的模块。
- 你的模块使用绑定器来定义绑定。
- 基于你所定义的绑定,Guice 创建一个注入器 Injector 并将其返回给你。
- 你使用注入器来注入对象。
运行
现在你可以使用第一阶段创建的注入器来注入对象并内省(introspect)我们的绑定了。Guice 的运行时模型
由一个可管理一定数量绑定的注入器
组成。
键 Key 唯一地
确定每一个绑定。
Key 包含了客户代码所依赖的类型以及一个可选的标注。你可以使用标注来区分指向同一类型的
多个绑定。
Key 的类型和标注对应于注入时的类型和标注。
每个绑定有一个提供者 provider,它提供所需类型的实例。你可以提供一个类,Guice 会帮你创建它的实例。你也可以给 Guice 一个你要绑定的
类的实例。你还可以实现你自己的 provider,Guice 可以向其中注入依赖关系。
每个绑定还有一个可选的作用域。缺省情况下绑定没有作用域,Guice 为每一次注入创建一个新的对象。一个定制的作用域可以使你控制 Guice
是否创建新对象。例如,你可以为每一个
HttpSession 创建一个实例。