IoC容器的实现( IoC 容器概述和在 Spring 中的应用场景)

0.336字数 3503阅读 3052

1 IoC 容器概述

1.1 IOC 容器和依赖反转模式

  • 我们日常的 java 项目开发都是由两个或多个类的彼此合作来实现业务逻辑的,这使得每个对象都需要与其合作的对象的引用(称为所依赖的对象),如果合作的对象的引用或依赖关系由具体的对象来实现,这对复杂的面向对象系统的设计与开发是非常不利的,由此,我们如果能把这些依赖关系和对象的注入交给框架来实现,让具体对象交出手中对于依赖对象的控制,那么就能很大程度上解耦代码,这显然是极有价值的。而这,就是“依赖反转”,即反转对依赖的控制,把控制权从具体的对象中转交到平台或者框架。

  • 实现依赖反转的实现有很多种,在 Spring 中,IoC 容器就是实现这个模式的载体,它可以在对象生成或初始化的过程中,直接将数据或者依赖对象的引用注入到对象的数据域中从而来实现方法调用的依赖。而这种依赖注入是递归的,依赖对象会被逐层注入,从而建立起一套有序的对象依赖关系,简化了对象依赖关系的管理,把面向对象过程中需要执行的如何新建对象、为对象引用赋值等操作交由容器统一管理,极大程度上减低了面向对象编程的复杂性。

1.2 IoC 在 Spring 中的应用场景

  • 在 Spring 中,Spring IoC 提供了一个基本 JavaBean 容器,通过 IoC 模式管理依赖关系,并通过依赖注入和 AOP 切面增强了为 JavaBean 这样的 POJO 对象提供了事务管理、声明周期管理等功能。

  • 在应用开发中,当我们在设计组件时,往往需要引入和调用其他组件的服务时,这种依赖关系如果固化在组件设计中就会导致组件之间的耦合和维护难度的增大,这个时候如果使用 IoC 容器,把资源获取的方式反转,让 IoC 容器主动管理这些依赖关系,将依赖关系注入到组件中,那么这些依赖关系的适配和管理就会更加灵活。

  • 在应用管理依赖关系时,如果在 IOC 实现依赖反转的过程中,能通过可视化的文本来完成配置,并且通过工具对这些配置信息进行可视化的管理和浏览,那么肯定能提高依赖关系的管理水平,而且如果耦合关系变动,并不需要重新修改和编译 Java 代码,这符合在面向对象过程中的开闭原则。

2 IoC 容器系列的设计与实现:BeanFactory 和 ApplicationContext

使用过 Spring 的同学应该都接触过 BeanFactory 和 ApplicationContext,其实它们就可以看做 Spring IoC 容器的具体表现形式,了解这两者的区别与联系对于我们理解和使用 IoC 容器是比较重要的。我们先来看一下 IoC 容器的接口设计图:

IoC 容器的接口设计图

下面我们来对这张接口设计图做一下解析:

  • 第一条接口设计主线是:BeanFactory -> HierarchicalBeanFactory -> ConfigureBeanFactory。在这条设计路径中,BeanFactory 定义了 IoC 容器的基本规范,包括了例如 getBean()(从容器中取得 Bean)这样的 IoC 容器基本方法。而 HierarchicalBeanFactory 接口则在继承 BeanFactory 基础上,增加了 getParentBeanFactory() 的接口功能,使得 BeanFactory 具备了双亲 IoC容器的管理功能。接下来的 ConfigureBeanFactory 接口中,主要定义了一些对 BeanFactory 的配置功能,如通过 setParentBeanFactory() 设置双亲容器和通过 addBeanPostProcessor() 设置 Bean 后置处理器等。通过这些接口的叠加,定义了 BeanFactory 就是简单 IoC 容器的基本功能。

  • 第二条设计主线是以 ApplicationContext 为核心的路径:BeanFactory -> ListableBeanFactory -> ApplicationContext 再到我们常用的 WebApplicationContext 或 ConfigureApplicationContext。在这条路径中,ListableBeanFactory 和 HierarchicalBeanFactory 连接了 BeanFactory 接口和 ApplicationContext 应用上下文,ListableBeanFactory 细化了 BeanFactory 接口功能,HierarchicalBeanFactory 上文已经提到了。对于 ApplicationContext,它通过集成 MessageSource、ResourceLoader、ApplicationEventPublisher 接口,在 BeanFactory 简单的 IoC 容器的基础上添加了许多对高级容器的特性和支持。

  • 这里主要涉及的是接口关系,而具体的容器是在这个接口体系下实现的,必须 DefaultListableBeanFactory 这个基本的 IoC 容器就实现了 ConfigureListableBeanFactory,从而成为一个简单的 IoC 容器的实现。而其他的 BeanFactory 必须 XmlBeanFactory 都是在 DefaultListableBeanFactory 的基础上做扩展,同样的 ApplicationContext 体系也是一样。

我们通过以上的接口设计图跟分析可以看出,整个 Spring IoC 容器就是以 BeanFactory 和 ApplicationContext 作为核心的。BeanFactory 定义了 IoC 容器的基本功能,而 ApplicationContext 体系则在 BeanFactory 基础上通过继承其他接口来实现高级容器特征。下面我们来看一下这两个体系的应用场景:

  • BeanFactory 的应用场景

    • BeanFactory 提供的是最基本的 IoC 容器的功能,关于这些功能,我们可以在接口中看到:
      BeanFactory接口
    • BeanFactory 接口定义了 IoC 容器最基本的形式,并且提供了 IoC 容器所应该遵循的最基本服务契约,同时也是我们使用 IoC 容器所应该遵守的最底层和最基本的编程规范,这些接口方法定义勾画出了 IoC 容器的基本轮廓。而在 Spring 的的代码实现中,BeanFactory 只是一个接口类,而后面的 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext 等都可以看成是容器附加了某种功能的实现。
    • BeanFactory 中定义了 getBean 方法,其参数类型有 Bean 的名字和其他参数,这些都是对 IoC 容器中存在的 Bean 进行索引。同时还定义了其他方法,方法的目的可以通过名字很明显的看出来,这里就不一一说明了。
    • 可以看到,这里定义的只是接口方法,而这一系列的接口,使用不同的 Bean 的检索方法,很方便的从 IoC 容器中得到所需要的 Bean,从这个角度来看,这些方法代表的是最基本的 IoC 容器入口。
  • BeanFacory 的设计原理

    • BeanFactory 接口提供了使用 IoC 容器的基本规范,而在这个规范之上, Spring 还提供了符合这个 IoC 容器接口的一系列容器的实现来供开发人员使用,下面我们以 XmlBeanFactory 为例来简单说明一下 IoC 容器的实现原理。


      XmlBeanFactory设计的类继承关系
    • 可以看到,作为最简单 IoC 容器系列最底层实现的 XmlBeanFactory,与我们 Spring 应用中用到的那些上下文相比,有一个明显的特点:它只提供最基本的 IoC 容器的功能,理解这一点有助于帮我我们理解 ApplicationContext 和 BeanFactory 之间的区别与联系。

    • XmlBeanFactory 在继承 DafaultListableBeanFactory 类的基础上,新增了新的功能。从类的名字上可以看出应该是与读取 xml 配置文件相关的 IoC 容器。那么该读取过程是怎么实现的?

      • 我们先来看一下 XmlBeanFactory 的源码:
      @Deprecated
      public class XmlBeanFactory extends DefaultListableBeanFactory {
      
          private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
          
          public XmlBeanFactory(Resource resource) throws BeansException {
              this(resource, null);
          }   
          public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
              super(parentBeanFactory);
              this.reader.loadBeanDefinitions(resource);
          }
      }
      
      • 可以看出,在 XmlBeanFactory 中,初始化了一个 XmlBeanDefinitionReader 对象,实际上,该对象就是处理以 XML 方式定义的 BeanDefinition 的地方。
      • 在构造 XmlBeanFactory 这个 IoC 容器,需要指定 BeanDefinition 的信息来源,而这个来源需要封装成 Spring 中的 Resource 类(Spring 用来封装 I/O 操作的类)。将 Resource 作为构造参数传递给 XmlBeanFactory 构造函数。这样, IoC 容器就可以方便的定位到需要的 BeanDefinition 信息来对 Bean 完成容器的初始化和依赖注入的过程。至于如何获取 Resource 和将 Resouce 转化为 BeanDefinition,这是我们后面要提的重点。
    • 我们看到 XmlBeanFactory 使用 DefaultListableBeanFactory 作为基类,该类非常重要,是我们经常用到的一个 IoC 容器的实现,必须在设计应用上下文 ApplicationContext 就会用到它,我们可以看到这个类包含了基本 IoC 容器所具有的重要功能,也是在很多地方都会用到的容器系列中的一个基本产品,其他很多的 IoC 容器都是通过持有和扩展 DefaultListableBeanFactory 来获得特性功能的 IoC 容器的。

    • 参考 XmlBeanFactory 的实现,我们使用编程的方式来使用 DefaultListableBeanFactory。从中我们可以看到 IoC 容器使用的一些基本过程,尽管实际开发中我们很少用到这种方式,但这对于我们理解 IoC 容器的工作原理是非常有帮助的。因为在这个编程式使用容器的过程中,很清楚的揭示了 IoC 容器实现中的那些关键类(Resource、DefaultListableBeanFactory、BeanDefinitionReader)之间的相互关系:

      // 编程式使用容器的代码:
      ClassPathResource res = new ClassPathResource("bean.xml") ;
      DefaultListableBeanFactory factory = new DefaultListableBeanFactory() ;
      XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory) ;
      reader.loadBeanDefinitions(res) ;
      

      通过上面的代码,我们可以看到使用 IoC 容器大概有以下四个步骤:

      1. 创建 IoC 配置文件的 Resource 资源,这个资源包含了 BeanDefinition 的定义信息。
      2. 创建一个 BeanFactory,这里使用 DefaultListableBeanFactory。
      3. 创建一个载入 BeanDefinition 的解读器,这里使用 XmlBeanDefinitionReader 来载入 XML 文件形式 BeanDefinition,通过一个回调配置给 factory。
      4. 从定义好的资源位置读入配置信息,具体的解析过程由 XmlBeanDefinitionReader 的 loadBeanDefinitions() 方法来完成。完成整个载入和注册 Bean 定义之后,需要的 IoC 容器就建立起来可以直接使用了。
  • ApplicationContext 的应用场景:

    • 在 Spring 中,系统已经为用户提供了很多定义好的容器实现。而相比那些简单扩展 BeanFactory 的基本 IoC 容器,开发人员常使用的 ApplicationContext 除了能够提供前面容器介绍的基本功能之外,还为用户提供了以下附加服务,可以让用户更加方便的使用。所以说 ApplicationContext 是一个高级形态的 IoC 容器,如下图所示可以看到 ApplicationContext 在 BeanFactory 的基础上所提供的附加服务:


      ApplicationContext 类继承关系
      • 支持不同的信息源。我们看到 ApplicationContext 扩展了 MessageSource 接口,这些信息源的扩展功能可以支持国际化的实现,为开发多语言版本的应用提供服务。
      • 访问资源。这一点体现在对 ResourcePatternResolver 的继承上。这样我们可以从不同的地方得到 Bean 定义资源。这种抽象使用户程序可以灵活地定义 Bean 信息,尤其是从不同的 I/O 途径得到 Bean 的定义信息。关于 Resource 在 IoC 容器中的使用,我们后面会详细介绍。
      • 支持应用事件。继承了接口 ApplicationEventPublisher,从而在上下文引入了事件机制,这些事件和 Bean 的生命周期的结合为 Bean 的管理提供了便利。
      • 在 ApplicationContext 中提供的附加服务,使得基本 IoC 容器的功能更加丰富,对它的使用是一种面向框架的使用风格,所以建议在开发应用时使用 ApplicationContext 作为 IoC 容器的基本形式
  • ApplicationContext 的设计原理:

    • 我们通过 FileSystemXmlApplicationContext 为例子来分析 ApplicationContext 的设计原理。
    • 作为具体的应用上下文,我们只需要关注和它自身设计相关的功能,而其主要功能已经在其基类 AbstractXmlApplicationContext 中实现了。
      • 一个功能是启动 IoC 容器的 refresh() 过程:
      public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
          throws BeansException {
      
          super(parent);
          setConfigLocations(configLocations);
          if (refresh) {
              refresh();
          }
      }
      
      这个 refresh() 涉及到 IoC 容器启动时候一系列复杂的操作,而不同的容器,其操作都是类似的,因此将其封装在基类 AbstractApplicationContext 中。在 FileSystemXmlApplicationContext 中仅仅涉及到简单的调用而已。关于 refresh() 在 IoC 容器启动时的表现,该方法可以说是所有 IoC 容器的入口,这是一个很重要的方法,我们将在后面具体分析。
      • 另外一个功能是与这个独特的应用上下文的设计有关,即从文件系统中加载 XML 的 Bean 定义资源。
      protected Resource getResourceByPath(String path) {
          if (path != null && path.startsWith("/")) {
              path = path.substring(1);
          }
          return new FileSystemResource(path);
      }
      
      可以看到,通过调用这个方法,可以得到 FileSystemResouce 的资源定位。

到此为止,我们大概了解了 IoC 容器的概念跟 Spring 中对于 IoC 容器的设计跟应用,之后我们要详细的分析 IoC 容器的初始化、依赖注入跟其他相关特征的设计与实现等。

推荐阅读更多精彩内容