实例讲解Java的Spring框架中的控制反转和依赖注入

近来总是接触到 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 是组件提供者),也就是控制反转了。

(0)

相关推荐

  • Spring 依赖注入的几种方式详解

    IoC 简介 平常的Java开发中,程序员在某个类中需要依赖其它类的方法. 通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理. Spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过Spring容器帮我们new指定实例并且将实例注入到需要该对象的类中. 依赖注入的另一种说法是"控制反转".通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员. 而控制反转是指new实例工作不由我们程序员来做而是交给Spring容器来做.

  • 详解Java Spring各种依赖注入注解的区别

    注解注入顾名思义就是通过注解来实现注入,Spring和注入相关的常见注解有Autowired.Resource.Qualifier.Service.Controller.Repository.Component. Autowired是自动注入,自动从spring的上下文找到合适的bean来注入 Resource用来指定名称注入 Qualifier和Autowired配合使用,指定bean的名称 Service,Controller,Repository分别标记类是Service层类,Contro

  • 深入解析Java的Spring框架中bean的依赖注入

    每一个基于java的应用程序都有一个共同工作来展示给用户看到的内容作为工作的应用几个对象.当编写一个复杂的Java应用程序,应用程序类应该尽可能独立其他Java类来增加重复使用这些类,并独立于其他类别的测试它们,而这样做单元测试的可能性.依赖注入(或有时称为布线)有助于粘合这些类在一起,同时保持他们的独立. 考虑有其中有一个文本编辑器组件的应用程序,要提供拼写检查.标准的代码将看起来像这样: public class TextEditor { private SpellChecker spell

  • 详解Spring的核心机制依赖注入

    详解Spring的核心机制依赖注入 对于一般的Java项目,他们都或多或少有一种依赖型的关系,也就是由一些互相协作的对象构成的.Spring把这种互相协作的关系称为依赖关系.如A组件调用B组件的方法,可称A组件依赖于B组件,依赖注入让Spring的Bean以配置文件组织在一起,而不是以硬编码的方式耦合在一起 一.理解依赖注入 依赖注入(Dependency Injection) = 控制反转(Inversion ofControl,IoC):当某个Java实例(调用者)需另一个Java实例(被调

  • 详解SpringBoot中实现依赖注入功能

    今天给大家介绍一下SpringBoot中是如何实现依赖注入的功能. 在以往spring使用中,依赖注入一般都是通过在Spring的配置文件中添加bean方法实现的,相对于这个方式SpringBoot的实现方式就显得非常便捷了.SpringBoot的实现方式基本都是通过注解实现的. 下面来看一下具体案例,这里我编写了三个测试类用于测试依赖注入到底是否可以正确实现. TestBiz接口: package example.biz; public interface TestBiz { public S

  • 详析Spring中依赖注入的三种方式

    前言 平常的java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中.依赖注入的另一种说法是"控制反转",通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做. 在Sprin

  • 因Spring AOP导致@Autowired依赖注入失败的解决方法

    发现问题: 之前用springAOP做了个操作日志记录,这次在往其他类上使用的时候,service一直注入失败,找了网上好多内容,发现大家都有类似的情况出现,但是又和自己的情况不太符合.后来总结自己的情况发现:方法为private修饰的,在AOP适配的时候会导致service注入失败,并且同一个service在其他的public方法中就没有这种情况,十分诡异. 解决过程: 结合查阅的资料进行了分析:在org.springframework.aop.support.AopUtils中: publi

  • 实例讲解Java的Spring框架中的控制反转和依赖注入

    近来总是接触到 IoC(Inversion of Control,控制反转).DI(Dependency Injection,依赖注入)等编程原则或者模式,而这些是著名 Java 框架 Spring.Struts 等的核心所在.针对此查了 Wikipedia 中各个条目,并从图书馆借来相关书籍,阅读后有些理解,现结合书中的讲解以及自己的加工整理如下: eg1 问题描述: 开发一个能够按照不同要求生成Excel或 PDF 格式的报表的系统,例如日报表.月报表等等.   解决方案: 根据"面向接口编

  • 实例讲解Java的Spring框架中的AOP实现

    简介 面向切面编程(AOP)提供另外一种角度来思考程序结构,通过这种方式弥补了面向对象编程(OOP)的不足. 除了类(classes)以外,AOP提供了 切面.切面对关注点进行模块化,例如横切多个类型和对象的事务管理. (这些关注点术语通常称作 横切(crosscutting) 关注点.) Spring的一个关键的组件就是 AOP框架. 尽管如此,Spring IoC容器并不依赖于AOP,这意味着你可以自由选择是否使用AOP,AOP提供强大的中间件解决方案,这使得Spring IoC容器更加完善

  • Spring框架中IoC容器与DI依赖注入教程

    目录 一.Spring 是什么 1.1 什么是容器 1.2 什么是 IoC 二.理解 IoC 2.1 传统程序开发的问题 2.2 分析 2.3 控制反转式程序开发 2.4 对比总结规律 2.5 理解 Spring IoC 三.DI 概念说明 四.总结 一.Spring 是什么 我们通常所说的 Spring 指的是 Spring Framework (Spring 框架),它是⼀个开源框架,有着活跃而庞大的社区,这就是它之所以能长久不衰的原因.Spring ⽀持⼴泛的应⽤场景,它可以让 Java

  • 举例讲解Java的Spring框架中AOP程序设计方式的使用

    1.什么是AOP AOP是Aspect Oriented Programming的缩写,意思是面向方面编程,AOP实际是GoF设计模式的延续. 2.关于Spring AOP的一些术语:  A.切面(Aspect):在Spring AOP中,切面可以使用通用类或者在普通类中以@Aspect 注解(@AspectJ风格)来实现 B.连接点(Joinpoint):在Spring AOP中一个连接点代表一个方法的执行 C.通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作.

  • laravel框架中你所用到的依赖注入详解

    前言 用Laravel开发前前后后有2个月左右了,之前一直写Java,就像找到Java和PHP之前的共同点,用Java的某些原理去理解PHP会发现还是有很多共通之处的.Java的依赖注入已经是一个很常见的概念了,Spring框架主要就是解决了这一点,在PHP的laravel框架中,也出现了依赖注入的方式. 依赖注入就控制反转的一种是实现方式,面向对象的特征的重要体现,那么依赖注入中什么是依赖呢,这点用Java开发的人很多都能理解.笼统的说依赖就是一种联系,变量和实现的联系.有关于依赖注入的理解之

  • Spring 控制反转和依赖注入的具体使用

    目录 控制反转的类型 1.依赖查找 1.1依赖拉取 1.2上下文依赖查找 2.依赖注入 2.1构造函数注入 2.2setter函数注入 Spring中的控制反转 1.Bean和BeanFactory 2.设置Spring配置 2.1XML配置 2.2注解配置 2.3Java配置 3.setter注入 4.构造函数注入 控制反转的类型 控制反转(IOC)旨在提供一种更简单的机制,来设置组件的依赖项,并在整个生命周期管理这些依赖项.通常,控制反转可以分成两种子类型:依赖注入(DI)和依赖查找(DL)

  • C++设计模式中控制反转与依赖注入浅析

    目录 控制反转 依赖注入(DI) 依赖注入框架(DI Framework) 依赖反转原则(DIP) 控制反转 “控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行.在使用框架之后,整个程序的执行流程可以通过框架来控制.流程的控制权从程序员“反转”到了框架. 大白话说,就是原先直接用main函数中的代码流程,转移到了框架中去. #include <iostream> #include <list> using namespace std

  • Java的Spring框架中AOP项目的一般配置和部署教程

    0.关于AOP 面向切面编程(也叫面向方面编程):Aspect Oriented Programming(AOP),是软件开发中的一个热点,也是Spring框架中的一个重要内容.利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率. AOP是OOP的延续. 主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等. 主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过

  • 深入理解Java的Spring框架中的IOC容器

    Spring IOC的原型 spring框架的基础核心和起点毫无疑问就是IOC,IOC作为spring容器提供的核心技术,成功完成了依赖的反转:从主类的对依赖的主动管理反转为了spring容器对依赖的全局控制. 这样做的好处是什么呢? 当然就是所谓的"解耦"了,可以使得程序的各模块之间的关系更为独立,只需要spring控制这些模块之间的依赖关系并在容器启动和初始化的过程中将依据这些依赖关系创建.管理和维护这些模块就好,如果需要改变模块间的依赖关系的话,甚至都不需要改变程序代码,只需要将

随机推荐