写点什么

Spring 之控制反转

作者:andy
  • 2022-10-27
    北京
  • 本文字数:5383 字

    阅读完需:约 18 分钟

一、工厂设计模式的分析


在传统的项目开发中,业务对象的调用需先要使用关键字 new 进行对象创建。但是,因过多的强引用对象的产生,会造成 JVM 中实例对象过多,无法进行垃圾回收。同时,使用关键字 new 会造成代码耦合。因此,已不是很好的开发方式。



图 传统开发方式


随著不断发展,工厂设计模式的产生,可以通过工厂模式,将对象创建交由工厂完成,减少过多使用关键字 new 创建实例对象,极大方便了调用者。



图 工厂设计模式


对象的调用需要通过工厂进行,也产生了一些问题:


1、开发者必须关注工厂类;

2、所有对象的调用需要经过明确的工厂类实现。


二、IoC(Inversion of Control)


第一,大量使用关键字 new 实例化对象,增加过多的强引用,会容易引起 OOM;

第二,通过工厂设计模式实例化对象,需要花费大量的精力去维护工厂。

基于以上问题,Spring 提出了控制反转的解决方案。

控制反转,也就是把对象的创建、初始化、销毁、依赖注入交给 spring 容器来管理,而不是由开发者控制,实现控制反转。对象与对象之间的关系,具体可以放到可配置的 XML 文件里。至于文件,则可以放到 classpath,filesystem、URL 网络资源,或者 servletContext 下。org.springframework.beans 和 org.springframework.context 包是 Spring Framework 的 IoC 容器的基础。


三、Spring IOC 体系结构


1、BeanFactory


BeanFactory 接口提供了一种能够管理任何类型的对象的高级配置机制。 BeanFactory 作为最顶层的一个接口类,它定义了 IOC 容器的基本功能规范。


定义方法:


public <T> T getBean(java.lang.String name,java.lang.Class<T> requiredType)throws BeansException:

获取 Bean 实例化对象

public boolean containsBean(java.lang.String name):

是否包含 Bean

public boolean isSingleton(java.lang.String name)throws NoSuchBeanDefinitionException:

是否单例设计模式

public boolean isPrototype(java.lang.String name)throws NoSuchBeanDefinitionException:

是否原型



BeanFactory 有三个子接口:ListableBeanFactory、HierarchicalBeanFactory 和 AutowireCapableBeanFactory。默认实现类是 DefaultListableBeanFactory,它实现了所有的接口。


ListableBeanFactory 接口表示这些 Bean 是可列表的,而 HierarchicalBeanFactory 表示的是这些 Bean 是有继承关系的,也就是每个 Bean 有可能有父 Bean。AutowireCapableBeanFactory 接口定义 Bean 的自动装配规则。这四个子接口共同定义了 Bean 的集合、Bean 之间的关系以及 Bean 行为。


工厂具体是如何产生对象的,需要看具体的 IOC 容器实现,spring 提供了许多 IOC 容器的实现。比如 XmlBeanFactory,ClasspathXmlApplicationContext 等。其中 XmlBeanFactory 就是针对最基本的 ioc 容器的实现,这个 IOC 容器可以读取 XML 文件定义的 BeanDefinition(XML 文件中对 bean 的描述)。


2、BeanDefinition


IOC 容器管理 Bean 和 Bean 之间的管理,Bean 在 Spring 的实现是以 BeanDefinition 描述的。Bean 的解析过程非常复杂,功能被分的很细,因为这里需要被扩展的地方很多,必须保证有足够的灵活性,以应对可能的变化。Bean 的解析主要就是对 Spring 配置文件的解析。


3、ApplicationContext


ApplicationContext 是 Spring 提供的一个高级的 IoC 容器,是 BeanFactory 的子接口,它除了能够提供 IoC 容器的基本功能外,还为用户提供了以下的附加服务:

1,支持信息源,可以实现国际化。(实现 MessageSource 接口)

2,访问资源。(实现 ResourcePatternResolver 接口)

3,支持应用事件。(实现 ApplicationEventPublisher 接口)



web 项目中使用的 XmlWebApplicationContext,ClasspathXmlApplicationContext 都是 ApplicationContext 体系下的子类。


ApplicationContext 允许上下文嵌套,通过保持父上下文可以维持一个上下文体系。对于 bean 的查找可以在这个上下文体系中发生,首先检查当前上下文,其次是父上下文,逐级向上,这样为不同的 Spring 应用提供了一个共享的 bean 定义环境。SpringMVC 的容器便是基于此。


四、IOC 容器初始化


IoC 容器的初始化包括 BeanDefinition 的 Resource 定位、载入和注册这三个基本的过程。


1、Resource 定位


Resource 定位就是对于用户定义好的 Bean 资源进行定位,一般使用外部资源来描述 Bean 对象,所以 IOC 容器需要定位 Resource 外部资源。由资源加载器 ResourceLoader 通过统一的 Resource 接口来完成的,这个 Resource 对各种形式的 BeanDefinition 使用都提供了统一接口。而容器 AbstractApplicationContext 抽象类又是 ResourceLoader 的子类。


2、载入


载入指的就是将用户定义好的 Bean 表示成 IOC 容器的内部数据结构,也就是 BeanDefinition。通过 BeanDefinitionReader 读取和解析 Resource 定位的资源。在 IOC 容器内部维护着一个 BeanDefinition Map 的数据结构,通过这样的数据结构,IOC 容器能够对 Bean 进行更好的管理。在配置文件中每一个都对应着一个 BeanDefinition 对象。


1)、AbstractApplicationContext 对 Bean 定义资源的载入是从 refresh()函数开始的

2)、AbstractRefreshableApplicationContext 子类的 loadBeanDefinitions 方法加载 BeanDefinition

3)、DocumentLoader 将 Bean 定义资源转换为 Document 对象


3、注册


注册,即向 IOC 容器注册 BeanDefinition,这个过程是通过 BeanDefinitionRegistery 接口来实现的。向 IOC 容器注册这些 BeanDefinition 的过程,这个过程就是将前面解析得到的 BeanDefition 保存到 HashMap 中的过程,IOC 容器通过这个 HashMap 来维护 BeanDefinition。

1)、DefaultBeanDefinitionDocumentReader 对 Bean 定义的 Document 对象解析

2)、DefaultListableBeanFactory 向 IoC 容器注册解析后的 BeanDefinition

经过 Resource 定位、载入、注册三个步骤,IOC 容器的初始化过程就已经完成了。


上面提到的过程一般是不包括 Bean 的依赖注入的实现,Bean 的载入和依赖注入是两个独立的过程,依赖注入是发生在应用第一次调用 getBean 向容器所要 Bean 时。


当然我们可以通过设置预处理,即对某个 Bean 设置 lazyinit 属性,那么这个 Bean 的依赖注入就会在容器初始化的时候完成。


五、容器启动原理


web 环境下 Spring 容器、SpringMVC 容器启动过程:

Spring 应用的 IOC 容器通过 tomcat 的 Servlet 或 Listener 监听启动加载;Spring MVC 的容器由 DispatchServlet 作为入口加载;Spring 容器是 Spring MVC 容器的父容器。



详细解读过程:


首先,对于一个 web 应用,其部署在 web 容器中,web 容器提供其一个全局的上下文环境,这个上下文就是 ServletContext,其为后面的 spring IoC 容器提供宿主环境;


其次,在 web.xml 中会提供有 contextLoaderListener(或 ContextLoaderServlet)。在 web 容器启动时,会触发容器初始化事件,此时 contextLoaderListener 会监听到这个事件,其 contextInitialized 方法会被调用,在这个方法中,spring 会初始化一个启动上下文,这个上下文被称为根上下文,即 WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是 XmlWebApplicationContext。这个就是 spring 的 IoC 容器,其对应的 Bean 定义的配置由 web.xml 中的 context-param 标签指定。在这个 IoC 容器初始化完毕后,spring 容器以 WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE 为属性 Key,将其存储到 ServletContext 中,便于获取;




再次,contextLoaderListener 监听器初始化完毕后,开始初始化 web.xml 中配置的 Servlet,这个 servlet 可以配置多个,以最常见的 DispatcherServlet 为例(Spring MVC),这个 servlet 实际上是一个标准的前端控制器,用以转发、匹配、处理每个 servlet 请求。DispatcherServlet 上下文在初始化的时候会建立自己的 IoC 上下文容器,用以持有 spring mvc 相关的 bean,这个 servlet 自己持有的上下文默认实现类也是 XmlWebApplicationContext。在建立 DispatcherServlet 自己的 IoC 上下文时,会利用 WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE 先从 ServletContext 中获取之前的根上下文(即 WebApplicationContext)作为自己上下文的 parent 上下文(即第 2 步中初始化的 XmlWebApplicationContext 作为自己的父容器)。有了这个 parent 上下文之后,再初始化自己持有的上下文(这个 DispatcherServlet 初始化自己上下文的工作在其 initStrategies 方法中可以看到,大概的工作就是初始化处理器映射、视图解析等)。初始化完毕后,spring 以与 servlet 的名字相关(此处不是简单的以 servlet 名为 Key,而是通过一些转换)的属性为属性 Key,也将其存到 ServletContext 中,以便后续使用。这样每个 servlet 就持有自己的上下文,即拥有自己独立的 bean 空间,同时各个 servlet 共享相同的 bean,即根上下文定义的那些 bean。



六、容器加载 Bean 原理


BeanDefinitionReader 读取 Resource 所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个 BeanDefinition 对象,并保存到 BeanDefinitionRegistry 中;

容器扫描 BeanDefinitionRegistry 中的 BeanDefinition;调用 InstantiationStrategy 进行 Bean 实例化的工作;使用 BeanWrapper 完成 Bean 属性的设置工作;

单例 Bean 缓存池:Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用 HashMap 实现的缓存器,单实例的 Bean 以 beanName 为键保存在这个 HashMap 中。



Spring 组件按其所承担的角色可以划分为两类:


1)物料组件:Resource、BeanDefinition、PropertyEditor 以及最终的 Bean 等,它们是加工流程中被加工、被消费的组件,就像流水线上被加工的物料;

BeanDefinition:Spring 通过 BeanDefinition 将配置文件中的<bean>配置信息转换为容器的内部表示,并将这些 BeanDefinition 注册到 BeanDefinitionRegistry 中。Spring 容器的后续操作直接从 BeanDefinitionRegistry 中读取配置信息。

2)加工设备组件:ResourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy 以及 BeanWrapper 等组件像是流水线上不同环节的加工设备,对物料组件进行加工处理。

InstantiationStrategy:负责实例化 Bean 操作,相当于 Java 语言中 new 的功能,并不会参与 Bean 属性的配置工作。属性填充工作留待 BeanWrapper 完成

BeanWrapper:继承了 PropertyAccessor 和 PropertyEditorRegistry 接口,BeanWrapperImpl 内部封装了两类组件:(1)被封装的目标 Bean(2)一套用于设置 Bean 属性的属性编辑器;具有三重身份:(1)Bean 包裹器(2)属性访问器 (3)属性编辑器注册表。

PropertyAccessor:定义了各种访问 Bean 属性的方法。

PropertyEditorRegistry:属性编辑器的注册表


详细解读过程:


1、ResourceLoader 从存储介质中加载 Spring 配置信息,并使用 Resource 表示这个配置文件的资源;

2、BeanDefinitionReader 读取 Resource 所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个 BeanDefinition 对象,并保存到 BeanDefinitionRegistry 中;

3、容器扫描 BeanDefinitionRegistry 中的 BeanDefinition,使用 Java 的反射机制自动识别出 Bean 工厂后处理器(实现 BeanFactoryPostProcessor 接口)的 Bean,然后调用这些 Bean 工厂后处理器对 BeanDefinitionRegistry 中的 BeanDefinition 进行加工处理。

主要完成以下两项工作:

1)对使用到占位符的<bean>元素标签进行解析,得到最终的配置值,这意味对一些半成品式的 BeanDefinition 对象进行加工处理并得到成品的 BeanDefinition 对象;

2)对 BeanDefinitionRegistry 中的 BeanDefinition 进行扫描,通过 Java 反射机制找出所有属性编辑器的 Bean(实现 java.beans.PropertyEditor 接口的 Bean),并自动将它们注册到 Spring 容器的属性编辑器注册表中(PropertyEditorRegistry);

4.Spring 容器从 BeanDefinitionRegistry 中取出加工后的 BeanDefinition,并调用 InstantiationStrategy 着手进行 Bean 实例化的工作;

5.在实例化 Bean 时,Spring 容器使用 BeanWrapper 对 Bean 进行封装,BeanWrapper 提供了很多以 Java 反射机制操作 Bean 的方法,它将结合该 Bean 的 BeanDefinition 以及容器中属性编辑器,完成 Bean 属性的设置工作;

6.利用容器中注册的 Bean 后处理器(实现 BeanPostProcessor 接口的 Bean)对已经完成属性设置工作的 Bean 进行后续加工,直接装配出一个准备就绪的 Bean。


七、Spring 容器中的 bean 范围


singleton:bean 范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个 bean 的实例,单例的模式由 bean factory 自身来维护;

prototype:原形范围与单例范围相反,为每一个 bean 请求,提供一个实例;

request:在请求 bean 范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean 会失效并被垃圾回收器回收;

Session:与请求范围类似,确保每个 session 中有一个 bean 的实例,在 session 过期后,bean 会随之失效;

global-session:global-session 和 Portlet 应用相关。当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。如果你想要声明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在 global-session 中。全局作用域与 Servlet 中的 session 作用域效果相同;

application:仅适用于 Web 环境下的 ApplicationContext,表示在 ServletContext 生命周期内会拥有一个单独的实例,即在整个 ServletContext 环境下只会拥有一个实例。


八、lazy-init


对单例范围的 bean 设置是否启动时立即加载,也就是单利模式中饿汉式和懒汉式区别。设置为 true,表示第一次调用时,实例化对象,选择 false,表示容器启动时,实例化对象。Spring 容器默认启动时实例化对象。

该方式只对单例范围起作用。

同时可以在 beans 节点上设置 lazy-init,即 default-lazy-init。


用户头像

andy

关注

还未添加个人签名 2019-11-21 加入

还未添加个人简介

评论

发布
暂无评论
Spring之控制反转_andy_InfoQ写作社区