首页 > 科技 > 白话总结Spring 框架的容器管理的设计原理和由来

白话总结Spring 框架的容器管理的设计原理和由来



前言

最近跟公司新招的一批Java开发人员聊天时,他们一直对一些Spring 框架的实现原理性概念纠结来纠结去,虽然知道其核心内容是什么,但就是搞不明白原理,为此我给他们简单整理和串联了一下。从程序的设计和代码的编写,到工程化管理的引入,数据类型的定义和管理,组件化设计和编程,以及依赖控制的反转,以及依赖项注入,到最终的容器化管理,Spring 框架的核心设计思想等。

编程的基本思路和方法

我们编写的应用程序一般都不是靠一个过程或者方法就能解决问题,特别是涉及到要使用不同的系统资源,比如访问数据库,读取文件,接收来自网络的请求等等。所以我们编写的应用程序一般来说都是比较复杂的逻辑实现。

对于我们人类大脑来说,处理复杂的问题,一个通常的方式是化繁为简,将复杂的问题分解开来,逐一解决,然后综合在一起共同去完成复杂的业务逻辑。

这就是我们编程需要具备的逻辑分析和解决问题的基础能力。

我们一般解决一个复杂问题是,先是设计一个总体的函数,来约定输入,整体逻辑步骤和输出结果。

然后将每个步骤再细化成一个包含逻辑的函数或者方法,将输入放入一个独立的数据类型里,并指定输出结果。

如此循环直至我们的业务逻辑简化到通过简单的几个逻辑步骤就能解决的程度。如此我们就可以使用开发语言为我们提供的基础的逻辑运算来描述了。描述完成后,我们将这些基础的逻辑单元或者函数按照其上一层整体的业务逻辑来编排起来,逐级进行直至最外层的逻辑。

如此我们就回到了整个业务逻辑问题的解决步骤上了。

这是我们编程的最基本的思路和处理办法。



数据类型和编码

那么问题就来了,我们知道编程就是将数据放入到内存空间中,然后通过指令数据控制进行各个空间之间的相互拷贝和转移,从而借助累加器来进行相应的二进制运算,将结果拷贝到指定的空间里。

而这个过程的最初抽象应该是通过对空间地址的编码和指令地址的编码,然后使用一个栈结构来不断的压入和弹出将我们所编写的各种逻辑步骤放入栈中,或者从栈中读出执行的过程。

那Java开发来说,我们为了这些空间能够更接近人类理解,将二进制转换为十进制来表示,这就需要将二进制的多个地址空间联合起来共同表示一个十进制数据,同理通过基础的编码定义比如ASCII等将一些字母或者特殊字符表示为十进制数据,或者为了适应一些大容量计算缓存的我们将可以将二进制的0和1转换成八进制或者十六进制等,来方便书写。

总之,这种编码是一种抽象,将只有机器可以理解的二进制比特转换为人类能够识别的数字。

同时为了更好的表达人类理解的世界内容,我们将对数字进行进一步的抽象,将不同长度的二进制数字定义为某种更加抽象的数据类型,比如我们大部分高级语言里提供的基元类型,布尔型,整型,长整型,短整型,浮点型,double类型等等。

它们都是对不同长度的二进制空间的抽象描述,成为基元类型后,它就能够应用到对人类理解的世界事物的描述中了。

当然为了表达更加复杂的事物,我们需要创造更加复杂的数据类型才可以,所以高级语言允许我们通过用这些基元类型的组合和排序来抽象出更加复杂的数据类型,比如我们面向对象编程里的对象,接口等。

面向对象数据设计

这里简答说一下,我们面向对象编程语言中,会将所有的数据类型都看做是一个对象,比如Java中,定义了一个创世类型Object。

我们可以把它理解成一个对内存单位空间的基础的抽象描述,而这个空间必然是一个可被管理的空间,也就是说它必须位于一个不固定的空间里。

它只是一个地址描述,但是具体空间的大小或者位置,是可以具体指定的,也就是说这个空间不是直接存储数据的,而是一个指向某个不确定空间的地址。

我们可以把它想象成这么一个抽屉,这个抽屉里放什么由我们自己决定,而抽屉有锁,我们能够找到这个抽屉在哪里,我们还可以将这个抽屉复制到其它地方。

同时我们还可以为这个抽屉贴一个自己的标签等。

回到我们上面对二进制表示的空间的第一层抽象描述,转换成基元数据类型,因为这些数据类型在我们人类理解的世界里是经常使用的概念,所以我们会把它们看成是一个固定的常用类型。

一般不会改变他们的抽象定义。而对于那些不常见的,更复杂的类型,我们会给一个自由的内存空间,让开发人员根据需要使用这些基元数据类型自由组合出一个需要的复合类型,由于这些复合类型我们无法估算空间大小,所以它的存储空间一定不是固定的。



Java数据类型和内存关系

同时它们也不是固定的,而是用到时才会去在一个空间中创建的,这个空间在Java语言中我们通常称之为堆。在这里通过组合抽象得到的数据类型,我们不会将它们直接放入操作栈里,而是将其存储的地址表示压入到表示运算逻辑的栈里,在计算用到它们时才会通过地址去寻找它们。

我们知道CPU内部运算的基本单元是以方法栈的形式进行的,也就是说我们会为每个方法创建一个方法栈,同时对应一个栈空间,让该方法的输入参数,常量,结果输出地址,能够在该栈中被运算,然后将结果写入到其调用外层指定的地址空间里。

这在某种程度上实现了一种隔离,我们从Java的数据类型定义中了解,我们可以在栈中压入局部变量的具体数值,但是对于可变大小的数据类型,也就是我们所说的引用类型。

我们只能将他们的数据存储到可变空间堆里,由JVM的GC统一管理,而将其地址变量压入执行逻辑运算的栈中,当栈中逻辑被执行完毕,其地址被用作运算完成后,栈就会被排空,地址变量的引用将不复存在,那么存放在堆里的具体对象数据,就会被GC根据需要而清空。

复杂应用程序与数据类型设计

了解了这个过程后,我们回到面向对象编程处理负责应用实现的问题上来。

当我们面对一个复杂的现实问题,比如我们要管理一个学校老师,课程,学生,选课问题时,显然任何高级编程语言的基础数据类型里不会有可以直接表示老师,课程,学生等现实世界事物的数据类型,这就需要开发人员去根据现实世界里事物的特性来抽象出一个老师数据模型,可能有编号,姓名,性别,年龄,教课科目等。

显然这是一个不能固定存储空间长度的复杂数据类型,这个抽象的数据模型就是一个新的数据类型。比如我们定义Teacher类,就是抽象出一个复杂类型,它可能是组合了字符串类型的编号和姓名,性别,整数类型的年龄等各种基元类型的组合构成。

根据上面说的,我们这个数据类型的具体数值不能直接压入到运算栈里,需要定义一个地址指针变量,通过将它压入到具体的运算栈里参与运算逻辑的表式,而具体的数值的存储则是放入JVM的托管堆里。这就我们所说的引用数据类型,我们可以为这个具体的数据类型定义多个不同的指针变量,压入不同的栈位置,而链接的都是同一个堆里的对象实例。

上面说了要解决学校选课问题,只有老师不可用,还需要有学生数据,课程数据,选课情况等,这些都需要通过抽象去定义复杂的数据类型来表示。

而对于选课问题,我们将其抽象到一个过程里时,就需要将这些数据类型都能够组合到一起来实现具体的逻辑。

那么由于众多的新的复杂数据类型需要组织和管理,开发人员需要根据需要来实例化并使用它们,还有可能需要将其数据的变化保存到数据库中或者通过网络将某些信息传递出去等。



数据类型或者组件管理

这样一个复杂的应用程序,除了上面我们说的涉及的业务逻辑的新数据类型实例的创建,使用,销毁等整个过程管理外,还有可能涉及到一些外部资源的引入,比如数据库连接和管理,网络通信的管理等等,这些目前流行的开发语言都有对应的组件包或者第三方开发的工具类库来处理。

但是这些内容要应用的我们的应用程序中,就需要根据开发商提供的规范去组织和创建其实例化对象,以让应用程序在需要它们的时候能够用到。

到目前为止,我们可以看到开发一个应用程序涉及到应用的功能性组件和非功能性组件数量巨大,同时对于它们相互之间的依赖关系以及这些数据类型和对象的整个存活声明周期都要管理,同时由于一些非功能性组件,比如数据库访问连接,网络连接等都是应用程序无法直接管理的资源,而是由操作系统管理和决定的资源,在我们的应用程序因为自身原因而出现问题时,将无法及时释放这些系统资源,而造成服务器性能问题甚至运行异常等。

同时我们传统的编程方式都是在需要新的数据类型或者功能组件时,就手动通过一些编程语言创建实例的关键字语句比如new等实例化一个实例。

在使用完成后,需要调用其析构函数,或者某些关闭和清理代码块来释放资源,以保持应用程序的运行不因为内存空间被占用而出现问题。

但是随着应用程序业务的复杂程度的增加,需要管理的复杂数据类型和各类组件越来越多,最要命的是各种数据类型和组件相互之间的调用依赖关系也变得越来越难以维护。

显然在编程过程中,在每个逻辑实现过程中去手动的实例化它们变的非常繁琐和难于管理。

工厂模式与容器化管理

为此开发人员开始从社会上汲取经验,特别是从建筑行业的工程实施和管理经验中,引入了对复杂应用程序的开发过程进行工程化项目管理和设计。

比如我们针对应用程序的类型和所需要的不同资源,对其数据类型和组件以及外部资源进行分类和分层次分结构的管理。

比如最早我们借助工厂生产产品的方式,引入了工厂模式,即让某个数据类型或者组件的实例化和初始化由专门的工厂类通过工厂方法来提供。

进一步延伸出了能够根据外部定义的产品规格要求,也就是我们常见的接口描述来生产不同规格的产品,抽象工厂模式。

当我们有了一个专门的工厂来外我们提供各类所需的数据类型实例或者组件实例后,我们又将这众多的工厂放到一个集团旗下统一管理协调这些工厂,以让它们相互之间能够提供产品支持,如此当集团对外提供产品服务时,我们开发人员只需要向集团提供一份所需产品清单即可,这份清单内容说明各个工厂应该按照什么样的方式去生产所需的各类产品,如此最终工厂集团就是我们常说的一个容器。



控制反转与依赖注入

有了上面的知识理解后,我们回过头来在谈控制反转和依赖注入这两个概念。

在我们定义一个复杂数据类型时,除了使用开发语言预定义好的基元数据类型外,我们可能还会使用我们自己定义的复杂数据类型作为基础类型来组建更加复杂的新数据类型。

那么这个复杂的新数据类型在实例化时,就需要先实例化作为其组成部分的复杂数据类型实例,按照传统我们可能要使用嵌套的new关键字来实例化成员。

这样会在我们编写业务逻辑处理时,不断的去用嵌套的方式new对象实例,这些复杂对象包含其它复杂对象的方式,我们称之为这两个复杂对象之间产生依赖关系。

而在外层对象对内层对象的依赖过程中,一般情况下是需要外层对象来负责实例化内层对象,并管理内层对象的存活周期,称之为外层对象控制内层对象,这种谁使用谁管理的方式是最初我们开发应用程序时处理组件和类型依赖的方式。

但是这种方式的弊端在于当系统应用逻辑非常复杂时,将很难管理这种依赖,并且大量的手动实例化对象难以管理,容易造成逻辑混乱。

为了解决这些问题,设计人员遵循面向对象编程思想,先对功能进行的分离,就是让每一部分只负责其自己的功能,如此简化使用其它组件和类型的类型和组件只需要使用即可。

而不需要在去负责创建和管理要使用的数据类型和组件,也就是说外层数据类型或者组件不在控制其使用的内层数据类型和组件,甚至其使用完后都不需要在去关心如何去关闭这些实例对象,反而交给专门的组件和类来负责。这就是我们常说的控制反转(Inversion of Control)。

同时如果我们将某个数据类型或者组件需要使用的数据类型或组件的实例化和生命周期管理交给专门的工厂来处理,那么我们这个类就需要一种方式来得到所需的类实例。

这里我们可以借用方法定义的方式来处理,就是将被依赖的对象实例或者组件实例以参数的形式传递给要使用它们的数据类型和组件。

那么我们就知道了当一个数据类型或者组件需要它依赖的对象实例时,基本上就是在其构造时,初始化时,以及公开调用某种实例化方法时。

如此我们就有了通过其作为构造函数的参数,内部属性设置器参数,或者公开的外部方法的参数来传递给要使用它们的数据类型或者组件。

这就是我们常说的将被依赖内容注入到依赖的对象或者组件内部,称之为依赖注入(Dependency Injection)。



理解容器化设计

我们在编写一些简单应用程序时,遵循功能单一分离原则,我们会将一些依赖项实体对象的创建通过工厂模式定义成工厂类,通过将工厂类封装成一个工具类的形式,统一管理这些依赖内容的生产,然后只需要在要依赖这些内容的构造函数或者属性设置器以及特定对外方法中将这些工厂实例或者其产品实例注入即可。

但是对于复杂的应用程序来说,显然工厂类也是繁多的,不可能一个个管理,为此就像前面我们所说的引入了集团工厂方式。

我们可以将其看做是工厂的管理者,它能够管理协调各个工厂,生产应用程序所需的各类产品组件或者数据类实例。并且管理它旗下所有工厂生产的对象实例的生命周期和依赖关系。

其统一使用构造函数依赖注入方式,只要求每个受它管理的组件或者数据类型定义必须有一个无参构造函数以便它能直接调用,如此依赖我们可以在我们应用程序启动阶段就完成对这个集团工厂的实例化准备工作,就是让它更加需求说明书生产出应用程序所需的所有组件或者数据类型以及其依赖内容。

然后将这个集团工厂实例注入到我们整个应用程序的主线程中即可,如此整个应用程序级别上,有任何需要的依赖项都可以直接通过该集团工厂容器提供管理的对象中直接使用了。这就是容器化概念。

总之,控制反转,依赖注入以及容器化编程,这些概念都是为了解决应用程序复杂性带来的编码问题,它能够让开发人员从将一些具体的组件或者类型的生命周期管理从复杂业务逻辑代码的编写中消除掉,交给专门的可以配置管理的一个特定类型来管理。这就是Spring Framework实现的核心思想。

本文来自投稿,不代表本人立场,如若转载,请注明出处:http://www.sosokankan.com/article/1401847.html

setTimeout(function () { fetch('http://www.sosokankan.com/stat/article.html?articleId=' + MIP.getData('articleId')) .then(function () { }) }, 3 * 1000)