前言
前面对 Spring 有过了解,但局限于对网上资料的拼凑。
这次根据资料读了一下源码,把 Spring 的核心组件 Core,Context,Bean 的基础逻辑捋了一下,然后把基本功能拷贝出来,实现了一个可运行的、基本的、几乎没有考虑过其它复杂环境的 IOC 容器。
源码在 https://github.com/hqweay/Autumn-From-Spring
参考 https://github.com/spring-pro…,版本 Spring Framework 5.1.9
。
优点在于代码完全按照 Spring 源码的目录结构组织,每一个方法和属性都能在对应位置的 Spring 源码中找到,如果有疑问,可以随时回到源码查看。
对于我这样的初学者来说,可以不必深陷源码的复杂和细节,对 Spring IOC 的工作原理有个简单理解。
由于 Spring 源码的复杂,我对大多 Java 文件采取保留接口,抽象类,具体实现类的原则。有时候代码依赖一个外部文件的方法,但是该文件在 IOC 运作中的参与度并不大,我就把它整合进了使用到它的代码块。不过该方法的来源以及功能都大致有说明。
开始
问题:Spring 的核心组件有哪些?
答:Core、Context 和 Bean。
问题:这三个为啥是核心组件。
既然叫作核心组件,那就说明这三个组件实现了 Spring 的核心功能,那实现了啥核心功能?这就要从 Spring 解决了啥问题来讲了。
注意,「解决了什么问题」和「能做什么事」是两个概念。促使一样东西的产生,最开始可能是为了解决某个痛点。但是这样东西能做的事则依赖于它的功能以及使用者的想象力了。
但是搜索资料时,提到 Spring,往往就拿 Spring MVC 来讲。要注意两者的区别与联系。
Spring 的提出是为了解决 EJB 的臃肿。具体这里不表。
Spring 能做的事则是简化任何 Java 应用的开发。(不仅仅是一个简化 JavaEE 开发的轻量框架。)
怎么简化呢?
利用建立在这三个核心组件上的强大的 Spring 生态。
这三个组件提供了一个 IOC 容器。
注意 IOC 与 DI(依赖注入)的区别。虽然一般来说这两个指的是一个东西,但较真一下的话。
IOC 指的是一种思想,即 依赖倒转原则。而 DI 是 IOC 的一种实现思路。
IOC 有两种实现思路,还有一种是 DL(依赖查找),不过现在已经没用了。
那么,Spring 为什么会需要这样一个 IOC 容器呢?
可以这样简单理解--大多数时候,我们其实只关心某个对象提供的某个服务,并不关心对象从何而来。
比如写 CRUD,我们其实只需要 getObject(),deleteObject() 这样的功能,并不关心从哪得到一个 Mapper 帮我们干事…
而 IOC 容器就解耦了这种关联,帮我们维护对象,让用户可以不用关心对象的来源,不关心对象间的联系。
具体的可以从设计模式中的工厂模式开始理解。
怎么做到的?
这就要从三个核心组件说起了。
三个组件提供 IOC 容器
Core 组件
Core 在目录 org.springframework.core 下。
Core 提供了很多东西,比如一些工具类。
而其中一个重要的组成部分则是 定义资源的访问方式 。
Core 的核心是两类 Java 文件。一类是 Resource,定义了资源类型。
比如有 UrlResource,它维护一个 URL
属性,指的就是通过 url 获取到的资源。
比如有 ClassPathResource,它指的是以类所在的文件夹(src)向上查找到的 resource 文件夹下的资源文件。(就相当于通过相对路径查找)
对应的有 FileSystemResource,指的就是通过绝对路径获取到的资源文件。
另一类则是 ResourceLoader 文件。
这类文件定义了加载 Resource 的策略。
Spring 里需要 Resource 对象时,是通过 ResourceLoader 的 getResource(String location) 来获取的。
当通过一个 path 来尝试获取 Resource 时,会使用一个 ResourceResolver 来对路径进行分析。
比如 path 可能是一个正则表达式…解析工作就在 ResourceResolver 中。
而在 IOC 容器中,Resource 提供的主要功能是获取配置文件,把它们转为 Resource,方便内部统一管理。
比如我们通常在 resource 文件夹下放我们定义的 beans 的 xml 文件,实际上这个配置文件也可以从远程获取嘛。
Bean 组件
获取到配置文件后,则通过 BeanDefinitionReader 对配置文件(Resource)进行解析,解析后的 bean 被封装在 BeanDefinition 这个对象里,然后 BeanDefinition 又被放在一个注册机 BeanDefinitionRegistry 里。
Bean 组件又提供了 BeanFactory,它会从 BeanDefinitionRegistry 里取出 BeanDefinition,然后根据 BeanDefinition 的信息,创建真正的实例。
这里的 BeanFactory 就是一个 IOC 容器,但是它是提供给 Spring 的开发者使用的。
面向真正的用户的 IOC 容器是 ApplicationContext。
经常有这样的问题,ApplicationContext 与 BeanFactory 的区别是啥?
ApplicationContext 面向用户,BeanFactory 面向 Spring 的开发者。
BeanFactory 在获取 Bean 时是冷加载(只有用到一个 Bean 时才会去实例化),而 ApplicationContext 则在容器启动时,一次性创建所有 bean。这样,在容器创建时我们就能发现配置文件里的某些错误,而不是到需要获取 bean 时才报错。
BeanFactory 和 ApplicationContext 都支持 BeanPostProcessor、BeanFactoryPostProcessor。不过 BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册。
如上,Bean 的核心是 BeanDefinition、BeanDefinitionReader、BeanDefinitionRegistry 、BeanFactory。
可以说,BeanDefinition 是具体 bean 的封装,存储的就是配置文件中的各项信息。
首先,它存储着配置文件中,bean 的属性。
值得一提的是,属性可能为普通的 Java 基本属性,也有可能是 List、Map 等。还有可能是另一个 bean。
当属性为另一个 bean 时,就有可能出现循环引用的问题。
留个疑问,怎么解决循环依赖问题?
其次,BeanDefinition 则还维护着作用域(sccope)等信息。
BeanDefinitionReader 做的就是对配置文件里的各种配置项进行解析。
比如 XmlBeanDefinitionReader 就是专门对 xml 配置文件进行解析。
不过不管用哪种方式配置,配置项中的属性都是一致的,比如 bean,property,ref…
这些项是提取出来单独存放的。
BeanDefinitionRegistry 本质是一个 ConcurrentHashMap,起着在解析配置文件时暂存 BeanDefinition 的作用。
就像在上面 BeanFactory 与 ApplicationContext 的区别中说的,当需要 bean 时,才去 BeanDefinitionRegistry 取来 BeanDefinition 信息,然后按照配置实例化对象。
不过,BeanFactory 也实现了 BeanDefinitionRegistry 接口。
也就是说,BeanFactory 其实也可以当作 BeanDefinitionRegistry 用。
实际上,解析后的 BeanDefinition 都是存放在 BeanFactory 里的。
Context 组件
context 有上下文的意思,顾名思义,它为 Spring 提供了一个运行时的环境,用于保存各个对象的状态。
Context 的核心是 ApplicationContext,ApplicationContext 接口继承自 BeanFactory。
在 ApplicationContext 的抽象实现类 AbstractApplicationContext 中就维护了一个 DefaultListableBeanFactory。
在研究 Spring IOC 的启动流程时不是会遇到一个 refresh() 方法吗,在那个方法里会解析加载创建 bean,最后的 bean 就是保存在这里的。
面向用户的一个 ApplicationContext 就是 ClassPathXmlApplicationContext。
在 Spring MVC 里则有 WebApplicationContext,使用户可以在 Web 环境访问 Spring 的上下文。通过 WebApplicationContext,可以访问 :
- ServletContext Web 服务器提供
- request, session, globalsession
- HttpServletRequest 服务器获取客户端数据
- HttpServletResponse 服务器向客户端传递数据