关于java:CRUD搬砖两三年了怎么阅读Spring源码

0次阅读

共计 10154 个字符,预计需要花费 26 分钟才能阅读完成。

作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!😄

👨‍💻连读共事写的代码都吃力,还读 Spring? 咋的,Spring 很难读!

这个与咱们码农朝夕相处的 Spring,就像睡在你身边的媳妇,你晓得找她要吃、要喝、要零花钱、要买皮肤。但你不晓得她的仓库共有多少存粮、也不晓得她是买了理财还是存了银行。🍑开个玩笑,接下来我要正经了!


一、为什么 Spring 难读懂?

为什么 Spring 天天用,但要想去读一读源码,怎么就那么难!因为由Java 和 J2EE 开发畛域的专家 Rod Johnson 于 2002 年提出并随后创立的 Spring 框架,随着 JDK 版本和市场须要倒退至今,至今它曾经越来越大了!

当你浏览它的源码你会感觉:

  1. 怎么这代码跳来跳去的,基本不是像本人写代码一样那么 单纯
  2. 为什么那么多的接口和接口继承,类 A 继承的类 B 还实现了类 A 实现的接口 X
  3. 简略工厂、工厂办法、代理模式、观察者模式,怎么用了会有这样多的设计模式应用
  4. 又是资源加载、又是利用上下文、又是 IOC、又是 AOP、贯通的还有 Bean 的申明周期,一片一片的代码从哪下手

怎么 ,这就是你在浏览 Spring 遇到的一些列问题吧?其实不止你甚至能够说只有是从事这个行业的码农,想读 Spring 源码都会有种不晓得从哪下手的感觉。所以我想了个方法,既然 Spring 太大不好理解,那么我就尝试从一个小的 Spring 开始, 手撸 实现一个 Spring 是不能够了解的更好,别说成果还真不错, 在花了将近 2 个月的工夫,实现一个简略版本的 Spring 后 当初对 Spring 的了解,有了很大的晋升,也能读懂 Spring 的源码了。

二、分享手撸 Spring

通过这样手写简化版 Spring 框架,理解 Spring 外围原理。在手写的过程中会简化 Spring 源码,摘取整体框架中的外围逻辑,简化代码实现过程,保留外围性能,例如:IOC、AOP、Bean 生命周期、上下文、作用域、资源解决等内容实现。

源码:https://github.com/fuzhengwei/small-spring

1. 实现一个简略的 Bean 容器

但凡能够存放数据的具体数据结构实现,都能够称之为容器。例如:ArrayList、LinkedList、HashSet 等,但在 Spring Bean 容器的场景下,咱们须要一种能够用于寄存和名称索引式的数据结构,所以抉择 HashMap 是最合适不过的。

这里简略介绍一下 HashMap,HashMap 是一种基于扰动函数、负载因子、红黑树转换等技术内容,造成的拉链寻址的数据结构,它能让数据更加散列的散布在哈希桶以及碰撞时造成的链表和红黑树上。它的数据结构会尽可能最大限度的让整个数据读取的复杂度在 O(1) ~ O(Logn) ~O(n)之间,当然在极其状况下也会有 O(n) 链表查找数据较多的状况。不过咱们通过 10 万数据的扰动函数再寻址验证测试,数据会平均的散列在各个哈希桶索引上,所以 HashMap 非常适合用在 Spring Bean 的容器实现上。

另外一个简略的 Spring Bean 容器实现,还需 Bean 的定义、注册、获取三个根本步骤,简化设计如下;

  • 定义:BeanDefinition,可能这是你在查阅 Spring 源码时常常看到的一个类,例如它会包含 singleton、prototype、BeanClassName 等。但目前咱们初步实现会更加简略的解决,只定义一个 Object 类型用于寄存对象。
  • 注册:这个过程就相当于咱们把数据寄存到 HashMap 中,只不过当初 HashMap 寄存的是定义了的 Bean 的对象信息。
  • 获取:最初就是获取对象,Bean 的名字就是 key,Spring 容器初始化好 Bean 当前,就能够间接获取了。

2. 使用设计模式,实现 Bean 的定义、注册、获取

将 Spring Bean 容器欠缺起来,首先十分重要的一点是在 Bean 注册的时候只注册一个类信息,而不会间接把实例化信息注册到 Spring 容器中。那么就须要批改 BeanDefinition 中的属性 Object 为 Class,接下来在须要做的就是在获取 Bean 对象时须要解决 Bean 对象的实例化操作以及判断以后单例对象在容器中是否曾经缓存起来了。整体设计如图 3-1

  • 首先咱们须要定义 BeanFactory 这样一个 Bean 工厂,提供 Bean 的获取办法 getBean(String name),之后这个 Bean 工厂接口由抽象类 AbstractBeanFactory 实现。这样应用模板模式的设计形式,能够对立收口通用外围办法的调用逻辑和规范定义,也就很好的管制了后续的实现者不必关怀调用逻辑,依照对立形式执行。那么类的继承者只须要关怀具体方法的逻辑实现即可。
  • 那么在继承抽象类 AbstractBeanFactory 后的 AbstractAutowireCapableBeanFactory 就能够实现相应的形象办法了,因为 AbstractAutowireCapableBeanFactory 自身也是一个抽象类,所以它只会实现属于本人的形象办法,其余形象办法由继承 AbstractAutowireCapableBeanFactory 的类实现。这里就体现了类实现过程中的各司其职,你只须要关怀属于你的内容,不是你的内容,不要参加。
  • 另外这里还有块十分重要的知识点,就是对于单例 SingletonBeanRegistry 的接口定义实现,而 DefaultSingletonBeanRegistry 对接口实现后,会被抽象类 AbstractBeanFactory 继承。当初 AbstractBeanFactory 就是一个十分残缺且弱小的抽象类了,也能十分好的体现出它对模板模式的形象定义。

3. 基于 Cglib 实现含构造函数的类实例化策略

填平这个坑的技术设计次要思考两局部,一个是串流程从哪正当的把构造函数的入参信息传递到实例化操作里,另外一个是怎么去实例化含有构造函数的对象。

  • 参考 Spring Bean 容器源码的实现形式,在 BeanFactory 中增加 Object getBean(String name, Object... args) 接口,这样就能够在获取 Bean 时把构造函数的入参信息传递进去了。
  • 另外一个外围的内容是应用什么形式来创立含有构造函数的 Bean 对象呢?这里有两种形式能够抉择,一个是基于 Java 自身自带的办法 DeclaredConstructor,另外一个是应用 Cglib 来动态创建 Bean 对象。Cglib 是基于字节码框架 ASM 实现,所以你也能够间接通过 ASM 操作指令码来创建对象

4. 为 Bean 对象注入属性和依赖 Bean 的性能实现

鉴于属性填充是在 Bean 应用 newInstance 或者 Cglib 创立后,开始补全属性信息,那么就能够在类 AbstractAutowireCapableBeanFactory 的 createBean 办法中增加补全属性办法。这部分大家在实习的过程中也能够对照 Spring 源码学习,这里的实现也是 Spring 的简化版,后续对照学习会更加易于了解

  • 属性填充要在类实例化创立之后,也就是须要在 AbstractAutowireCapableBeanFactory 的 createBean 办法中增加 applyPropertyValues 操作。
  • 因为咱们须要在创立 Bean 时候填充属性操作,那么就须要在 bean 定义 BeanDefinition 类中,增加 PropertyValues 信息。
  • 另外是填充属性信息还包含了 Bean 的对象类型,也就是须要再定义一个 BeanReference,外面其实就是一个简略的 Bean 名称,在具体的实例化操作时进行递归创立和填充,与 Spring 源码实现一样。Spring 源码中 BeanReference 是一个接口

5. 设计与实现资源加载器,从 Spring.xml 解析和注册 Bean 对象

按照本章节的需要背景,咱们须要在现有的 Spring 框架雏形中增加一个资源解析器,也就是能读取 classpath、本地文件和云文件的配置内容。这些配置内容就是像应用 Spring 时配置的 Spring.xml 一样,外面会包含 Bean 对象的形容和属性信息。在读取配置文件信息后,接下来就是对配置文件中的 Bean 形容信息解析后进行注册操作,把 Bean 对象注册到 Spring 容器中。整体设计构造如下图:

  • 资源加载器属于绝对独立的局部,它位于 Spring 框架外围包下的 IO 实现内容,次要用于解决 Class、本地和云环境中的文件信息。
  • 当资源能够加载后,接下来就是解析和注册 Bean 到 Spring 中的操作,这部分实现须要和 DefaultListableBeanFactory 外围类联合起来,因为你所有的解析后的注册动作,都会把 Bean 定义信息放入到这个类中。
  • 那么在实现的时候就设计好接口的实现层级关系,包含咱们须要定义出 Bean 定义的读取接口 BeanDefinitionReader 以及做好对应的实现类,在实现类中实现对 Bean 对象的解析和注册。

6. 设计与实现资源加载器,从 Spring.xml 解析和注册 Bean 对象

为了能满足于在 Bean 对象从注册到实例化的过程中执行用户的自定义操作,就须要在 Bean 的定义和初始化过程中插入接口类,这个接口再有内部去实现本人须要的服务。那么在联合对 Spring 框架上下文的解决能力,就能够满足咱们的指标需要了。整体设计构造如下图:

  • 满足于对 Bean 对象扩大的两个接口,其实也是 Spring 框架中十分具备重量级的两个接口:BeanFactoryPostProcessBeanPostProcessor,也简直是大家在应用 Spring 框架额定新增开发本人组建需要的两个必备接口。
  • BeanFactoryPostProcessor,是由 Spring 框架组建提供的容器扩大机制,容许在 Bean 对象注册后但未实例化之前,对 Bean 的定义信息 BeanDefinition 执行批改操作。
  • BeanPostProcessor,也是 Spring 提供的扩大机制,不过 BeanPostProcessor 是在 Bean 对象实例化之后批改 Bean 对象,也能够替换 Bean 对象。这部分与前面要实现的 AOP 有着亲密的关系。
  • 同时如果只是增加这两个接口,不做任何包装,那么对于使用者来说还是十分麻烦的。咱们心愿于开发 Spring 的上下文操作类,把相应的 XML 加载、注册、实例化以及新增的批改和扩大都交融进去,让 Spring 能够主动扫描到咱们的新增服务,便于用户应用。

7. 实现利用上下文,自动识别、资源加载、扩大机制

可能面对像 Spring 这样宏大的框架,对外裸露的接口定义应用或者 xml 配置,实现的一系列扩展性操作,都让 Spring 框架看上去很神秘。其实对于这样在 Bean 容器初始化过程中额定增加的解决操作,无非就是事后执行了一个定义好的接口办法或者是反射调用类中 xml 中配置的办法,最终你只有依照接口定义实现,就会有 Spring 容器在解决的过程中进行调用而已。整体设计构造如下图:

  • 在 spring.xml 配置中增加 init-method、destroy-method 两个注解,在配置文件加载的过程中,把注解配置一并定义到 BeanDefinition 的属性当中。这样在 initializeBean 初始化操作的工程中,就能够通过反射的形式来调用配置在 Bean 定义属性当中的办法信息了。另外如果是接口实现的形式,那么间接能够通过 Bean 对象调用对应接口定义的办法即可,((InitializingBean) bean).afterPropertiesSet(),两种形式达到的成果是一样的。
  • 除了在初始化做的操作外,destroy-methodDisposableBean 接口的定义,都会在 Bean 对象初始化实现阶段,执行注册销毁办法的信息到 DefaultSingletonBeanRegistry 类中的 disposableBeans 属性里,这是为了后续对立进行操作。这里还有一段适配器的应用,因为反射调用和接口间接调用,是两种形式。所以须要应用适配器进行包装,下文代码解说中参考 DisposableBeanAdapter 的具体实现
    - 对于销毁办法须要在虚拟机执行敞开之前进行操作,所以这里须要用到一个注册钩子的操作,如:Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!"))); 这段代码你能够执行测试,另外你能够应用手动调用 ApplicationContext.close 办法敞开容器。

8. 向虚拟机注册钩子,实现 Bean 对象的初始化和销毁办法

可能面对像 Spring 这样宏大的框架,对外裸露的接口定义应用或者 xml 配置,实现的一系列扩展性操作,都让 Spring 框架看上去很神秘。其实对于这样在 Bean 容器初始化过程中额定增加的解决操作,无非就是事后执行了一个定义好的接口办法或者是反射调用类中 xml 中配置的办法,最终你只有依照接口定义实现,就会有 Spring 容器在解决的过程中进行调用而已。整体设计构造如下图:

  • 在 spring.xml 配置中增加 init-method、destroy-method 两个注解,在配置文件加载的过程中,把注解配置一并定义到 BeanDefinition 的属性当中。这样在 initializeBean 初始化操作的工程中,就能够通过反射的形式来调用配置在 Bean 定义属性当中的办法信息了。另外如果是接口实现的形式,那么间接能够通过 Bean 对象调用对应接口定义的办法即可,((InitializingBean) bean).afterPropertiesSet(),两种形式达到的成果是一样的。
  • 除了在初始化做的操作外,destroy-methodDisposableBean 接口的定义,都会在 Bean 对象初始化实现阶段,执行注册销毁办法的信息到 DefaultSingletonBeanRegistry 类中的 disposableBeans 属性里,这是为了后续对立进行操作。这里还有一段适配器的应用,因为反射调用和接口间接调用,是两种形式。所以须要应用适配器进行包装,下文代码解说中参考 DisposableBeanAdapter 的具体实现
    - 对于销毁办法须要在虚拟机执行敞开之前进行操作,所以这里须要用到一个注册钩子的操作,如:Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!"))); 这段代码你能够执行测试,另外你能够应用手动调用 ApplicationContext.close 办法敞开容器。

9. 定义标记类型 Aware 接口,实现感知容器对象

如果说我心愿拿到 Spring 框架中一些提供的资源,那么首先须要思考以一个什么形式去获取,之后你定义进去的获取形式,在 Spring 框架中该怎么去承接,实现了这两项内容,就能够扩大出你须要的一些属于 Spring 框架自身的能力了。

在对于 Bean 对象实例化阶段咱们操作过一些额定定义、属性、初始化和销毁的操作,其实咱们如果像获取 Spring 一些如 BeanFactory、ApplicationContext 时,也能够通过此类形式进行实现。那么咱们须要定义一个标记性的接口,这个接口不须要有办法,它只起到标记作用就能够,而具体的性能由继承此接口的其余功能性接口定义具体方法,最终这个接口就能够通过 instanceof 进行判断和调用了。整体设计构造如下图:

  • 定义接口 Aware,在 Spring 框架中它是一种感知标记性接口,具体的子类定义和实现能感知容器中的相干对象。也就是通过这个桥梁,向具体的实现类中提供容器服务
  • 继承 Aware 的接口包含:BeanFactoryAware、BeanClassLoaderAware、BeanNameAware 和 ApplicationContextAware,当然在 Spring 源码中还有一些其余对于注解的,不过目前咱们还是用不到。
  • 在具体的接口实现过程中你能够看到,一部分 (BeanFactoryAware、BeanClassLoaderAware、BeanNameAware) 在 factory 的 support 文件夹下,另外 ApplicationContextAware 是在 context 的 support 中,这是因为不同的内容获取须要在不同的包下提供。所以,在 AbstractApplicationContext 的具体实现中会用到向 beanFactory 增加 BeanPostProcessor 内容的 ApplicationContextAwareProcessor 操作,最初由 AbstractAutowireCapableBeanFactory 创立 createBean 时解决相应的调用操作。对于 applyBeanPostProcessorsBeforeInitialization 曾经在后面章节中实现过,如果遗记能够往前翻翻

10. 对于 Bean 对象作用域以及 FactoryBean 的实现和应用

对于提供一个能让使用者定义简单的 Bean 对象,性能点十分不错,意义也十分大,因为这样做了之后 Spring 的生态种子孵化箱就此提供了,谁家的框架都能够在此规范上实现本人服务的接入。

但这样的性能逻辑设计上并不简单,因为整个 Spring 框架在开发的过程中就曾经提供了各项扩大能力的 接茬,你只须要在适合的地位提供一个接茬的解决接口调用和相应的性能逻辑实现即可,像这里的指标实现就是对外提供一个能够二次从 FactoryBean 的 getObject 办法中获取对象的性能即可,这样所有实现此接口的对象类,就能够裁减本人的对象性能了。MyBatis 就是实现了一个 MapperFactoryBean 类,在 getObject 办法中提供 SqlSession 对执行 CRUD 办法的操作 整体设计构造如下图:

  • 整个的实现过程包含了两局部,一个解决单例还是原型对象,另外一个解决 FactoryBean 类型对象创立过程中对于获取具体调用对象的 getObject 操作。
  • SCOPE_SINGLETONSCOPE_PROTOTYPE,对象类型的创立获取形式,次要辨别在于 AbstractAutowireCapableBeanFactory#createBean 创立实现对象后是否放入到内存中,如果不放入则每次获取都会从新创立。
  • createBean 执行对象创立、属性填充、依赖加载、前置后置解决、初始化等操作后,就要开始做执行判断整个对象是否是一个 FactoryBean 对象,如果是这样的对象,就须要再继续执行获取 FactoryBean 具体对象中的 getObject 对象了。整个 getBean 过程中都会新增一个单例类型的判断factory.isSingleton(),用于决定是否应用内存寄存对象信息。

11. 基于观察者实现,容器事件和事件监听器

其实事件的设计自身就是一种观察者模式的实现,它所要解决的就是一个对象状态扭转给其余对象告诉的问题,而且要思考到易用和低耦合,保障高度的合作。

在性能实现上咱们须要定义出事件类、事件监听、事件公布,而这些类的性能须要联合到 Spring 的 AbstractApplicationContext#refresh(),以便于处理事件初始化和注册事件监听器的操作。整体设计构造如下图:

  • 在整个性能实现过程中,依然须要在面向用户的利用上下文 AbstractApplicationContext 中增加相干事件内容,包含:初始化事件发布者、注册事件监听器、公布容器刷新实现事件。
  • 应用观察者模式定义事件类、监听类、公布类,同时还须要实现一个播送器的性能,接管到事件推送时进行剖析解决合乎监听事件接受者感兴趣的事件,也就是应用 isAssignableFrom 进行判断。
  • isAssignableFrom 和 instanceof 类似,不过 isAssignableFrom 是用来判断子类和父类的关系的,或者接口的实现类和接口的关系的,默认所有的类的终极父类都是 Object。如果 A.isAssignableFrom(B)后果是 true,证实 B 能够转换成为 A, 也就是 A 能够由 B 转换而来。

12. 基于 JDK 和 Cglib 动静代理,实现 AOP 外围性能

在把 AOP 整个切面设计交融到 Spring 前,咱们须要解决两个问题,包含:如何给合乎规定的办法做代理 以及做完代理办法的案例后,把类的职责拆分进去 。而这两个性能点的实现,都是以切面的思维进行设计和开发。如果不是很分明 AOP 是啥,你能够把切面了解为用刀切韭菜,一根一根切总是有点慢,那么用手( 代理 ) 把韭菜捏成一把,用菜刀或者斧头这样不同的拦挡操作来解决。而程序中其实也是一样,只不过韭菜变成了办法,菜刀变成了拦挡办法。整体设计构造如下图:

  • 就像你在应用 Spring 的 AOP 一样,只解决一些须要被拦挡的办法。在拦挡办法后,执行你对办法的扩大操作。
  • 那么咱们就须要先来实现一个能够代理办法的 Proxy,其实代理办法次要是应用到办法拦截器类解决办法的调用 MethodInterceptor#invoke,而不是间接应用 invoke 办法中的入参 Method method 进行 method.invoke(targetObj, args) 这块是整个应用时的差别。
  • 除了以上的外围性能实现,还须要应用到 org.aspectj.weaver.tools.PointcutParser 解决拦挡表达式 "execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))",有了办法代理和解决拦挡,咱们就能够实现设计出一个 AOP 的雏形了。

13. 把 AOP 动静代理,融入到 Bean 的生命周期

其实在有了 AOP 的外围性能实现后,把这部分性能服务融入到 Spring 其实也不难,只不过要解决几个问题,包含:怎么借着 BeanPostProcessor 把动静代理融入到 Bean 的生命周期中,以及如何组装各项切点、拦挡、前置的性能和适配对应的代理器。整体设计构造如下图:

  • 为了能够让对象创立过程中,能把 xml 中配置的代理对象也就是切面的一些类对象实例化,就须要用到 BeanPostProcessor 提供的办法,因为这个类的中的办法能够别离作用与 Bean 对象执行初始化前后批改 Bean 的对象的扩大信息。但这里须要汇合于 BeanPostProcessor 实现新的接口和实现类,这样能力定向获取对应的类信息。
  • 但因为创立的是代理对象不是之前流程里的一般对象,所以咱们须要前置于其余对象的创立,所以在理论开发的过程中,须要在 AbstractAutowireCapableBeanFactory#createBean 优先实现 Bean 对象的判断,是否须要代理,有则间接返回代理对象。在 Spring 的源码中会有 createBean 和 doCreateBean 的办法拆分
  • 这里还包含要解决办法拦截器的具体性能,提供一些 BeforeAdvice、AfterAdvice 的实现,让用户能够更简化的应用切面性能。除此之外还包含须要包装切面表达式以及拦挡办法的整合,以及提供不同类型的代理形式的代理工厂,来包装咱们的切面服务。

三、学习阐明

本代码仓库 https://github.com/fuzhengwei/small-spring 以 Spring 源码学习为目标,通过手写简化版 Spring 框架,理解 Spring 外围原理。

在手写的过程中会简化 Spring 源码,摘取整体框架中的外围逻辑,简化代码实现过程,保留外围性能,例如:IOC、AOP、Bean 生命周期、上下文、作用域、资源解决等内容实现。


  1. 此专栏为实战编码类材料,在学习的过程中须要联合文中每个章节里,要解决的 指标 ,进行的思路 设计,带入到编码实操过程。在学习编码的同时也最好了解对于这部分内容为什么这样的实现,它用到了哪样的设计模式,采纳了什么伎俩做了什么样的职责拆散。只有通过这样的学习能力更好的了解和把握 Spring 源码的实现过程,也能帮忙你在当前的深刻学习和实际利用的过程中打下一个扎实的根底。
  2. 另外此专栏内容的学习上联合了设计模式,下对应了 SpringBoot 中间件设计和开发,所以读者在学习的过程中如果遇到不了解的设计模式能够翻阅相应的材料,在学习完 Spring 后还能够联合中间件的内容进行练习。
  3. 源码:此专栏波及到的源码曾经全副整合到以后工程下,能够与章节中对应的案例源码一一匹配上。大家拿到整套工程能够间接运行,也能够把每个章节对应的源码工程独自关上运行。
  4. 如果你在学习的过程中遇到什么问题,包含:不能运行、优化意见、文字谬误等任何问题都能够提交 issue
  5. 在专栏的内容编写中,每一个章节都提供了清晰的设计图稿和对应的类图,所以学习过程中肯定不要只是在乎代码是怎么编写的,更重要的是了解这些设计的内容是如何来的。

😁 好嘞,心愿你能够学的欢快!

正文完
 0