- 作者简介
- 内容提要
- 关于本书
- 路线图
- 代码规范与下载
- 作者在线
- 封面插图简介
- 前言
- 译者序
- 致谢
- 第1部分 Spring 的核心
- 第1章 Spring 之旅
- 第2章 装配 Bean
- 第3章 高级装配
- 第4章 面向切面的 Spring
- 第2部分 Web 中的 Spring
- 第5章 构建 Spring Web 应用程序
- 第6章 渲染 Web 视图
- 第7章 Spring MVC 的高级技术
- 第8章 使用 Spring Web Flow
- 第9章 保护 Web 应用
- 第3部分 后端中的 Spring
- 第10章 通过 Spring 和 JDBC 征服数据库
- 第11章 使用对象-关系映射持久化数据
- 第12章 使用 NoSQL 数据库
- 第13章 缓存数据
- 第14章 保护方法应用
- 第4部分 Spring 集成
- 第15章 使用远程服务
- 第16章 使用 Spring MVC 创建 REST API
- 第17章 Spring消息
- 第18章 使用 WebSocket 和 STOMP 实现消息功能
- 第19章 使用 Spring 发送 Email
- 第20章 使用 JMX 管理 Spring Bean
- 第21章 借助 Spring Boot 简化 Spring 开发
1.1.2 依赖注入
依赖注入这个词让人望而生畏,现在已经演变成一项复杂的编程技巧或设计模式理念。但事实证明,依赖注入并不像它听上去那么复杂。在项目中应用DI,你会发现你的代码会变得异常简单并且更容易理解和测试。
DI功能是如何实现的
任何一个有实际意义的应用(肯定比Hello World示例更复杂)都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。
举个例子,考虑下程序清单1.2所展现的Knight类。
程序清单1.2 DamselRescuingKnight只能执行RescueDamselQuest探险任务
可以看到,DamselRescuingKnight在它的构造函数中自行创建了Rescue DamselQuest。这使得DamselRescuingKnight紧密地和RescueDamselQuest耦合到了一起,因此极大地限制了这个骑士执行探险的能力。如果一个少女需要救援,这个骑士能够召之即来。但是如果一条恶龙需要杀掉,或者一个圆桌……额……需要滚起来,那么这个骑士就爱莫能助了。
更糟糕的是,为这个DamselRescuingKnight编写单元测试将出奇地困难。在这样的一个测试中,你必须保证当骑士的embarkOnQuest()方法被调用的时候,探险的embark()方法也要被调用。但是没有一个简单明了的方式能够实现这一点。很遗憾,DamselRescuingKnight将无法进行测试。
耦合具有两面性(two-headed beast)。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性(修复一个bug,将会出现一个或者更多新的bug)。另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合是必须的,但应当被小心谨慎地管理。
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如图1.1所示,依赖关系将被自动注入到需要它们的对象当中去。
图1.1 依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖
为了展示这一点,让我们看一看程序清单1.3中的BraveKnight,这个骑士不仅勇敢,而且能挑战任何形式的探险。
程序清单1.3 BraveKnight足够灵活可以接受任何赋予他的探险任务
我们可以看到,不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。
更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight能够响应RescueDamselQuest、 SlayDragonQuest、 MakeRound TableRounderQuest 等任意的Quest实现。
这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是哪种类型的探险就无关紧要了。这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的;但是可以轻松地测试BraveKnight,只需给它一个Quest的mock实现即可,如程序清单1.4所示。
程序清单1.4 为了测试BraveKnight,需要注入一个mock Quest
你可以使用mock框架Mockito去创建一个Quest接口的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight实例,并通过构造器注入这个mock Quest。当调用embarkOnQuest()方法时,你可以要求Mockito框架验证Quest的mock实现的embark()方法仅仅被调用了一次。
将Quest注入到Knight中
现在BraveKnight类可以接受你传递给它的任意一种Quest的实现,但该怎样把特定的Query实现传给它呢?假设,希望BraveKnight所要进行探险任务是杀死一只怪龙,那么程序清单1.5中的SlayDragonQuest也许是挺合适的。
程序清单1.5 SlayDragonQuest是要注入到BraveKnight中的Quest实现
我们可以看到,SlayDragonQuest实现了Quest接口,这样它就适合注入到BraveKnight中去了。与其他的Java入门样例有所不同,SlayDragonQuest没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。这里最大的问题在于,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢?
创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。程序清单1.6展现了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnight、SlayDragonQuest和PrintStream装配到了一起。
程序清单1.6 使用Spring将SlayDragonQuest注入到BraveKnight中
在这里,BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,它在构造时传入了对SlayDragonQuest bean的引用,将其作为构造器参数。同时,SlayDragonQuest bean的声明使用了Spring表达式语言(Spring Expression Language),将System.out(这是一个PrintStream)传入到了SlayDragonQuest的构造器中。
如果XML配置不符合你的喜好的话,Spring还支持使用Java来描述配置。比如,程序清单1.7展现了基于Java的配置,它的功能与程序清单1.6相同。
程序清单1.7 Spring提供了基于Java的配置,可作为XML的替代方案
不管你使用的是基于XML的配置还是基于Java的配置,DI所带来的收益都是相同的。尽管BraveKnight依赖于Quest,但是它并不知道传递给它的是什么类型的Quest,也不知道这个Quest来自哪里。与之类似,SlayDragonQuest依赖于PrintStream,但是在编码时它并不需要知道这个PrintStream是什么样子的。只有Spring通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。
这个样例展现了在Spring中装配bean的一种简单方法。谨记现在不要过多关注细节。第2章我们会深入讲解Spring的配置文件,同时还会了解Spring装配bean的其他方式,甚至包括一种让Spring自动发现bean并在这些bean之间建立关联关系的方式。
现在已经声明了BraveKnight和Quest的关系,接下来我们只需要装载XML配置文件,并把应用启动起来。
观察它如何工作
Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。
因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext[1]作为应用上下文相对是比较合适的。该类加载位于应用程序类路径下的一个或多个XML配置文件。程序清单1.8中的main()方法调用ClassPathXmlApplicationContext加载knights.xml,并获得Knight对象的引用。
程序清单1.8 KnightMain.java加载包含Knight的Spring上下文
这里的main()方法基于knights.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的引用后,只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任务,而且完全没有意识到这是由BraveKnight来执行的。只有knights.xml文件知道哪个骑士执行哪种探险任务。
通过示例我们对依赖注入进行了一个快速介绍。纵览全书,你将对依赖注入有更多的认识。如果你想了解更多关于依赖注入的信息,我推荐阅读Dhanji R. Prasanna的《Dependency Injection》,该著作覆盖了依赖注入的所有内容。
现在让我们再关注Spring简化Java开发的下一个理念:基于切面进行声明式编程。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论