理解依赖注入
虽然Spring能够完成很多事情,但是Spring框架的核心是DI。它听起来有点令人却步,好像是一种用了魔法的复杂编程技术或设计模式,但DI并没有像它听起来那样复杂。实际上,在项目中通过使用DI,你会发现你的代码明显变得简单易懂而且容易测试。
但是,“DI(dependency injection)”是什么意思呢?
1 依赖注入
最初,依赖注入通常被称为反向控制。Martin Fowler在2004年初的一篇论文中问到,控制的什么方面被反转了?他总结说是获得依赖对象的方式反转了。根据这个启示,他为反向控制提出了一个更好的名字叫做依赖注入。

任何重要的应用(几乎任何比HelloWorld.java复杂的应用)都需要至少两个相互合作的类来完成业务逻辑。通常,每个对象都要自己负责得到它的合作(依赖)对象。你会发现,这样会导致代码耦合度高而且难以测试。
使用DI,对象的依赖都是在对象创建时由负责协调应用中各个对象的外部实体提供的。这就是依赖被注入到对象中。所以,DI意味着关于对象如何得到它的协作对象的责任反转了(参见图1.2)。
DI的关键优点是松散耦合。如果一个对象通过其他接口(不是通过实现或实例化)只知道其依赖对象,那么该依赖将使用一个不同的实现来交互,其依赖的对象并不知道这种变化。
例如,如图1.2中的Foo类通过一个接口知道Bar依赖,Bar的实现对于Foo来说并不重要。Bar可以是一个本地POJO、一个远程Web服务、一个EJB或一个单元测试的模拟实现——Foo无需知道更多细节。
如果像我一样,你肯定急切地想知道其代码实现方式,但是抱歉,这需要到后面章节来介绍。
2 DI应用
假设你们公司最棒的销售团队聚集在一起交流他们的专业市场分析及研究结果,最后认为客户最需要的是骑士。也就是说,他们需要一个Java类来表示一个骑士。在考察了他们的需求后,你知道了他们的确切需求对你来说就是实现一个类,用它表示亚瑟王的圆桌骑士,这些骑士勇敢地从事着寻找圣杯的神圣任务。
这是一个奇怪的需求,但是你已经习惯了市场部的古怪想法和念头。所以,你毫不犹豫地打开喜爱的IDE,匆匆完成了程序清单1.5所示的这个类。
程序清单1.5 圆桌骑士Bean
在程序清单1.5中,骑士有一个以姓名作为参数的构造方法。这个构造方法实例化一个HolyGrailQuest,并将它当做骑士的探险任务。HolyGrailQuest的实现很简单,见程序清单1.6。
程序清单1.6 赋予骑士的Holy Grail bean查询
你对自己的工作很满意,自豪地将代码提交到版本控制服务器上。你想把它展示给市场部人员,但是感觉有些不对劲的地方。在意识到尚未写任何单元测试之前,你几乎是胸有成竹。
测试骑士
单元测试是软件开发的一个重要部分。它不但保证每个单元能如期工作,而且,它可以看作是每个单元的最准确的文档。为了矫正疏忽单位测试的失误,你为骑士类汇总了测试用例(见程序清单1.7)。
程序清单1.7 测试骑士
写完这个测试用例之后,你开始为HolyGrailQuest编写测试用例。但是在开始之前,你认识到KnightOfTheRoundTableTest测试用例间接地测试了HolyGrailQuest。你也想知道是否测试了所有的情况。如果HolyGrailQuest的embark()方法返回null或抛出GrailNotFoundException,会发生什么?
谁调用谁?
目前KnightOfTheRoundTable的主要问题是它如何获得HolyGrailQuest。不管它是通过实例化一个新的HolyGrail得到,还是通过JNDI得到,每个骑士都要自己负责获得探险任务(如图1.3所示)。所以,无法完全孤立地测试骑士类。事实上,每次当你测试KnightOfTheRoundTable的时候,你就间接地测试了HolyGrailQuest。
而且,你无法让HolyGrailQuest为不同的测试做不同的动作(如返回null或抛出GrailNotFoundException) 。如果你能创建一个HolyGrailQuest的模拟实现,由你决定它的动作,这样或许能有帮助。但既使你创建了一个模拟实现,KnightOfTheRoundTable得到的仍然是它自己的HolyGrailQuest。这意味着你必须为测试而修改KnightOfTheRoundTable(以后产品发布还要改回去),以得到模拟的Quest。
图1.3 骑士类通过实例化或其他方法自己得到它的探险任务
使用接口解耦合
一言蔽之,现在的问题是耦合。从这个观点看,KnightOfTheRoundTable与HolyGrailQuest紧密地耦合在一起。他们被紧紧地绑在了一起,以至于有一个KnightOfTheRoundTable就一定有一个HolyGrailQuest。
耦合是一个双头怪物,一方面,紧密耦合的代码难以测试,难以重用,难以理解,带来典型的“摧毁大堤”bug(如修改一个bug会引来另一个或更多的 bug)。另一方面,完全没有耦合的代码什么也做不了。为了做一些有意义的工作,类必须以某种方式知道其他类的存在。耦合是必须的,但需要小心管理。
减少耦合的一个通常的做法是将具体实现隐藏在接口下面,这样具体实现类的替换不会影响到引用类。例如,假设你要创建一个Quest接口:
现在,让HolyGrailQuest来实现这个接口。同时注意,embark()方法返回的是一个Object并且会抛出QuestFailedException异常。
同样,为了与这个Quest接口兼容,KnightOfTheRoundTable的方法也要加以修改:
同样,你也要让KnightOfTheRoundTable实现下面的这个Knight接口:
将类的实现隐藏在接口后面,我们向正确方向迈出了一步。但是很多开发人员在如何得到Quest实例上又犯了错误。例如,考虑一下下面对KnightOfTheRoundTable的修改:
现在,类KnightOfTheRoundTable通过Quest接口从事探险。但是,该类仍然只能得到一种特殊类型的Quest(这里是HolyGrailQuest)。现在情况并没有多少好转,圆桌骑士还是只能进行圣杯探险而不是其他的探险。
给予与获得
在这个时候,你应该问的问题是,是应该让骑士自己负责获得探险任务,还是应该给予骑士他要从事的探险任务?
考虑下面对KnightOfTheRoundTable的修改:

注意到有什么不同吗?比较图1.4和图1.3可以看出,骑士得到它的探险任务的方式是不同的。
现在,骑士是被动得到探险任务而不是自主得到。KnightOfTheRoundTable不再负责得到自己的探险任务。并且因为他只知道Quest接口,所以,你可以按自己的需要给他任何一种Quest的实现。在发布的产品中或许你想给它一个HolyGrailQuest,但在测试用例中,你给它一个Quest的模拟实现。
简言之,这就是DI的全部:协调依赖对象之间合作的责任从对象自身中转移出来。这就是像Spring这样的轻量级框架所起的作用。
给骑士指定探险任务
现在,你已经修改了类KnightOfTheRoundTable,它可以接受任何指定的Quest对象,你怎么指定该给它哪个Quest呢?
创建应用组件之间关联的动作叫做装配。在Spring中,有很多方式可以把组件装配起来,但是,最常见的就是使用XML。程序清单1.8显示一个简单的Spring配置文件knight.xml,这个文件把一个探险任务(一个HolyGrailQuest)赋给一个KnightOfTheRoundTable。
程序清单1.8 在knight.xml中把一个探险任务装配给一个骑士

这是一种简单的装配Bean的方法。现在不要太担心它的细节。在第2章我们将详细解释,并向你介绍更多如何在Spring中装配Bean的方法。
现在我们已经声明了骑士和探险任务之间的关系,我们需要装载XML文件,并开始运行我们的应用。
观察它如何工作
在Spring应用中,BeanFactory负责装载Bean的定义并把它们装配起来。由于在骑士的例子中,Bean是用XML文件定义的,所以在这个例子中适合使用XmlBeanFactory。程序清单1.9中的main()方法使用XmlBeanFactory来装载knight.xml文件,然后得到“knight”对象的引用。
程序清单1.9 运行Knight例子

应用得到KnightOfTheRoundTable对象的引用后,只是简单调用embarkOnQuest()方法就可以开始骑士的探险任务了。注意,这个类不知道骑士会接受哪种探险任务。只有knight.xml文件知道骑士从事什么探险任务。

使用DI派遣骑士去执行探险任务很有趣,但是,现在让我们看一下如何在真实的企业应用中使用依赖注入。
1.3.3 企业级应用中的依赖注入
假设你被要求写一个在线购物应用。该应用需要一个订单服务组件来处理所有与订单相关的函数。图1.5演示了一个Web层Checkout组件(可能是一个WebWork动作或一个Tapestry页面)可以访问订单服务的几种方法。
Checkout组件(也许是WebWork动作或Tapestry页面)会访问订单服务。一个简单的方法是在需要的时候直接实例化订单服务u。除了直接耦合Web层到指定服务类之外,这个方法还会导致创建不经济的OrderServiceImpl类。
如果订单服务通过2.x EJB来实现,可以通过JNDI来检索主接口来访问该服务v,然后它被用来访问EJB服务接口的实现。此时,Web层不再耦合到特定接口,但是它耦合到JNDI和EJB 2.x编程模型。
作为EJB 3 bean,可以从JNDI直接查找订单服务w(不需要通过主接口)。而且,这不会有任何到特定实现类的耦合,但是它对JNDI有一个依赖。
使用或不使用EJB,都可以选择隐藏服务寻址之后的查找细节x。这会解决与其他方法的耦合关系,但是现在Web层仍然会耦合到服务定位器。
所有这些方法的关键问题是Web层组件也包含在其依赖组件中,它知道有关订单服务内容及其实现方法的更多细节。
图1.6 通过将OrderService注入到Checkout组件中,那么实现服务的地方就可以检索到Checkout
知道太多依赖对象的内容会导致紧密耦合代码,知道越少耦合就越松散。参看图1.6,该图显示了Checkout组件如何给予了一个OrderService,而不是主动请求。
现在,让我们看看如果使用DI来如何实现这种方案:
没有查找代码!到OrderService(它是一个接口)的引用将通过setOrderService()方法来给定。Web组件并不知道OrderSerive来自何处。它可以由Spring注入或者可以通过显示调用setOrderService()来手动注入。它也不知道OrderService是如何实现的——它仅知道通过OrderService接口来访问。使用DI,可以释放应用对象获取其依赖对象的负担,从而可以集中除了自己的任务,相关依赖对象在需要的时候才使用。
依赖注入是一种松散耦合代码,这样可以尽可能是的应用对象之间保持疏远的关系。但是,我们只是简单介绍了Spring容器和DI。在第2章和第3章中,我们将就介绍在Spring容器中配置对象的多个方法。
依赖注入只是Spring提供的一种技术,它支持松散耦合。面向切面编程技术提供了另外一种解耦合的功能,它将应用功能(例如安全和事务)和对象分开。让我们快速地看一下Spring对AOP的支持。