tiny-spring 分析
tiny-spring 分析
前言
在阅读 Spring 的源代码(依赖注入部分和面向切面编程部分)时遇到不少困惑,庞大的类文件结构、纷繁复杂的方法调用、波诡云谲的多态实现,让自己深陷其中、一头雾水。
后来注意到 code4craft 的 tiny-spring 项目,实现了一个微型的 Spring,提供对 IoC 和 AOP 的最基本支持,麻雀虽小,五脏俱全,对 Spring 的认知清晰了不少。这个微型框架的结构包括文件名、方法名都是参照 Spring 来实现的,对于初读 Spring 的学习者,作为研究 Spring 的辅助工具应该能够受益匪浅。
在研究 tiny-spring 的时候,收获颇多,把对这个微型框架的一些分析写了下来,行文可能有点紊乱。本文结构
- 第一部分 IoC 容器的实现 对应了 tiny-spring 的 step-1 到 step-5 部分,这 5 个 step 实现了基本的 IoC 容器,支持singleton类型的bean,包括初始化、属性注入、以及依赖 Bean 注入,可从 XML 中读取配置,XML 读取方式没有具体深入。
- 第二部分 AOP 容器的实现 对应了 tiny-spring 的 step-6 到 step-9 部分。step-10 中对 cglib 的支持没有分析。这 4 个 step 可以使用 AspectJ 的语法进行 AOP 编写,支持接口代理。考虑到 AspectJ 语法仅用于实现
execution("***")
部分的解析,不是主要内容,也可以使用 Java 的正则表达式粗略地完成,因此没有关注这些细节。参考书目
《Spring 实战》《Spring 技术内幕》
为什么 GitHub 不支持 TOC
IoC 容器的实现
文件结构
Resource
以 Resource
接口为核心发散出的几个类,都是用于解决 IoC 容器中的内容从哪里来的问题,也就是 配置文件从哪里读取、配置文件如何读取 的问题。
类名 | 说明 |
---|---|
Resource |
接口,标识一个外部资源。通过 getInputStream() 方法 获取资源的输入流 。 |
UrlResource |
实现 Resource 接口的资源类,通过 URL 获取资源。 |
ResourceLoader |
资源加载类。通过 getResource(String) 方法获取一个 Resouce 对象,是 获取 Resouce 的主要途径 。 |
注: 这里在设计上有一定的问题,ResourceLoader
直接返回了一个 UrlResource
,更好的方法是声明一个 ResourceLoader
接口,再实现一个 UrlResourceLoader
类用于加载 UrlResource
。
BeanDefinition
以 BeanDefinition
类为核心发散出的几个类,都是用于解决 Bean
的具体定义问题,包括 Bean
的名字是什么、它的类型是什么,它的属性赋予了哪些值或者引用,也就是 如何在 IoC 容器中定义一个 Bean
,使得 IoC 容器可以根据这个定义来生成实例 的问题。
类名 | 说明 |
---|---|
BeanDefinition |
该类保存了 Bean 定义。包括 Bean 的 名字 String beanClassName 、类型Class beanClass 、属性 PropertyValues propertyValues 。根据其 类型 可以生成一个类实例,然后可以把 属性 注入进去。propertyValues 里面包含了一个个 PropertyValue 条目,每个条目都是键值对 String - Object ,分别对应要生成实例的属性的名字与类型。在 Spring 的 XML 中的 property 中,键是 key ,值是 value 或者 ref 。对于 value 只要直接注入属性就行了,但是 ref 要先进行解析。Object 如果是 BeanReference 类型,则说明其是一个引用,其中保存了引用的名字,需要用先进行解析,转化为对应的实际 Object 。 |
BeanDefinitionReader |
解析 BeanDefinition 的接口。通过 loadBeanDefinitions(String) 来从一个地址加载类定义。 |
AbstractBeanDefinitionReader |
实现 BeanDefinitionReader 接口的抽象类(未具体实现 loadBeanDefinitions ,而是规范了 BeanDefinitionReader 的基本结构)。内置一个 HashMap rigistry ,用于保存 String - beanDefinition 的键值对。内置一个 ResourceLoader resourceLoader ,用于保存类加载器。用意在于,使用时,只需要向其 loadBeanDefinitions() 传入一个资源地址,就可以自动调用其类加载器,并把解析到的 BeanDefinition 保存到 registry 中去。 |
XmlBeanDefinitionReader |
具体实现了 loadBeanDefinitions() 方法,从 XML 文件中读取类定义。 |
BeanFactory
以 BeanFactory
接口为核心发散出的几个类,都是用于解决 IoC 容器在 已经获取 Bean
的定义的情况下,如何装配、获取 Bean
实例 的问题。
类名 | 说明 |
---|---|
BeanFactory |
接口,标识一个 IoC 容器。通过 getBean(String) 方法来 获取一个对象 |
AbstractBeanFactory |
BeanFactory 的一种抽象类实现,规范了 IoC 容器的基本结构,但是把生成 Bean 的具体实现方式留给子类实现。IoC 容器的结构:AbstractBeanFactory 维护一个 beanDefinitionMap 哈希表用于保存类的定义信息(BeanDefinition )。获取 Bean 时,如果 Bean 已经存在于容器中,则返回之,否则则调用 doCreateBean 方法装配一个 Bean 。(所谓存在于容器中,是指容器可以通过 beanDefinitionMap 获取 BeanDefinition 进而通过其 getBean() 方法获取 Bean 。) |
AutowireCapableBeanFactory |
可以实现自动装配的 BeanFactory 。在这个工厂中,实现了 doCreateBean 方法,该方法分三步:1,通过 BeanDefinition 中保存的类信息实例化一个对象;2,把对象保存在 BeanDefinition 中,以备下次获取;3,为其装配属性。装配属性时,通过BeanDefinition 中维护的 PropertyValues 集合类,把 String - Value 键值对注入到 Bean 的属性中去。如果 Value 的类型是 BeanReference 则说明其是一个引用(对应于 XML 中的 ref ),通过 getBean 对其进行获取,然后注入到属性中。 |
ApplicationContext
以 ApplicationContext
接口为核心发散出的几个类,主要是对前面 Resouce
、 BeanFactory
、BeanDefinition
进行了功能的封装,解决 根据地址获取 IoC 容器并使用 的问题。
类名 | 说明 |
---|---|
ApplicationContext |
标记接口,继承了 BeanFactory 。通常,要实现一个 IoC 容器时,需要先通过 ResourceLoader 获取一个 Resource ,其中包括了容器的配置、Bean 的定义信息。接着,使用 BeanDefinitionReader 读取该 Resource 中的 BeanDefinition 信息。最后,把 BeanDefinition 保存在 BeanFactory 中,容器配置完毕可以使用。注意到 BeanFactory 只实现了 Bean 的 装配、获取,并未说明 Bean 的 来源 也就是 BeanDefinition 是如何 加载 的。该接口把 BeanFactory 和 BeanDefinitionReader 结合在了一起。 |
AbstractApplicationContext |
ApplicationContext 的抽象实现,内部包含一个 BeanFactory 类。主要方法有 getBean() 和 refresh() 方法。getBean() 直接调用了内置 BeanFactory 的 getBean() 方法,refresh() 则用于实现 BeanFactory 的刷新,也就是告诉 BeanFactory 该使用哪个资源(Resource )加载类定义(BeanDefinition )信息,该方法留给子类实现,用以实现 从不同来源的不同类型的资源加载类定义的效果。 |
ClassPathXmlApplicationContext | 从类路径加载资源的具体实现类。内部通过 XmlBeanDefinitionReader 解析 UrlResourceLoader 读取到的 Resource ,获取 BeanDefinition 信息,然后将其保存到内置的 BeanFactory 中。 |
注 1:在 Spring 的实现中,对 ApplicatinoContext
的分层更为细致。AbstractApplicationContext
中为了实现 不同来源 的 不同类型 的资源加载类定义,把这两步分层实现。以“从类路径读取 XML 定义”为例,首先使用 AbstractXmlApplicationContext
来实现 不同类型 的资源解析,接着,通过 ClassPathXmlApplicationContext
来实现 不同来源 的资源解析。
注 2:在 tiny-spring 的实现中,先用 BeanDefinitionReader
读取 BeanDefiniton
后,保存在内置的 registry
(键值对为 String
- BeanDefinition
的哈希表,通过 getRigistry()
获取)中,然后由 ApplicationContext
把 BeanDefinitionReader
中 registry
的键值对一个个赋值给 BeanFactory
中保存的 beanDefinitionMap
。而在 Spring 的实现中,BeanDefinitionReader
直接操作 BeanDefinition
,它的 getRegistry()
获取的不是内置的 registry
,而是 BeanFactory
的实例。如何实现呢?以 DefaultListableBeanFactory
为例,它实现了一个 BeanDefinitonRigistry
接口,该接口把 BeanDefinition
的 注册 、获取 等方法都暴露了出来,这样,BeanDefinitionReader
可以直接通过这些方法把 BeanDefiniton
直接加载到 BeanFactory
中去。
设计模式
注:此处的设计模式分析不限于 tiny-spring,也包括 Spring 本身的内容
模板方法模式
该模式大量使用,例如在 BeanFactory
中,把 getBean()
交给子类实现,不同的子类 **BeanFactory
对其可以采取不同的实现。
代理模式
在 tiny-spring 中(Spring 中也有类似但不完全相同的实现方式),ApplicationContext
继承了 BeanFactory
接口,具备了 getBean()
功能,但是又内置了一个 BeanFactory
实例,getBean()
直接调用 BeanFactory
的 getBean()
。但是ApplicationContext
加强了 BeanFactory
,它把类定义的加载也包含进去了。
AOP 的实现
重新分析 IoC 容器
注:以下所说的 BeanFactory
和 ApplicationContext
不是指的那几个最基本的接口类(例如 BeanFactory
接口,它除了 getBean
空方法之外,什么都没有,无法用来分析。),而是指这一类对象总体的表现,比如 ClasspathXmlApplicationContext
、FileSystemXmlApplicationContext
都算是 ApplicationContext
。
BeanFactory 的构造与执行
BeanFactory
的核心方法是 getBean(String)
方法,用于从工厂中取出所需要的 Bean
。AbstractBeanFactory
规定了基本的构造和执行流程。
getBean
的流程:包括实例化和初始化,也就是生成 Bean
,再执行一些初始化操作。
doCreateBean
:实例化Bean
。
a.createInstance
:生成一个新的实例。
b.applyProperties
:注入属性,包括依赖注入的过程。在依赖注入的过程中,如果Bean
实现了BeanFactoryAware
接口,则将容器的引用传入到Bean
中去,这样,Bean
将获取对容器操作的权限,也就允许了 编写扩展 IoC 容器的功能的Bean
。initializeBean(bean)
: 初始化Bean
。
a. 从BeanPostProcessor
列表中,依次取出BeanPostProcessor
执行bean = postProcessBeforeInitialization(bean,beanName)
。(为什么调用BeanPostProceesor
中提供方法时,不是直接 post...(bean,beanName) 而是 bean = post...(bean,beanName) 呢?见分析1 。另外,BeanPostProcessor
列表的获取有问题,见分析2。)
b. 初始化方法(tiny-spring 未实现对初始化方法的支持)。
c. 从BeanPostProcessor
列表中, 依次取出BeanPostProcessor
执行其bean = postProcessAfterInitialization(bean,beanName)
。
ApplicationContext 的构造和执行
ApplicationContext
的核心方法是 refresh()
方法,用于从资源文件加载类定义、扩展容器的功能。
refresh
的流程:
loadBeanDefinitions(BeanFactory)
:加载类定义,并注入到内置的BeanFactory
中,这里的可扩展性在于,未对加载方法进行要求,也就是可以从不同来源的不同类型的资源进行加载。registerBeanPostProcessors(BeanFactory)
:获取所有的BeanPostProcessor
,并注册到BeanFactory
维护的BeanPostProcessor
列表去。onRefresh
:
a.preInstantiateSingletons
:以单例的方式,初始化所有Bean
。tiny-spring 只支持singleton
模式。
IoC 实现的一些思考与分析
分析 1:AOP 可以在何处被嵌入到 IoC 容器中去?
在 Bean
的初始化过程中,会调用 BeanPostProcessor
对其进行一些处理。在它的 postProcess...Initialization
方法中返回了一个 Bean
,这个返回的 Bean
可能已经不是原来传入的 Bean
了,这为实现 AOP 的代理提供了可能!以 JDK 提供的动态代理为例,假设方法要求传入的对象实现了 IObj
接口,实际传入的对象是 Obj
,那么在方法中,通过动态代理,可以 生成一个实现了 IObj
接口并把 Obj
作为内置对象的代理类 Proxy
返回,此时 Bean
已经被偷偷换成了它的代理类。
分析 2:BeanFactory 和 ApplicationContext 设计上的耦合
BeanFactory
中的 BeanPostProcessor
的列表是哪里生成的呢?是在 ApplicationContext
中的 refresh
方法的第二步,这里设计上应该有些问题,按理说 ApplicationContext
是基于 BeanFactory
的,BeanFactory
的属性的获取,怎么能依赖于 ApplicationContext
的调用呢?
分析 3:tiny-spring 总体流程的分析
总体来说,tiny-spring 的 ApplicaitonContext
使用流程是这样的:
1. ApplicationContext
完成了类定义的读取和加载,并注册到 BeanFactory
中去。
2. ApplicationContext
从 BeanFactory
中寻找 BeanPostProcessor
,注册到 BeanFactory
维护的 BeanPostProcessor
列表中去。
3. ApplicationContext
以单例的模式,通过主动调用 getBean
实例化、注入属性、然后初始化 BeanFactory
中所有的 Bean
。由于所有的 BeanPostProcessor
都已经在第 2 步中完成实例化了,因此接下来实例化的是普通 Bean
,因此普通 Bean
的初始化过程可以正常执行。
4. 调用 getBean
时,委托给 BeanFactory
,此时只是简单的返回每个 Bean
单例,因为所有的 Bean
实例在第三步都已经生成了。
JDK 对动态代理的支持
JDK 中几个关键的类:
类名 | 说明 |
---|---|
Proxy |
来自 JDK API。提供生成对象的动态代理的功能,通过Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 方法返回一个代理对象。 |
InvocationHandler |
来自 JDK API。通过 Object invoke(Object proxy, Method method,Object[] args) 方法实现代理对象中方法的调用和其他处理。 |
假设以下的情况:
- 对象
obj
实现了IObj
接口,接口中有一个方法func(Object[] args)
。 - 对象
handler
是InvocationHandler
的实例。
那么,通过 Proxy
的 newProxyInstance(obj.getClassLoader(), obj.getClass().getInterfaces(), handler
,可以返回 obj
的代理对象 proxy
。
当调用 proxy.func(args)
时,对象内部将委托给 handler.invoke(proxy, func, args)
函数实现。
因此,在 handler
的 invoke
中,可以完成对方法拦截的处理。可以先判断是不是要拦截的方法,如果是,进行拦截(比如先做一些操作,再调用原来的方法,对应了 Spring 中的前置通知);如果不是,则直接调用原来的方法。
AOP 的植入与实现细节
在 Bean
初始化过程中完成 AOP 的植入
解决 AOP 的植入问题,首先要解决 在 IoC 容器的何处植入 AOP 的问题,其次要解决 为哪些对象提供 AOP 的植入 的问题。
tiny-spring 中 AspectJAwareAdvisorAutoProxyCreator
类(以下简称 AutoProxyCreator
)是实现 AOP 植入的关键类,它实现了两个接口:
BeanPostProcessor
:在postProcessorAfterInitialization
方法中,使用动态代理的方式,返回一个对象的代理对象。解决了 在 IoC 容器的何处植入 AOP 的问题。BeanFactoryAware
:这个接口提供了对BeanFactory
的感知,这样,尽管它是容器中的一个Bean
,却可以获取容器的引用,进而获取容器中所有的切点对象,决定对哪些对象的哪些方法进行代理。解决了 为哪些对象提供 AOP 的植入 的问题。
AOP 中动态代理的实现步骤
动态代理的内容
首先,要知道动态代理的内容(拦截哪个对象、在哪个方法拦截、拦截具体内容),下面是几个关键的类:
类名 | 说明 |
---|---|
PointcutAdvisor |
切点通知器,用于提供 对哪个对象的哪个方法进行什么样的拦截 的具体内容。通过它可以获取一个切点对象 Pointcut 和一个通知器对象 Advisor 。 |
Pointcut |
切点对象可以获取一个 ClassFilter 对象和一个 MethodMatcher 对象。前者用于判断是否对某个对象进行拦截(用于 筛选要代理的目标对象),后者用于判断是否对某个方法进行拦截(用于 在代理对象中对不同的方法进行不同的操作)。 |
Advisor |
通知器对象可以获取一个通知对象 Advice 。就是用于实现 具体的方法拦截,需要使用者编写,也就对应了 Spring 中的前置通知、后置通知、环切通知等。 |
动态代理的步骤
接着要知道动态代理的步骤:
AutoProxyCreator
(实现了BeanPostProcessor
接口)在实例化所有的Bean
前,最先被实例化。- 其他普通
Bean
被实例化、初始化,在初始化的过程中,AutoProxyCreator
加载BeanFactory
中所有的PointcutAdvisor
(这也保证了PointcutAdvisor
的实例化顺序优于普通Bean
。),然后依次使用PointcutAdvisor
内置的ClassFilter
,判断当前对象是不是要拦截的类。 - 如果是,则生成一个
TargetSource
(要拦截的对象和其类型),并取出AutoProxyCreator
的MethodMatcher
(对哪些方法进行拦截)、Advice
(拦截的具体操作),再,交给AopProxy
去生成代理对象。 AopProxy
生成一个InvocationHandler
,在它的invoke
函数中,首先使用MethodMatcher
判断是不是要拦截的方法,如果是则交给Advice
来执行(Advice
由用户来编写,其中也要手动/自动调用原始对象的方法),如果不是,则直接交给TargetSource
的原始对象来执行。
设计模式
代理模式
通过动态代理实现,见分析1中的内容,不再赘述。
策略模式
生成代理对象时,可以使用 JDK 的动态代理和 Cglib 的动态代理,对于不同的需求可以委托给不同的类实现。
为 tiny-spring 添加拦截器链
tiny-spring 不支持拦截器链,可以模仿 Spring 中拦截器链的实现,实现对多拦截器的支持。
tiny-spring 中的 proceed()
方法是调用原始对象的方法 method.invoke(object,args)
。(参见 ReflectiveMethodInvocation
类)
为了支持多拦截器,做出以下修改:
- 将
proceed()
方法修改为调用代理对象的方法method.invoke(proxy,args)
。 - 在代理对象的
InvocationHandler
的invoke
函数中,查看拦截器列表,如果有拦截器,则调用第一个拦截器并返回,否则调用原始对象的方法。
tiny-spring 分析的更多相关文章
- Tiny Spring 分析一
近期一直想看spring的源代码,可是奈何水平太低,庞杂的源代码令我一阵阵的头晕. 非常有幸,在网上看到了黄亿华大神的<<1000行代码读懂Spring(一)- 实现一个主要的IoC容器& ...
- 【转】Spring,Spring MVC及Spring Boot区别
对于一个Java开发者来说,Spring可谓如雷贯耳,无论是Spring框架,还是Spring引领的IOC,AOP风格,都对后续Java开发产生的深远的影响,同时,Spring社区总能及时响应开发者的 ...
- 【TencentOS tiny】又有一个操作系统开源
新闻 2019年9月18日,腾讯宣布将开源 自主研发的轻量级物联网实时操作系统TencentOS tiny.相比市场上其它系统,腾讯TencentOS tiny在资源占用.设备成本.功耗管理以及安全稳 ...
- Spring Boot入门学习
1. Spring Boot概述 1.1.什么是Spring Boot SpringBoot是一个可使用Java构建微服务的微框架.是Spring框架及其社区对"约定优先于配置"理 ...
- Spring:面向切面编程的AOP
一.前言 除了依赖注入(DI),Spring框架提供的另一个核心功能是对面向方面的编程(AOP)的支持. AOP通常被称为实现横切关注点的工具.横切关注点一词是指应用程序中的逻辑不能与应用程序的其余部 ...
- 学习Java Web开发
学习DreamWaveMX中文版的网页设计技术 HTML网页设计,这是最基本的.学习XML的一些基本知识.初步掌握一些JSCRIPT的应用. 学习JAVA语言. 这应该分成2次来进行: 第1次找一本国 ...
- 01-Spring概述(总览)
Spring概述 前言 Spring 发展至现在,俨然成为一个生态,但要理解其余的 Spring Boot.Spring Cloud 等框架,需要先对 Spring 的整个体系有一定的理解,因为其余的 ...
- spring remoting源码分析--Hessian分析
1. Caucho 1.1 概况 spring-remoting代码的情况如下: 本节近分析caucho模块. 1.2 分类 其中以hession为例,Hessian远程服务调用过程: Hessian ...
- spring.net 框架分析(三)ContextRegistry.GetContext()
我们通过ContextRegistry.GetContext()建立了一个IApplicationContext得实例,那么这个实例具体是怎么建立的了. 我们来分析一下容器实例建立的过程: 我们在配置 ...
- 深入理解 spring 容器,源码分析加载过程
Spring框架提供了构建Web应用程序的全功能MVC模块,叫Spring MVC,通过Spring Core+Spring MVC即可搭建一套稳定的Java Web项目.本文通过Spring MVC ...
随机推荐
- linux上查看swf文件.靠谱
在linux上查看swf文件,本来想用gnash 来看,可是有的电脑上看的时候只有声音,没有图像 所以用网页来查看,推荐谷歌 我们在和flash文件的同目录下新建一个文件名为:index.html 注 ...
- 页面预加载loading动画,再载入内容
默认情况下如果网站请求速度慢,所以会有一段时间的空白页面等等,用户体验效果不好,见到很多的页面都有预加载的效果,加载之前先加载一个动画,后台进程继续加载页面内容,当页面内容加载完之后再退出动画显示内容 ...
- 预计阅读时间核心jS代码
<script type="text/javascript"> jq(document).ready(function() { var read_time=jq(&qu ...
- SpringBoot——IDEA使用 Spring Initializer快速创建项目【四】
前言 使用Spring Initializer快速创建项目 步骤 首先肯定是打开我们的IDEA的编辑器呀~ 创建项目 File -> New -> Project Spring Initi ...
- 使用Git管理品优购项目 开始部分
- Java的修饰、继承、接口、抽象类
1.private 修饰属性或者方法,只能在本类中被访问,定义后需要加get()set()方法,这样提高数据的安全性 私有属性虽然不能直接访问,但是其对象 或者 子类对象可以通过公有方法进行设值和获 ...
- cookies , sessionStorage 及 localStorage 的初步的区别
cookies 保存在浏览器中,关闭浏览器后再次打开,任然存在,当然可能存在一定的有效期.(仅限存在同一台电脑,同一个浏览器中) Session依赖Cookie!! sessionStorage ...
- Django 1.11 使用命令makemigrations命令无法执行表修改动作
由于在学习过程中,遇到models模型变动,变动后合并发生问题,故当时做了删除应用文件夹下migrations文件,由于数据库里无较多新数据,故删除后重建,但重建后执行模型合并操作结果为No Chan ...
- python jenkins api
#!/usr/bin/pythonimport sys, timeimport shutil, commands#coding=utf-8 import sysreload(sys)sys.setde ...
- 微信公众平台开发(150)——从新浪云SAE上传图片到图文消息
从新浪云SAE上传图片到图文消息,只能用于图文消息中, 没有个数限制 if (!empty($_FILES['qrcode']['name'])){ $filename = time()." ...