Advertisement

Spring真的这么难懂吗?还是你没找对方法?

阅读量:

???连读同事写的代码都费劲,还读Spring? 咋的,Spring 很难读!

这个与我们朝夕相处的Spring软件系统犹如你的得力助手,在它身上有着丰富的功能需求:从基本的操作到高级的功能开发都应有尽有,并非没有需求。然而对于这个软件系统而言,并非没有需求,并未了解它的资源储备有多少?也未了解它的资金运作模式是怎样的?开个玩笑哦!接下来我要正经讲讲实际情况了!


一、为什么Spring难读懂?

为什么Spring越来越受欢迎却难以深入理解其内部逻辑?因为它最初由Java和J2EE开发领域的专家Rod Johnson于2002年提出并随后逐渐发展完善的Spring框架,在经过不断优化和完善后如今已经非常成熟并得到了广泛应用!

当你阅读它的源码你会感觉:

怎么这代码跳来跳去的,根本不是像自己写代码一样那么单纯

为什么那么多个接口?因为它们能够实现相同的职责。

基本工厂、工厂架构、代理结构、观察者机制,在应用时会带来如此多的设计方案数量。

又涉及到资源加载问题、又与应用上下文相关的问题、还涉及IoC问题以及AOP问题的同时、而Bean的声明周期也是其中的重要组成部分;如何处理这些代码的整体思路是什么?

怎样呢?这确实是在阅读Spring时会遇到的一系列问题吧?其实不然地说,在这个行业中工作的人(不仅仅是码农),想要深入理解Spring源码也会感到无从下手。于是我想出了一个办法:既然Spring太过庞大难以完全掌握知识体系,则不妨从小处着手——手撸实现一个完整的小型Spring框架是完全可行的!没想到的是这一努力竟然事半功倍——经过近两个月的努力打造了一个简单版本的Spring框架后发现对整个框架的理解不仅更加透彻而且能够真正读懂其源码实质上实现了质的飞跃

这段文字主要讲述了学习Spring过程中遇到的问题以及通过分步构建小型框架来逐步掌握其核心原理的经历与感受。通过这种循序渐进的学习方法不仅帮助加深了对知识点的理解而且也培养了解决实际开发问题的能力

二、分享手撸 Spring

学习者可以通过手写一个简化的Spring框架实现来理解其核心原理,在构建过程中会对Spring的核心源码进行提炼并提取整个框架中的关键逻辑进而简化代码实现流程并保留主要功能模块例如IOC(对象注入)AOP(面向切面编程)Bean生命周期管理以及上下文管理等具体内容的具体实现

源码:github.com/fuzhengwei/…

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

所有能够存储数据的具体数据结构实现都可以称为容器。比如 ArrayList、LinkedList 和 HashSet 等等,在 Spring Bean 容器的情境下,我们需要一种能够用于存储和命名索引式的数据结构。因此选择 HashMap 是最适当的选择。

HashMap 是一种利用多种技术手段形成的拉链寻址数据结构,在软件工程领域具有重要地位。该数据结构通过扰动函数、负载因子以及红黑树优化机制实现了键值对的高度分散存储与快速检索功能。其核心优势在于能够将操作复杂度维持在 O(1) 到 O(Logn) 之间,在极端情况下可能出现 O(n) 链表查找的情况。经过对大量数据(多达 10 万条)进行扰动函数优化与再寻址测试验证表明,在各个哈希桶索引位置上能够实现键值对的有效分散存储,并且碰撞链表与红黑树节点的访问效率表现优异。这些特性使得 HashMap 成为了 Spring Bean 容器实现中的理想选择。

另一种更直观的 SpringBean 容器构建方案,在基于Bean对象的注入模式下实现了容器的基本功能。该方案通过以下三个关键步骤完成:首先定义Bean类的对象结构;其次通过注解或显式绑定的方式进行注册;最后通过调用获取Bean实例的方法完成获取操作。优化方案概述如下:

定义:BeanDefinition被称为常见于Spring源码中的一个类(比如该类通常包含单例模式、原型模式以及具体业务逻辑等)。然而,在目前的初步实现中将采用更为简单的方法——仅需定义一个Object类型用于存储对象。

注冊:该过程等同于我们将数据存储到HashMap中;然而现在HashMap中存放的是已被定义的Bean的对象信息。

最后步骤就是要获得对象名称;当Spring 容器准备好Bean之后, 就可以直接完成获取操作.

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

完善 SpringBean 容器的配置过程中,在豆(Bean)注册过程中仅记录类信息而不直接注入实例化数据到Spring容器中作为一项关键步骤。通过将豆定义(BeanDefinition)中的属性类型从Object更改为Class来实现这一点,在后续的操作中,在获取豆(Bean)对象时需执行相应的操作:一方面处理豆(Bean)对象的实例化操作;另一方面判断当前单例(Singleton)对象是否已被缓存。整体设计如图3-1所示

首先我们定义了一个名为BeanFactory的工厂类它实现了获取指定名称Bean的方法getBean(String name)该工厂接口基于抽象类AbstractBeanFactory实现这种设计采用了模板模式使得调用逻辑得以统一化继承者只需专注于实现具体方法的功能即可通过这种方式实现了对后续开发者的封装效果从而降低了维护复杂度

那么通过继承AbstractBeanFactory这一摘要类之后生成的AbstractAutowireCapableBeanFactory将能够达成相应的功能。这是因为被继承为一个同样具备功能的扩展型自动生成框架。因此它负责处理本类特有的功能模块而由继承该自动生成框架的其他类型自动处理各自的功能模块。这种代码设计体现了良好的模块化架构理念在这种模式下各个组件能够协同工作而不互相干扰每个组件专注于自己所应承担的功能部分并通过明确的角色划分实现了系统的高效运行和可维护性。

此外,在这里还有一个非常重要且关键的知识点涉及到单例模式下 SingletonBeanRegistry 接口的具体实现方案。DefaultSingletonBeanRegistry 实现该接口后,默认情况下会被继承并由其具体实现将被继承并由 AbstractBeanFactory 承受这一结构将使其成为功能全面且极为强大的框架,并且这一结构也很好地体现了其对模板模式的通用化支持。

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

解决这一技术难点的设计方案主要包含两个方面的内容:其一是串行流程中合理地将构造函数的入参信息传递至对象的实例化操作阶段;其二是同时涵盖对象构造函数的实际应用处理过程。

借鉴 Spring 豆丁容器的具体实现方式,在我们的项目中新增了一个名为 getBean 的 Object 类型接口(String name, Object… args)。通过这一设计,在获取 bean 的过程中能够传递构造函数所需的参数信息。

核心内容是采用何种方式构建带有构造函数的Bean对象?系统提供了两种构建Bean对象的方法:一种是基于Java自身提供的机制DeclaredConstructor;另一种则是利用Cglib动态生成Bean实例。其中Cglib采用了ASM字节码框架作为基础,在ASM框架下可以直接操作指令码来生成Bean实例

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

基于此,在 AbstractAutowireCapableBeanFactory 类中,在 createBean 方法内进行属性填充操作。这样可以在此类的createBean方法中添加补充属性的方法。同时,在实习过程中也可以参考Spring源码中的相关内容。其中的具体实现类似于Spring的一个简化版本。通过后续的学习对比将更加容易理解。

属性填充必须在类实例化之后进行;也就是说,在 AbstractAutowireCapableBeanFactory 类的 createBean 方法中加入 applyPropertyValues 操作。

因为我们在为Bean进行属性操作时需要填充数据内容的关系,在定义一个BeanDefinition类中时,请确保设置好PropertyValues信息。

此外,在填充属性信息的同时还包含了 Bean 对象类型的信息。即需要再定义一个 BeanReference 接口,在具体的实例化操作时递归创建和填充这些属性值,并与 Spring 源码实现一致。需要注意的是,在实例化操作时递归创建和填充这些属性值。而根据 Spring 源码可知,BeanReference 是一个接口

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

基于本章节的需求背景,在现有Spring框架雏形的基础上增添一个资源解析器能够读取classpath本地文件以及云端存储文件中的配置信息这些配置信息类似于我们在Spring框架中所使用的Spring.xml文件中的设置经过解析后完成注册操作其Bean对象描述信息经过解析流程后完成注册操作并将其Bean对象注册到Spring容器中整体架构设计如图所示

该资源加载器作为一个相对独立的组件存在,在Spring框架的核心包中,并专注于实现相关的接口;该组件负责管理Class、本地以及云环境中文件的信息处理工作,并支持多种数据格式的操作。

一旦某个对象完成加载过程,则下一步将是整合与绑定Bean至Spring容器中。这一系列操作将通过DefaultListableBeanFactory核心组件进行处理。最终将整理后的Bean定义信息存储在此核心组件中。

那么在实施过程中就要规划好接口及其层次结构,并在此基础上明确具体的业务逻辑与功能模块之间的交互关系。其中需要明确的是Bean定义所需的读取接口BeanDefinitionReader及其对应的业务处理逻辑实现类。同时,在相应的业务处理类中完成对Bean对象的数据解析与注册工作。

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

为了能够满足于在Bean对象从注册到实例化的过程中执行用户的自定义操作,则需要在其定义与初始化阶段嵌入一个接口类;该接口将通过外部机制来实现所需服务。结合Spring框架对对象生命周期管理的能力,则可达成我们的目标需求。整体设计架构如附图所示:

涉及关于Bean对象扩展的重要方面;实际上也是Spring框架中非常重要且关键性十足的部分;也可以说是大家在使用Spring框架构建个人项目所需的核心组件。

BeanFactoryPostProcessor组件是由 Spring 框架提供的容器扩展机制 允许在 Bean 对象注册但尚未进行实例化时 对 Bean 的定义信息 BeanDefinition 执行相应的修改操作

BeanPostProcessor 是 Spring 提供的一个扩展机制,在 Bean 实例化后会修改 Bean 或替代其功能。这部分与后续要实现的 AOP 有紧密联系。

如果仅添加这两个接口,并且不进行额外封装的话,在使用上仍然会带来不少困扰。我们期望于开发一个Spring上下文操作类,并将相应的XML加载、注册、实例化等操作以及新增功能和扩展融合进去。这将使得Spring能够自动识别并集成我们的新增服务,并让用户更加方便地使用这些新功能。

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

当我们需要处理像Spring这样功能繁重的框架时

在Spring的配置文件中添加了init-method和destroy-method两个注解,并在配置文件加载时将这些注解一并定义到BeanDefinition的属性中。
这样,在initializeBean初始化操作的过程中, 就可以利用反射机制调用Bean定义属性中的方法信息。
另一种情况是通过Bean对象直接调用相关接口的方法。
最后一步是在Bean初始化完成后执行的动作。

除了在对象初始化阶段执行的操作外,在Bean对象初始化完成阶段会配置destroy-method和DisposableBean接口的声明,并负责将注册销毁方法的信息配置到DefaultSingletonBeanRegistry类中的disposableBeans属性中。这是为了后续统一进行操作的安排。此外,在处理此类操作时会采用适配器包装的方式进行处理:通过反射调用与接口直接调用两种方式来实现功能的一致性。后续代码示例将参考DisposableBeanAdapter的具体实现以完成这一过程

对于销毁方法而言,在虚拟机执行关闭操作之前必须先进行操作。因此,在实现这一功能时需要特别注意这一点,并且可以通过在虚拟机生命周期中注册一个钩子来实现这一点。具体来说,在Runtime类中可以找到并调用addShutdownHook方法,并创建一个自定义的线程来触发相应的操作:例如打印出'close!'以确认操作完成。为了验证该功能,请尝试运行以下代码进行测试:$vm.addShutdownHook(new Runnable() { public void run() { System.out.println("close!"); } });此外,在不影响系统稳定性的情况下也可以直接调用ApplicationContext.close()来关闭容器。

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

当我们面临像Spring这样功能强大的框架时,在其对外提供的接口定义使用或XML配置文件设置的基础上完成了一系列扩展性的操作时

在 spring.xml 配置中增添 init-method 和 destroy-method 两个注解,在配置文件加载过程中同步将这些注解配置整合到 BeanDefinition 属性中。这样,在 Bean 的 initializeBean 初始化操作过程中就能够通过反射机制调用 Bean 定义属性中预设的方法信息了。另外,在接口实现的方式下,则可以直接通过 Bean 对象调用对应接口定义的方法实现类似效果(见图 1)。两种方式均能达到相同的功能效果,并且后者通过 BeanReflectionManager 实现了对方法信息的动态获取和管理。

在Bean对象初始化阶段进行的操作主要包括:首先定义destroy-method和DisposableBean接口的含义;其次将这些销毁方法注册到DefaultSingletonBeanRegistry中的disposableBeans属性中;这是为了后续能够统一进行操作。此外,在实际应用中可能会遇到反射调用与接口直接调用两种不同的方式;因此需要通过适配器进行相应的处理和包装;具体实现细节将在后续的代码讲解部分进行阐述

为了确保销毁过程的有效性,在虚拟机处于未关闭状态时必须执行销毁操作;因此必须使用钩子机制来实现这一功能;例如:addShutdownHook(new Runnable() { public void run() { System.out.println("close!"); } });此外还可以直接调用ApplicationContext.close()方法来终止容器运行

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

如果我想要从Spring框架中获得一些提供的资源的话,则需要首先思考如何取得这些资源。一旦确定了取得这些资源的方式之后,在Spring框架中如何处理这种获取方式?通过实现这两项核心功能并将其整合到系统中就能拓展出一系列属于Spring框架自身的能力。

我们在 Bean 对象实例化的过程中涉及了创建了一些自定义属性,并完成了初始化与销毁操作。实际上,在像获取 Spring 这样的框架中使用 BeanFactory 或 ApplicationContext 类型的对象时,则可以通过类似的机制来完成功能。因此我们需要设计一种具有标识功能的虚拟接口——该 interface 本身不包含任何业务逻辑, 其主要作用仅在于作为判断依据的标准类型, 而具体的实现则由继承该 interface 的所有功能性子类进一步明确。最终这种 interface 将能够通过 instanceof 操作符来进行判断与引用, 如下图所示:

在Spring框架中定义如下接口Aware类(如图1所示),其子类的定义与实现能够识别并处理容器中的相关对象。该元类的主要功能体现在能够将外部服务与内部组件进行有效对接

该类组件涉及多个标准接口及其相关扩展功能。
具体而言,
该组件会继承 BeanFactoryAware 等等接口,
并支持 BeanClassLoaderAware、BeanNameAware 和 ApplicationContextAware 等功能。
然而,
在 Spring 源码中还涉及其他的注解接口,
目前我们暂时不需要使用它们。

在具体接口的实现过程中可以看到, 其中一部分(如BeanFactoryAware、BeanClassLoaderAware、BeanNameAware)位于factory目录下的支持文件夹中, 而ApplicationContextAware则位于context目录下的支持文件夹中, 这是因为不同内容的获取通常会在不同的包中提供支持. 因此, 在AbstractApplicationContext的具体实现中会使用将BeanPostProcessor集成到ApplicationContextAwareProcessor的操作, 最后由AbstractAutowireCapableBeanFactory创建的createBean方法将处理相应的操作. 关于applyBeanPostProcessorsBeforeInitialization这部分内容已经在前面章节中有提及, 如果忘记相关内容也可以向前查找相关知识

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

关于针对允许用户自定义复杂Bean对象的功能设计而言,则是一个非常出色的功能特性,并具有重要意义。因为这种做法实施后,在Spring生态中种子孵化箱体系随之建立起来,并为各类框架在此统一标准下完成服务接入提供了便利条件

然而这种功能逻辑的设计并非十分复杂,因为在整个 Spring 框架在开发阶段已经具备了相应的扩展接口,开发者只需在此基础上进行适配性改造即可.具体而言,该系统通过为各个实现该接口的对象类创建统一的业务逻辑入口,使得各类对象都能方便地接入并完成增删查改等基本操作.以 MyBatis 为例,其核心实现就是通过 MapperFactoryBean 类向 SqlSession 提供执行 CRUD 操作所需的代码片段.整体的设计思路如图所示:

实现过程分为两个阶段:第一阶段负责单一实例或原型对象的问题;第二阶段则专注于FactoryBean类的对象创建过程中如何获取具体的调用对象的操作。

该系统支持两种对象类型管理策略:单一实例模式(SCOPE_SINGLETON)与原型模式(SCOPE_PROTOTYPE)。这两种策略的主要区别在于:基于AbstractAutowireCapableBeanFactory.createBean方法生成的对象,在完成创建后是否被保留在内存中;若不在内存中,则每次获取时会再次生成。

createBean 负责执行以下操作:包括创建运行时环境、填充属性值、加载依赖项、处理前置与后置事件以及启动初始化流程等步骤完成后,则需要对当前的对象进行进一步的判断:该对象是否为FactoryBean类?如果是,则需要继续获取其对应的FactoryBean具体实例中的目标对象以完成后续操作。在整个getBean过程中都会增加一个单例实例类型的检查(即$factory.isSingleton()),以决定该实例信息应当采用内存缓存的方式还是持久化存储的方式进行管理。

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

其实在设计事件本身就是一个实现Observer模式的方式,在解决对象状态变化引发其他对象接收通知的问题上具有重要意义;同时需兼顾易用性和低耦合性以确保高度协作。

在功能模块设计上需要明确划分出事件类、事件监听器以及事件发布者这三个核心组件,并使它们的功能与Spring框架中的AbstractApplicationContext#refresh()方法集成到一起,从而实现对事件初始化和注册事件监听器的操作支持。如图所示为整体架构规划图

在功能实现的整体过程中,在应用层面的AbstractApplicationContext中仍需配置必要的事件监听机制。具体而言,则需依次执行以下步骤:首先设置事件发布者模块以启动关键操作流程;其次按照既定规则配置事件监听器以捕获变化信号;最后确保容器刷新过程中的状态更新被及时记录下来。

基于观察者模式实现一种完善的事件处理机制,在设计过程中需要包含三个主要组件:一个是事件类(Event),一个是监听组件(Listener),还有一个是发布组件(Publisher)。在此框架下还需构建广播功能模块(Broadcast Functionality),该模块的主要职责是在接收到来自不同来源的事件推送后进行处理逻辑,并根据特定条件筛选出感兴趣的相关事件类型(Interest Event Type),通常会采用 isAssignableFrom 方法来实现这种关系判断。

is_appable 和 instanceof 具有类似的用途,在某些特定场景中也被认为是用来判断一个类型是否为另一种类型的子类型的一种方法。\n\n它不仅用于检查具体继承关系(即某个具体子类能否直接继承自某个特定父/基类),还被用来检查一种类型的可实施性(即某个类型能否实施另一种类型的协议或接口)。\n\n默认来说,在大多数编程环境中,默认情况下的某个属性通常会返回 Object 类型本身。\n或者说 default 的行为通常是什么样的?这可能取决于具体的编程环境以及相关语言特性的设定。\n具体情况请参考相关的文档说明

12. 基于JDK和Cglib动态代理,实现AOP核心功能

在将整个 AOP 切面设计成功整合到 Spring 中之前,则必须解决两大关键问题:一是为符合既定规则的方法提供代理服务;二是完成方法代理后需将类的职责进行模块化拆分与处理。这两种功能实现过程均需以切面思想为核心进行规划与开发工作。若对 AOP 概念仍不清晰,则可将其类比为使用刀具切割韭菜的过程:单独一根根切割较为缓慢耗时;若能以手捏成一把韭菜的方式运用不同类型的拦截手段(如菜刀或斧头)来实现一次性处理多根韭菜的效果,则程序逻辑则等价于将方法视为韭菜并利用拦截方法(如代理)来进行批量处理

与Spring的AOP机制相似地,在完成对这些需要进行拦截的对象之后实施扩展操作

为了能够实现一种能够代理的方法Proxy,在实际操作中我们主要采用的是MethodInterceptor类中处理的方法调用方式

除了上述核心功能之外,此外还需要利用org.aspectj.weaver.tools.PointcutParser来处理拦截表达式执行(cn.bugstack.springframework.test.bean.IUserService.(...))。通过实施代理技术和拦截机制的应用,我们便能够构建起一个AOP框架。

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

其实在完成了AOP的核心功能之后,将其这部分功能服务整合到Spring框架中并不困难,但需要解决以下问题:一是如何借助BeanPostProcessor将动态代理成功融入到Bean的生命历程中,二是如何将各个切点、拦截器以及前置功能与相应的代理器进行适配。整体架构如下图所示

为了能够使对象创建过程顺利进行,并将配置好的XML中所定义的代理对象(即切面)的一些类对象实例化出来,则必须依赖于BeanPostProcessor所提供的相关方法。这些方法能够分别承担着对Bean对象初始化前后的修改工作,并存储关于Bean对象扩展信息的具体内容。然而,在此过程中则必须通过开发新的接口并实现相应的类来获取所需的具体类信息。

由于当前所创建的对象是代理实例而非常规流程中的普通对象,在处理其他对象时必须提前考虑 AbstractAutowireCapableBeanFactory.createBean 方法的操作顺序。该方法会首先对 Bean 进行判断,并根据是否需要生成代理实例来决定后续的行为:如果需要,则直接返回相应的 proxy instance;否则则继续执行常规操作。值得注意的是,在 Spring 源码中会将 createBean 方法与 doCreateBean 方法进行了分离处理以实现特定功能。

此外涵盖该种机制的具体功能。支持提供BeforeAdvice及AfterAdvice的实现方案,并允许用户更为简便地使用切面功能。除了上述之外还需要将切面表达式与拦截方法进行整合。此外还可以通过不同类型的代理工厂来包装我们的切面服务

三、 学习说明

该代码库专门用于Spring源码的学习目的,并通过手动编写一个简洁版本的Spring框架来深入理解其核心原理

编写手写的代码时会将Spring源码进行提炼与优化,并从整体架构中提取关键逻辑模块以达到简化开发流程的目的。通过优化代码的实现流程与设计架构细节安排能够有效降低开发难度并提升系统性能。具体而言该方法将包括以下几个方面的内容:例如IOC(面向服务接口)、AOP(方面编程)、Bean生命周期管理、上下文管理、作用域划分以及资源处理相关内容的具体实现。


本专栏旨在提供实战型编码教程,在学习过程中建议结合每一章的具体目标进行思路设计,并融入到编码实践环节中去完成相应的技术实现任务。在掌握编程技能的同时最好能够深入理解这些核心概念与实现细节,并通过系统的学习和深入理解这些核心概念与实现细节,则能为后续的学习和发展应用打下坚实的基础。

本专栏在学习过程中融合了设计模式的相关知识,并具体涉及SpringBoot中间件设计与开发的内容。因此,在学习过程中如遇到难以理解的设计模式时建议查阅相关资料以加深理解。完成对Spring的理解后建议配合中间件内容进行实际操作与练习以巩固所学知识。

此专栏所包含的所有代码已全部整合至当前工程中,并且这些代码与各章节中的案例源码能够一一对应起来。这样用户就能方便地直接运行整个项目,并且也可选择分别打开各章节的代码包进行测试。

如果你在学习过程中遇到任何问题,请提交issue。以下涉及的问题均可提交issue:程序无法运行、优化建议以及文字错误等。

对于专栏内容的编写过程而言,在每一章里都配备了详尽的设计图谱,并配以相应的类图示例。因此,在学习过程中应当避免仅仅关注代码实现细节,并更加注重弄清楚这些设计是如何产生的

原文链接:https://juejin.cn/post/6989013058035122207

全部评论 (0)

还没有任何评论哟~