你可能想知道为什么会有两种不同的IoC类型,为什么这些类型又会进一步分解为不同的实现。这好像没有一个清晰的答案;的确,这些不同的类型提供灵活的弹性,但是对于我们,IoC看起来更象是新旧概念的一个混合体;这两种不同类型的IoC说明了这个问题。
依赖查找是一种更加传统的方法,第一眼看上去,Java程序员们对它都很熟悉。依赖注入相对新一些,方法还没有完全定型,虽然第一眼看上去有些反常,事实上它比依赖查找更加富有弹性且有用。
在依赖查找风格的IoC中,一个组件必须获得一个依赖的参考,反观在依赖注入风格中,依赖关系由IoC容器从字面意义上注入到组件中。依赖查找有两种类型:依赖托拽和上下文配置依赖查找。依赖注入同样也有两种常见的风格:构造器依赖注入和Setter依赖注入。
注意:在本章讨论中,我们并不关心虚构的IoC容器如何得到所有不同的依赖,只是在某些方面,为了描述每种机制来展现具体行为过程。
依赖托拽
对于一个Java开发者,依赖托拽是最熟悉的一种IoC。在依赖托拽中,依赖关系是根据需要从一个登记处获取(托拽)下来。任何一个写过访问EJB代码的程序员都使用过依赖托拽。Spring提供了依赖托拽作为从框架管理的组件中重新获取组件的一种机制;你可以在第2章中看到这种实践。Listing4-1展现了一个基于Spring的应用程序中典型的依赖托拽。
Listing 4-1. Spring中的依赖托拽
public static void main(String[] args) throws Exception {
// get the bean factory
BeanFactory factory = getBeanFactory();
MessageRenderer mr = (MessageRenderer) factory.getBean("renderer");
mr.render();
}
这种类型的IoC不仅流行于J2EE应用中,在从注册处获得依赖关系的JNDI查找(JNDI lookups)中也得到广泛应用,它在使用Spring的许多环境中也起着关键的作用。
上下文配置依赖查找
上下文配置依赖查找(CDL)也类似,一些地方与依赖托拽相仿。但是在CDL中,查找是在容器管理的资源中进行的,而不是从一个集中的注册处,同时它一般在规定点执行。CDL通过类
似Listing 4-2中的形式来工作,组件实现一个特定的接口。
Listing 4-2. 提供CDL的组件接口
package com.apress.prospring.ch4;
public interface ManagedComponent {
public void performLookup(Container container);
}
通过实现这个接口,组件通知容器它需要获取依赖关系。当容器准备好传递依赖到组件中去,它会依次调用各个组件的performLookup()方法。组件这个时候就能够通过容器的接口来查找依赖关系,就像Listing 4-3中的例子。
Listing 4-3. 在CDL中获取依赖关系
package com.apress.prospring.ch4;
public class ContextualizedDependencyLookup implements ManagedComponent {
private Dependency dep;
public void performLookup(Container container) {
this.dep = (Dependency) container.getDependency("myDependency");
}
}
构造器依赖注入
构造器依赖注入就是在组件的构造器处提供依赖关系的注入。这种组件声明一个构造器或者一组构造器从构造参数中获取依赖关系,IoC容器会在实例化它的时候将依赖关系传送给它,例如Listing 4-4的形式。
Listing 4-4. 构造器依赖注入
package com.apress.prospring.ch4;
public class ConstructorInjection {
private Dependency dep;
public ConstructorInjection(Dependency dep) {
this.dep = dep;
}
}
Setter依赖注入
在Setter依赖注入中,IoC容器通过JavaBean形式的方法将组件的依赖关系注入到组件中。组件的Setter方法将一组依赖关系暴露给IoC容器,并受之控制。Listing 4-5展示了一个典型的基于Setter依赖注入的组件。
Listing 4-5. Setter依赖注入
package com.apress.prospring.ch4;
public class SetterInjection {
private Dependency dep;
public void setMyDependency(Dependency dep) {
this.dep = dep;
}
}
在容器中,依赖需要通过setMyDependency()方法来暴露出来,它是myDependency遵循JavaBeans风格命名的方法。实际上,setter注入是最广泛使用的注入机制,它也是最易于实现的IoC机制中之一。
注入 对 查找
在IoC中选择应用哪种风格,是“注入”还是“查找”,一般来说这并不难抉择。大部分情况下,IoC的类型一般委由你所使用的容器来决定。例如,如果你在使用EJB2.0,那么你就必须使用J2EE容器提供的“查找”风格的IoC。在Spring中,除了初始化时的bean查找,你的组件和它们之间的依赖关系都可以通过“注入”风格的IoC来连接到一起。
注意:当你使用Spring时,你可以在访问EJB资源时不必进行明确的查找操作。Spring可以在查找风格的IoC与注入风格的IoC系统之间扮演一个适配器的角色,这样你就可以完全通过注入方式来管理所有资源了。
问题在于:如果非要做个选择,你会是用哪种方式,注入还是查找?大部分情况下当然会选择注入。如果你看看Listing 4-4与4-5的代码,可以清楚地看到应用注入对你的代码没有任何影响。反观依赖托拽的代码,它必须实际包含一个对注册处(registry)的引用并且要与之交互来获取依赖关系,使用上下文配置依赖查找(CDL),需要你的类实现一个特定的接口然后手动查找所有的依赖关系。如果你使用注入,你的类所需要做的就是允许依赖通过构造器或者setters注入。
使用依赖,你可以自由的使用完全与IoC容器解耦的类,可以手动设定各个独立对象之间的协作关系。反之,在查找的方式下,你的类总是依赖于容器定义的特定类和接口。查找的另一个缺点是很难脱离容器来测试你的代码。使用注入,测试你的组件将变得非常轻松,因为你只需要通过适当的构造器和setter来设定它们之间的依赖关系。
注意:关于通过Spring和依赖注入来测试的详细讨论包括在附录A中。
基于查找的解决方案要比基于注入的方案更复杂。虽然我们并不害怕复杂性,但是添加大量无用的复杂机制到你的程序中并以此作为依赖关系管理的核心工作,我们认为这样是错误的。
暂且不论这些缺点,最大的问题是选择注入来代替查找将会使你的生活更加轻松。如果使用注入你可以节省大量的代码,并且你写的代码将会非常简单,甚至,一般来说这些代码可以通过IDE自动生成。你会注意到所有的演示注入的代码都是被动的,这样它们就不需要主动尝试完成任务;你会发现在注入的代码中最激动人心的是对象被保存在一个统一的地方——那里不会产生什么错误!被动的代码比主动的更容易维护,因为那里很少会产生错误。可以思考一下下面这个例子的代码Listing 4-3:
public void performLookup(Container container) {
this.dep = (Dependency) container.getDependency("myDependency");
}
public void performLookup(Container container) {
this.dep = (Dependency) container.getDependency("myDependency");
}
在这段代码中,很多地方可能出问题:依赖的关键词(key)可能改变,容器的实例可能为null,或者返回的依赖可能是错误的形式。我们参考这段代码是因为这里有太多活动的部分,因为有很多地方可能发生变数。使用查找也许可以将你的应用程序的组件解耦,添加进来的代码用来将这些组件一一连接起来去完成一些有用得任务,但是这些代码提升了应用的复杂性。
Setter注入 对 构造器注入
现在我们已经确定哪种IoC方法更加优越,我们还需要选择使用setter注入还是构造器注入。构造器注入在你需要实例化存在依赖的类之前就使用这些组件的时候特别有用。很多容器,包括Spring,在使用setter注入的时候提供了一种检测依赖关系是否已经定义的机制,但是,如果使用构造器依赖注入时你却在容器无法察觉的情况下声明(assert)需要的依赖关系。
Setter注入在很多不同情况下都有用。如果这个组件对容器暴露了他的依赖关系,但是也愿意自己提供一个默认依赖关系,那么setter注入一般是这种情况下最好的实现方法。Setter注入的另一个优点是它允许以来关系被声明为借口,虽然这并不像一开始想象的那么有用。想象一个典型的商业逻辑接口,它包括一个商业逻辑方法defineMeaningOfLife()。如果,对于这个方法你追加定义一个setter注入,例如setEncylopedia(),那样就以为这你要求所有的具体实现都必须使用或者至少知道对encyclopedia的依赖。事实上你并不需要定义这些setter依赖注入,现有的任何IoC容器,包括Spring,都可以与应用了商业逻辑接口的组件一同工作,仍然对实现了这些接口的类提供依赖关系。一个使用这种方法的例子可以简单阐明这个问题。思考一下Listing 4-6里面的商业接口。
Listing 4-6. Oracle接口
package com.apress.prospring.ch4;
public interface Oracle {
public String defineMeaningOfLife();
}
注意商业逻辑没有定义任何依赖注入的setters方法。这个接口在Listing 4-7中会被实现。
Listing 4-7. 实现Oracle接口
package com.apress.prospring.ch4;
public class BookwormOracle implements Oracle {
private Encyclopedia enc;
public void setEncyclopedia(Encyclopedia enc) {
this.enc = enc;
}
public String defineMeaningOfLife() {
return "Encyclopedias are a waste of money - use the Internet";
}
}
如你所见,BookwormOracle不只实现了Oracle接口,同时还定义了setter依赖注入。Spring在处理这类结构的时候异常舒适,完全不需要在商务逻辑接口里定义依赖关系。使用接口来定义依赖的能力一般是setter注入所经常吹捧的,但是实际上,你应该努力保持setters注入的应用于商务逻辑接口保持独立。除非你非常肯定所有的特定商务逻辑接口的实现都需要与之对应的依赖关系,否则,最好让每个具体的实现定义它自己的以来关系,保证商务逻辑接口里只包括商务逻辑的方法。
虽然你不应该总是在商务逻辑接口中放置setters来注入依赖关系,但是在商务逻辑接口中放置setters和getters方法作为配置参数是一个很好的想法,这样setter注入就变成有价值的工具了。我们认为配置参数是一种特殊的依赖关系。当然你的组件依赖于配置数据,但是配置数据显然不同于你见过的其他依赖关系。我们马上就会讨论他们的不同,不过现在先思考一下Listing 4-8中的商业逻辑接口。
Listing 4-8. NewsletterSender接口
package com.apress.prospring.ch4;
public interface NewsletterSender {
public void setSmtpServer(String smtpServer);
public String getSmtpServer();
public void setFromAddress(String fromAddress);
public String getFromAddress();
public void send();
}
NewsletterSender接口是用来定义通过e-mail来发送一组新闻通讯的类。Send()方法是唯一的商务逻辑方法,但是可以看到我们在接口中定义了两个JavaBean属性。我们为什么这样做呢,我们不是刚刚说过不应该在商务逻辑接口中定义以来关系么?原因在于这些值,包括SMTP服务器地址和发送e-mails的客户端的地址,实际上没有依赖关系;相反的,它们的具体配置将会影响到所有的NewsletterSender接口的具体实现的运作。Spring的依赖注入能力为应用程序组件的外部配置提供了完美的解决方案。这里问题在于:配置参数和其它种类的依赖之间的区别在哪里呢?大部分情况下,你可以清晰的看出一个依赖是否应该以配置参数的方式封装,如果你不缺定,看看配置参数下面这三个特征:
1. 配置参数是被动的。在Listing 4-8中的NewsletterSender那个例子中,SMTP服务器的参数是一个被动依赖关系的范例。被动的依赖关系不是直接用来完成一个操作的;取而代之,他们是用来在内部使用或者由其它的依赖关系来完成实际操作。对比在第二章中的MessageRenderer那个例子中,MessageProvider的依赖不是被动的——它通过必须通过一个需要MessageRenderer的功能来完成这个工作。
2. 配置参数一般是消息,而不是其它组件。我们这里是说配置参数一般是一些消息片断,组件需要它们来完成工作。显然SMTP服务器就是一个NewsletterSender所需要的消息片断,但是,MessageProvider则确实是MessageRenderer正确完成功能所必须的另外一个组件。
3. 配置参数一般是简单的值或者是一组简单值的集合。这实际上是1和2所述问题的一个副产品,但是配置参数的确一般是简单的值。在Java中意味着它们是原生的(或者对应的封装类)或者是String再或是这些值的集合。简单值通常是被动的。这意味着你除了操作它本身的数据外,无法对String做其它的事情;而且你基本上知识拿这些值作为消息使用——例如,某一个int值代表监听的网络接口的端口号,或者某个String代表一个e-mail程序发送信息要访问的SMTP服务器。当决定是否要在商务逻辑接口中定义配置选项的时候,同时需要考虑一下这个配置参是对接口的所有实现都适用还是只对一个有用。例如,在实现NewsletterSender接口的时候,显然所有的实现都需要知道发送e-mails所用的SMTP服务器。然而,我们会选择将是否发送受保护e-mail的标志从商业逻辑接口中剥离,因为并不是所有的e-mail应用程序接口(APIs)都能够识别它,设想有大量的实现都不会考虑安全问题才是正确的。
注意:回忆一下在第2章中,我们选择在商用需求中定义依赖关系。这些都是为了描述的目的,它们都不是最好的实践。
Setter依赖注入还允许你在运行中将依赖关系替换为一个不同的实现,而不需要重新建立一个父组件的实例。Spring目前还不支持这项功能,但是一旦Spring支持JMX,这个功能就会自我实现了。也许setter依赖注入的最大优点就是它对注入机制的侵入最小。
如果你为一个恰巧只有默认构造器的类定义了构造器依赖注入,那么你将会在非IoC环境下使用依赖于它的所有类的时候遇到麻烦。以IoC为目的在类中定义额外的setters并不会影响到其它类与之交互的能力。
总的来说,基于setter的依赖注入是最佳选择,因为它会使你在非IoC的设置下将其对你代码的影响降到最低。构造器注入,当你需要在依赖关系传递给组件时要确认的情况下,是一个不错的选择,但是要记住,很多容器为这种情况提供了它们自己的基于setter依赖注入的机制。示例程序中的大部分代码都使用了setter依赖注入,虽然那里也有一些例子是基于构造器依赖注入的。





