likes
comments
collection
share

【重写SpringFramework】从零开始构建Spring框架:序言

作者站长头像
站长
· 阅读数 26

1. 概述

在如今的 Java 后端开发中,引入几个依赖,添加几个注解,一个 web 应用就能启动起来。但我们能说开发者真的掌握相关技术了吗?高度封装的框架有利有弊,一方面提高了开发的效率,另一方面屏蔽了大量的实现细节,开发者只知其然而不知其所以然。对于开发人员来说,框架就是产品,提供的是成熟的服务。 一般说掌握了某某框架,只是会使用而已,并不代表了解其原理。

要想更深入地理解框架,就需要阅读源码,这是横亘在每个开发者面前的大山。只有跨过了这道坎,编程水平会得到长足的进步,技术视野也会变得更加广阔。所谓他山之石,可以攻玉。通过对优秀框架的研究,掌握高明的设计理念和编程技巧,对于自身技术能力的提升大有裨益。

Spring 框架作为 Java 语言最流行最重要的开源框架,对于开发者来说不仅是使用工具,也是学习对象。我们将通过造轮子的方式重新构建整个框架,全面而深入地理解 Spring 框架的核心理念以及主体流程。

2. 面临的问题

2.1 内容庞杂

阅读源码的第一个问题是面对庞杂的体系,无处下手。即使阅读过一些书籍和文章,研究问题时往往也是一叶障目,不见泰山。这一现象产生的原因是缺乏系统化、模块化的思维方式,不能从全局视角进行整体把握。

目前市面上充斥着关于 Spring 框架的书籍和文章,往往有深度的无广度,有广度的无深度。一部分号称面面俱到,实际上蜻蜓点水,没有触及问题的核心。另外一部分对某些问题有比较深入的研究,但缺乏横向的联系,形成了一个个信息孤岛。要知道一个严密的系统是环环相扣的,仅对个别问题的研究并不能使我们认识到庐山真面目。

2.2 细节地狱

在阅读源码时,面对某一个功能,很容易陷入分支流程的无尽迷宫,以及异常繁琐的细节实现。我们将这种困境称为细节地狱(particulars hell) ,这种情况应当设法避免,应对的方法包括两个方面。

首先需要对功能进行划分,将一个大问题分解成若干个小问题。每个小问题都只关注一个方面,把代码量控制在一定的范围内。其次,阅读源码并不意味着一定要把所有的细节搞清楚,关键是掌握一个度。比如 JDK 原生的 API 会使用就足够了,当然对于一些重要的部分掌握其原理更好。Spring 核心包与 JDK 类似,提供了一些基础功能。因此我们并不打算深究其实现细节,直接引入 Spring 核心包作为整个项目的基石。

2.3 代码瘦身

Spring 框架演变至今,已经从一个功能单一的框架发展到了一个庞大的生态。即便仅考虑微服务体系的技术栈,也包括了几十个模块。对模块的选择是第一步,具体内容见下文的学习目标。仅对模块进行精简还是不够,代码量依然很大。虽然是造轮子,但并不是一比一复制,这样做既无可能也无必要。因此我们需要对代码进行精简,在这一过程中需要遵循一定的原则,要做到涵盖核心功能,确保主体流程的完整。

在当今的社会实践中,二八原则作为一项重要理论指导生产生活,其基本表述为少数实体占据多数资源。同样地,二八原则也适用于软件领域。对于一个框架来说,核心代码只占两成,另外八成是为了处理特殊情况,以保证代码的健壮性和扩展性。如果贪多求全,代码量会急剧增长,想独立完成 Spring 框架的重建几乎是不可能的。鉴于此,我们在代码层面上会进行一定的精简,但也不是那种浮于表面的 demo。我们关注的是 Spring 框架的核心流程,用尽可能少的代码来实现功能。

3. 学习目标

3.1 总体规划

本教程的学习计划分为三个阶段,首先是 Spring Framework,构建一个传统的 web 应用。其次,在 Spring Framework 的基础上,结合嵌入式 servlet 容器,构建 Spring Boot 应用。第三,以 Spring Boot 为基础,实现分布式的微服务体系。三个阶段环环相扣,通过逐步学习,对 Spring 框架的核心功能有全面而深入的理解。

【重写SpringFramework】从零开始构建Spring框架:序言

第一阶段的目标是构建一个传统 web 应用,将项目打包成一个 war 包,然后置于 servlet 容器(比如 Tomcat)的工作目录下。当 servlet 容器启动时,运行 web 应用。本阶段我们将实现 Spring Framework 的六个模块,包括 beans、aop、context、tx、web、webmvc 等。

第二阶段的目标是构建一个 Spring Boot 应用,最大的特点是将 servlet 容器嵌入到 web 应用中,实现开箱即用、约定大于配置等特性。本阶段我们将实现 Spring Boot 的四个模块,包括 tomcat-embed、mybatis、boot、boot-autoconfigure 等。其中 tomcat-embed 是嵌入式 servlet 容器,mybatis 是流行的 ORM 框架。它们都是第三方框架,属于选学部分。

第三阶段的目标是构建一组基于 Spring Boot 应用的微服务体系,包括注册中心、网关服务、业务服务(business service)等经典角色,实现服务治理、负载均衡、断路保护、高可用等分布式环境下的功能。本阶段我们将实现 Spring Cloud 的五大组件,包括 Eureka、Ribbon、Hystrix、Feign、Zuul 等,构建一个完整的微服务体系。

3.2 最终目标

本教程的最终目标是构建一个微服务体系,包括注册中心、网关服务,以及两个业务服务,即会员服务和订单服务。进一步,我们可以构建多组微服务,各组之间通过注册中心进行通信,从而形成高可用的分布式系统。

【重写SpringFramework】从零开始构建Spring框架:序言

对于一个微服务应用来说,需要引入若干依赖。以下是本教程所涉及的模块,按照三个阶段进行分组。其中,绿色标识属于 Spring Framework 的相关依赖,蓝色标识为 Spring Boot 的相关依赖,红色标识为 Spring Cloud 的相关依赖。

【重写SpringFramework】从零开始构建Spring框架:序言

在项目构建方面,我们严格按照 Spring Cloud 的标准方式来构建。包括使用 starter 引入依赖包、配置文件、声明注解来启动服务等。以网关服务为例进行说明,以下是 pom.xml 引入的依赖项:

【重写SpringFramework】从零开始构建Spring框架:序言

yaml 的配置包括服务名、端口号、与注册中心的通信、路由规则等,与 Zuul 服务的配置完全一致。

【重写SpringFramework】从零开始构建Spring框架:序言

应用主类声明了 @SpringBootApplication@EnableZuulProxy 注解,并以 Spring Boot 应用的方式启动。

【重写SpringFramework】从零开始构建Spring框架:序言

在 IDE 中启动四个微服务应用,控制台打印启动日志,如下所示。

【重写SpringFramework】从零开始构建Spring框架:序言

3.3 本阶段目标

在第一阶段,我们的目标是实现 Spring Framework 六个模块,各模块之间的依赖关系如下所示。首先,我们需要依赖 Spring 核心包,作为整个项目的基石。然后是 beans 和 aop 模块,它们提供核心的 IOC 和 AOP 功能。接下来是 context 模块,作为门面与用户(包括其他模块)打交道,比如大名鼎鼎的 ApplicationContext 就属于 context 模块。

接下来出现了两个分支,它们是 Spring Framework 的两个应用方向,通常会结合起来使用。在一个典型的 web 应用中,至少要引入 webmvc 和 jdbc 模块,然后通过依赖关系引入其他模块。

  • tx 和 jdbc 模块是为访问数据库提供服务的,由于 jdbc 模块的代码较少,我们将其归入了 tx 模块中。
  • web 模块提供了 web 应用的基础组件,比如 WebApplicationContext 表示运行在 web 环境中的 Spring 容器。理论上我们可以利用这些组件构建 web 应用,实际上很难直接使用。
  • webmvc 模块引入了 MVC 架构,极大地提高了 web 应用的开发效率。事实上,构建 web 应用可以选择其他 MVC 架构,比如以前比较流行的 Struts2 框架。

【重写SpringFramework】从零开始构建Spring框架:序言

4. 本教程的特点

4.1 分层方法论

前面分析了阅读源码时的种种困难,那么正确的做法是什么,我们针对高中低不同的层次总结了三条原则。

  • 在顶层设计方面,做到高屋建瓴,俯瞰全局。这一层次主要是抽象的理念上的认识,比如 Spring Framework、Spring Boot、Spring Cloud 三者之间的关系是如何组织的,各模块的定位以及模块之间的依赖是如何处理的。
  • 在中间架构方面,做到提纲挈领,纲举目张。对于一个业务范畴,比如 IOC 和 AOP 机制、数据库事务等,它们的主体结构是如何建立的,可以分解为多少个具体的功能,其中用到了哪些编程思想和设计模式。一个模块往往包含一个或几个业务范畴,抓住了这些关键节点,对于整个模块的理解会变得事半功倍。
  • 在底层实现方面,做到条分缕析,举一反三。我们需要了解具体的功能用到了哪些技术,这些技术的特点以及各自的适用情况。特别是很多功能之间有着清晰的演进脉络,比如一个功能看上去很复杂,但如果了解前置的技术,再来看该功能,大部分是对原有技术的整合,还有一部分新内容,总体而言更容易理解。

如果我们把 Spring 框架看作一个大型建筑,那么顶层设计就是项目蓝图,规划了建筑的形制与功能。中间架构是地基和框架,承载了整个建筑的主体结构。底层实现是砖石材料,填充完善细节部分。三者不可偏废,互相配合,最终造就了 Spring 这座宏伟的殿堂。

4.2 渐进式开发

本教程的出发点是从框架编写者的角度考虑问题,而不是从使用者的角度考虑问题。面向对象编程的核心是组织和调度,单独编写一个功能对开发者来说很简单,但要将若干个功能整合在一起,从而构成一个庞大的、严密的、完备的体系,就必须遵循一定的规律。因此我们采取的是渐进式开发,先从最基础的功能做起,从简单到复杂,从单一功能到统一的有机的整体结构。我们将按照模块的依赖关系进行讲解,先介绍底层模块,然后是上层模块,以此类推。大多数情况下,我们会遵循这一原则,当然也有例外,届时会进行说明。

举个例子,在介绍 IOC 容器时,通常是把 BeanFactoryApplicationContext 结合起来一起讲。实际上,前者属于 beans 模块,后者属于 context 模块。我们会先介绍 BeanFactory 的基本功能,再介绍 ApplicationContext 的作用,至于两者之间的区别和联系,这又牵扯到 beans 和 context 模块各自的定位,这些问题将在学习的过程中予以解答。

4.3 测试用例

本教程提供了丰富的测试内容,每一节都会设计若干测试用例,使得读者在读完本节后,知道该做什么。而不是像某些纯源码读物,看了之后云山雾里,不知所云。出于控制代码量的考虑,测试用例仅覆盖重点介绍的核心功能。此外,还有一些常用但比较次要的功能,该部分的实现和测试交由读者来完成,进一步巩固学习效果。

5. 版本选择

为了方便读者对照源码,我们在版本的选择上进行一定的考量。Spring Cloud 的版本选择 Edgware 或 Dalston,对应的 Spring Boot 版本是 1.5.x。Spring Boot 的版本选择 1.5.22.RELEASE,这是 1.5.x 的最后一个发布版。与之对应的是 Spring Framework 的版本是 4.3.25.RELEASE,这也是 Spring4 的最后一个发布版。

【重写SpringFramework】从零开始构建Spring框架:序言

为什么选择低版本的 Spring Cloud?一开始 Spring Cloud 是对 Netflix 的微服务组件进行封装,后来 Netflix 停止了相关组件的开源。如果使用高版本的 Spring Cloud,由于 Netflix 停止了对 Zuul、Feign 等组件的支持,Spring Cloud 不得不使用 Gateway 和 OpenFeign 来代替。此外,Eureka 也可以使用 Nacos、Appllo 等注册中心组件来替代。我们来分析一下这些组件的影响:

  • Gateway 使用 Webflux 代替了 SpringMVC 框架,使用了响应式编程的代码风格,增加了理解的难度。更重要的是,嵌入式的 Tomcat 服务替换成了 Netty 框架。而 Spring Boot 使用 Tomcat 作为 servlet 容器,我们不希望增加学习的成本,因此选择 Zuul 作为网关服务。
  • OpenFeign 大体上继承了原本 Feign 组件的逻辑,区别不大。
  • Eureka 虽然也有替代的 Nacos 等注册中心,但它们毕竟是第三方框架,不便研究源码。

此外,Spring Framework 版本的选择也有一定的考量。Spring5 相比 Spring4 来说,增加了很多新东西。比如对 Koltin 语言的支持,很多地方的代码使用函数式编程的风格重写,增加了阅读源码的难度。因此,选择 Spring4 的最后一个发行版,既保证了稳定性,又与 Spring Boot 版本保持一致,可谓一举两得。

总的来说,我们的目标从整体层面了解微服务的治理,为了分析核心组件的原理,因此选择较低版本的 Spring Cloud。至于 Spring Framework 和 Spring Boot 则选择了大版本的最后一个发布版,我们认为这一版本的完成度和稳定性是很高的,可以作为源码的参考依据。

6. 结语

庖丁解牛是《庄子•养生主》中记载的一则脍炙人口的故事。庖丁为文惠君表演杀牛的技艺,文惠君对庖丁高超的技巧叹为观止,并询问是如何做到的。庖丁解释说自己一开始杀牛时,看到的只是眼前的牛。三年之后,就看不到全牛了,是因为自己做到了以神遇而不以目视,官知止而神欲行。这是说掌握了事物的运行规律,就能做到胸有成竹,随心所欲。具体的做法是彼节者有间,而刀刃者无厚。以无厚入有间,恢恢乎其于游刃必有余地矣。这是说牛的骨骼肌肉是有间隙的,但刀刃很薄几乎没有厚度。把没有厚度的刀刃放到骨肉的间隙中,自然就游刃有余了。

文惠君从庖丁解牛的过程中领悟了养生的道理,同样地,我们也可以用来研究编程的问题。Spring 框架对我们来说是一个庞然大物,但再大的系统也是有间隙的,是由一个个基本单元组成的。我们只要合理运用编程思想、设计模式等工具,对复杂系统进行分解,把每一个基本单元搞清楚,也能达到「以无厚入有间,游刃有余」的境界。


欢迎关注公众号【Java编程探微】,回复「重写SpringFramework」加群一起讨论。

原创不易,觉得内容不错请分享一下。

转载自:https://juejin.cn/post/7369838480984768562
评论
请登录