实例讲解Java的Spring框架中的控制反转和依赖注入
作者:haolloyin
近来总是接触到 IoC(Inversion of Control,控制反转)、DI(Dependency Injection,依赖注入)等编程原则或者模式,而这些是著名 Java 框架 Spring、Struts 等的核心所在。针对此查了 Wikipedia 中各个条目,并从图书馆借来相关书籍,阅读后有些理解,现结合书中的讲解以及自己的加工整理如下:
eg1
问题描述:
开发一个能够按照不同要求生成Excel或 PDF 格式的报表的系统,例如日报表、月报表等等。
解决方案:
根据“面向接口编程”的原则,应该分离接口与实现,即将生成报表的功能提取为一个通用接口ReportGenerator,并提供生成 Excel 和 PDF格式报表的两个实现类 ExcelGenerator 和 PDFGenerator,而客户Client 再通过服务提供者 ReportService 获取相应的报表打印功能。
实现方法:
根据上面所述,得到如下类图:
代码实现:
interface ReportGenerator { public void generate(Table table); } class ExcelGenerator implements ReportGenerator { public void generate(Table table) { System.out.println("generate an Excel report ..."); } } class PDFGenerator implements ReportGenerator { public void generate(Table table) { System.out.println("generate an PDF report ..."); } } class ReportService { // 负责创建具体需要的报表生成器 private ReportGenerator generator = new PDFGenerator(); // private static ReportGenerator generator = new ExcelGenerator(); public void getDailyReport(Date date) { table.setDate(date); // ... generator.generate(table); } public void getMonthlyReport(Month month) { table.setMonth(month); // ... generator.generate(table); } } public class Client { public static void main(String[] args) { ReportService reportService = new ReportService(); reportService.getDailyReport(new Date()); //reportService.getMonthlyReport(new Date()); } }
eg2
问题描述:
如上面代码中的注释所示,具体的报表生成器由 ReportService 类内部硬编码创建,由此 ReportService 已经直接依赖于 PDFGenerator 或 ExcelGenerator ,必须消除这一明显的紧耦合关系。
解决方案:引入容器
引入一个中间管理者,也就是容器(Container),由其统一管理报表系统所涉及的对象(在这里是组件,我们将其称为 Bean),包括 ReportService 和各个 XXGenerator 。在这里使用一个键-值对形式的 HashMap 实例来保存这些 Bean。
实现方法:
得到类图如下:
代码实现:
class Container { // 以键-值对形式保存各种所需组件 Bean private static Map<String, Object> beans; public Container() { beans = new HashMap<String, Object>(); // 创建、保存具体的报表生起器 ReportGenerator reportGenerator = new PDFGenerator(); beans.put("reportGenerator", reportGenerator); // 获取、管理 ReportService 的引用 ReportService reportService = new ReportService(); beans.put("reportService", reportService); } public static Object getBean(String id) { return beans.get(id); } } class ReportService { // 消除紧耦合关系,由容器取而代之 // private static ReportGenerator generator = new PDFGenerator(); private ReportGenerator generator = (ReportGenerator) Container.getBean("reportGenerator"); public void getDailyReport(Date date) { table.setDate(date); generator.generate(table); } public void getMonthlyReport(Month month) { table.setMonth(month); generator.generate(table); } } public class Client { public static void main(String[] args) { Container container = new Container(); ReportService reportService = (ReportService)Container.getBean("reportService"); reportService.getDailyReport(new Date()); //reportService.getMonthlyReport(new Date()); } }
时序图大致如下:
效果:
如上面所示,ReportService 不再与具体的 ReportGenerator 直接关联,已经用容器将接口和实现隔离开来了,提高了系统组件 Bean 的重用性,此时还可以使用配置文件在 Container 中实时获取具体组件的定义。
eg3
问题描述:
然而,观察上面的类图,很容易发现 ReportService 与 Container 之间存在双向关联,彼此互相有依赖关系。并且,如果想要重用 ReportService,由于它也是直接依赖于单独一个 Container 的具体查找逻辑。若其他容器具体不同的组件查找机制(如 JNDI),此时重用 ReportService 意味着需要修改 Container 的内部查找逻辑。
解决方案:引入 Service Locator
再次引入一个间接层 Service Locator,用于提供组件查找逻辑的接口,请看Wikipedia 中的描述 或者 Java EE 对其的描述1 、描述2 。这样就能够将可能变化的点隔离开来。
实现方法:
类图如下:
代码实现:
// 实际应用中可以是用 interface 来提供统一接口 class ServiceLocator { private static Container container = new Container(); public static ReportGenerator getReportGenerator() { return (ReportGenerator)container.getBean("reportGeneraator"); } } class ReportService { private ReportGenerator reportGenerator = ServiceLocator.getReportGenerator(); // ... }
eg4
问题描述:
然而,不管是引入 Container 还是使用 Service Locator ,ReportService 对于具体组件的查找、创建的方式都是‘主动'的,这意味着作为客户的 ReportService 必须清楚自己需要的是什么、到哪里获取、如何获取。一下子就因为 What、Where、How 而不得不增加了具体逻辑细节。
例如,在前面‘引入Container '的实现方法中,有如下代码:
class ReportService { // 消除紧耦合关系,由容器取而代之 // private static ReportGenerator generator = new PDFGenerator(); // 通过 Container..getBean("reportGenerator") ‘主动'查找 private ReportGenerator generator = (ReportGenerator) Container .getBean("reportGenerator");
在‘引入 Service Locator '的实现方法中,有如下代码:
class ServiceLocator { privatestatic Container container = new Container(); publicstatic ReportGenerator getReportGenerator() { // 还是container.getBean(), 用了委托而已 return (ReportGenerator) container.getBean("reportGeneraator"); } } class ReportService { // ReportService 最终还是‘主动'查找,委托给ServiceLocator 而已 private ReportGenerator reportGenerator = ServiceLocator.getReportGenerator(); }
解决方案:
在这种情况下,变‘主动'为‘被动'无疑能够减少 ReportService 的内部知识(即查找组件的逻辑)。根据控制反转(IoC)原则,可江此种拉(Pull,主动的)转化成推(Push,被动的)的模式。
例如,平时使用的 RSS 订阅就是Push的应用,省去了我们一天好几次登录自己喜爱的站点主动获取文章更新的麻烦。
而依赖注入(DI)则是实现这种被动接收、减少客户(在这里即ReportService)自身包含复杂逻辑、知晓过多的弊病。
实现方法:
因为我们希望是‘被动'的接收,故还是回到 Container 的例子,而不使用 Service Locator 模式。由此得到修改后的类图如下:
而原来的类图如下,可以对照着看一下,注意注释的提示:
代码实现:
为了使例子能够编译、运行,并且稍微利用跟踪代码的运行结果来显式整个类图实例化、互相协作的先后顺序,在各个类的构造器中加入了不少已编号的打印语句,以及两个无关紧要的类,有点啰唆,具体如下:
import java.util.Date; import java.util.HashMap; import java.util.Map; // 为了能够编译运行,多了两个无关紧要的类 class Month { } class Table { publicvoid setDate(Date date) { } publicvoid setMonth(Month month) { } } // ------------ 以下均无甚重要改变 ----------------- // interface ReportGenerator { publicvoid generate(Table table); } class ExcelGenerator implements ReportGenerator { public ExcelGenerator() { System.out.println("2...开始初始化 ExcelGenerator ..."); } publicvoid generate(Table table) { System.out.println("generate an Excel report ..."); } } class PDFGenerator implements ReportGenerator { public PDFGenerator() { System.out.println("2...开始初始化 PDFGenerator ..."); } publicvoid generate(Table table) { System.out.println("generate an PDF report ..."); } } //------------ 以上均无甚重要改变 ----------------- // class Container { // 以键-值对形式保存各种所需组件 Bean privatestatic Map<String, Object> beans; public Container() { System.out.println("1...开始初始化 Container ..."); beans = new HashMap<String, Object>(); // 创建、保存具体的报表生起器 ReportGenerator reportGenerator = new PDFGenerator(); beans.put("reportGenerator", reportGenerator); // 获取、管理 ReportService 的引用 ReportService reportService = new ReportService(); // 注入上面已创建的具体 ReportGenerator 实例 reportService.setReportGenerator(reportGenerator); beans.put("reportService", reportService); System.out.println("5...结束初始化 Container ..."); } publicstatic Object getBean(String id) { System.out.println("最后获取服务组件...getBean() --> " + id + " ..."); returnbeans.get(id); } } class ReportService { // private static ReportGenerator generator = new PDFGenerator(); // 消除上面的紧耦合关系,由容器取而代之 // private ReportGenerator generator = (ReportGenerator) Container // .getBean("reportGenerator"); // 去除上面的“主动”查找,提供私有字段来保存外部注入的对象 private ReportGenerator generator; // 以 setter 方式从外部注入 publicvoid setReportGenerator(ReportGenerator generator) { System.out.println("4...开始注入 ReportGenerator ..."); this.generator = generator; } private Table table = new Table(); public ReportService() { System.out.println("3...开始初始化 ReportService ..."); } publicvoid getDailyReport(Date date) { table.setDate(date); generator.generate(table); } publicvoid getMonthlyReport(Month month) { table.setMonth(month); generator.generate(table); } } publicclass Client { publicstaticvoid main(String[] args) { // 初始化容器 new Container(); ReportService reportService = (ReportService) Container .getBean("reportService"); reportService.getDailyReport(new Date()); // reportService.getMonthlyReport(new Date()); } }
运行结果:
1...开始初始化 Container ... 2...开始初始化 PDFGenerator ... 3...开始初始化 ReportService ... 4...开始注入 ReportGenerator ... 5...结束初始化 Container ... 最后获取服务组件...getBean() --> reportService ... generate an PDF report ...
注意:
1、根据上面运行结果的打印顺序,可见代码中加入的具体编号是合理的,模拟了程序执行的流程,于是也就不再画序列图了。
2、注意该例子中对IoC、DI的使用,是以ReportService为客户端(即组件需求者)为基点的,而代码中的Client 类main()中的测试代码才是服务组件的最终用户,但它需要的不是组件,而是组件所具有的服务。
3、实际在Spring框剪中,初始化Container显然不是最终用户Client应该做的事情,它应该由服务提供方事先启动就绪。
4、在最终用户Client中,我们还是用到Container.getBean("reportService")来获取事先已在Container的构造函数中实例化好的服务组件。而在具体应用中,通常是用XML等配置文件将可用的服务组件部署到服务器中,再由Container读取该配置文件结合反射技术得以创建、注入具体的服务组件。
分析:
之前是由ReportService主动从Container中请求获取服务组件,而现在是被动地等待Container注入(Inject,也就是Push)服务组件。控制权明显地由底层模块(ReportService 是组件需求者)转移给高层模块(Container 是组件提供者),也就是控制反转了。