@mircode
2017-03-17T14:00:24.000000Z
字数 10170
阅读 609
组件化
分布式组件
OSIG中文社区:http://www.osgi.com.cn/topic/bj
OSGI.NET:http://www.cnblogs.com/baihmpgy/p/3305215.html
云栖:https://yq.aliyun.com/search/articles/?q=OSGI
BNDTOOLS:http://www.javafxchina.net/blog/
Fragment :http://www.myexception.cn/operating-system/1498109.html
“分而治之”,是解决复杂软件问题的有效方式之一。面对业务复杂的企业级软件,我们通常需要安装各种标准和规则对软件进行拆分。通过将软件拆分成独立可维护的组件,我们可以分析组件内在的逻辑和组件间的依赖,以方便评估软件在需求发生变更时,对软件所造成的影响,也就是可以很方便的评估出软件的柔性程度。
【软件模式】:http://www.tuicool.com/articles/zYFFVrR
软件自发展至今,面临最大的问题,就是需求变更。软件需求的变更就意味着要对软件代码进行修改,而对于人软件部分的修改,往往会导致意向不带的后果,随着软件迭代版本的增加,软件越来越难以维护。一切归根到底,都是软件内部过于耦合,往往修改一处代码就能起到牵一发而动全身的效果,系统的整体粘连性比较高。
针对代码耦合性过高,给软件柔性带来的影响。软件界,经过大量实践得出,系统架构设计,是概述软件耦合程度的最好方式。划分软件的耦合单位,也就是划分模块。
常见的划分模式有:分层模式,分层模式将软件的各个模块,按照作用和功能,进行划分。层与层之间相互调用。常见的应用有:网络的分层协议,Web开发中的MVC模式等,都是按层划分的软件架构。
然而分层结构,并不能解决需求多变的问题。应对用户多变的许,我们需要一种全新的模式去设计软件,这种模式就是微核模式。微核模式,通过将软件核心功能和用户特定功能分离开来,可以做到软件柔性的最大化。基于微核模式开发的软件,通常具有高度的可移植性,可拓展性,可靠性等诸多特性。微核模式,通常适用与系统需要随时间演化(添加更改现有服务),可能需要频繁的处理用户后期的新需求,还可能需要为不同用户提供不同的服务。
最常见的微核模式,就是我们常用的操作系统。操作系统可以说是人类软件史上最成功的软件。操作系统,将用将系统核心功能封装成软件内核。为上层应用软件,提供了运行环境。可以说,操作系统是一个稳定的内核环境,运行之上的应用软件的崩溃是不会导致系统崩溃的。并且,随着时间演进,操作系统和运行于之上的应用软件,始终能够满足人们的需求变化。可以这样说,操作系统是一款柔性程度很大的软件。
借鉴操作系统和微核模式的成功案例,我们可以据此开发和设计我们自己的软件。我们可以将软件核心或者基础功能,封装成软件的内核。内核为组件提供了运行环境,并且能够管理组件的生命周期。我们通过拓展,内核外围的组件,可以实现对软件最大程度的定制。因为内核标准一致,我们开发的组件,可以安装到相同的内核环境之中。就像操作系统里面,我们可将软件复制到其他的操作系统安装运行一样。组件的崩溃,并不会造成这个软件的崩溃。并且组件的升级,也可以做到不中断软件对应的进程。可以说,通过微核设计的软件,不仅具有很高的柔性,而且还有很高的稳定性。
微核模式示例如下:
上一节,我们通过微核模式,将软件划分为内核和组件。这一节我们说说组件由什么构成,有哪些特性。
对于一个完整的软件来说,通常包含运行该软件的程序,以及用于描述软件的相关文档。对于不同的用户,文档的描述内容往往是不一样的,一般来说分为用户文档和设计文档。无论哪个文档,都是对一个软件信息的描述,我们可以称作元信息。对于程序来说,通常包含软件运行程序代码,以及程序运行所需要的资源,诸如XML配置文件,JPG图片文件等,我们将这些文件称为资源文件。
对于一个标准的组件,通常拥有可部署、可管理、可复制、可组装、可测试等特性。为了实现这些特性,我们通常要合理的划分组件的边界,只有合理的边界才能保证组件可以高效的管理,加快软件的迭代效率。一个标准可独立维护的组件,通常包含三类资源,包括组件运行所需要的资源文件,源码文件,元信息描述文件。通过将这三类文件封装打包,从而形成了一个可以独立维护的软件模块,我们称这个模块为可拆卸的组件。打包后的软件实体,作为一个独立模块,可以在软件内核之上,进行安装,运行等。其显著的一个特点,因为组件具有比模块更高一级的独立性,所以可以复制到任何相同的内核环境进行安装。只要实现了标准接口的组件,都可以运行于软件内核之上,这也就是说我们可以安装第三方厂商提供的组件,这大大加快了我们日常的开发进度,同时也让软件本身拥有了最大的灵活程度。
注意,这里所阐述的组件,并不等同于模块化,这里的组件比模块化更高一级,最显著的特点就是,组件有完整的生命周期,可以独立的安装运行卸载等。模块化,只是划分和设计软件的一种理念。而组件化,是对该理念的进一步拓展和延伸。
一般来说,一个组件通常由一下几部分组成:
源代码
组件运行所依赖的代码文件。包括各类的脚本文文件(.py文件,.sh文件,.bat文件),以及源码编译后可直接运行于内核之上的代码文件。
资源文件
组件运行所依赖的资源文件。这些资源可以是XML配置文件,或者是其他的HTML文件,帮助文件,图标文件等。这些资源供组件独立使用,外部组件不能够进行访问,这一点降低了软件组件之间的耦合性。
元信息描述文件
元信息描述文件,用于描述该组件属性的相关信息。指明了组件名称,组件依赖包,组件暴露包,组件版本等信息。内核会通过解析元信息文件,来完成对指定组件的安装,卸载等操作,同时内核也会更具元信息中的依赖关系,加载该组件对应的依赖包。
元信息属性
属性值 | 属性名称 | 描述 |
---|---|---|
NAME |
组件名称 | 用于描述当前组件的名称,该属性可以用于组件的安装,卸载,启动,停止等操作。并且通过组件名称和组件版本,能够唯一确定一个组件。 |
VERSION |
组件版本 | 用于描述当前组件的版本,属性格式为[主版本].[次版本].[微版本],如:version 1.0.0。一般来说,组件应当实现版本的向后兼容。 |
MAIN |
组件入口 | 用于描述当前组件的入口,通常为一个类的路径,内核通过该类的start方法启动改组件。 |
EXPORT |
导出包 | 用于描述组件内部包,哪些是可以是可以暴露给外部组件引用的,起到对非导出包的隔离作用。 |
IMPORT |
导出包 | 用于描述组件所依赖的外部包。多个依赖包,可以用逗号隔开。 |
ENV |
运行环境 | 非必选,用于描述组件的运行环境。如Java环境,.Net环境等。 |
AUTHOR |
运行环境 | 非必选,描述组件的作者以及版权信息。 |
DESCRIPTION |
运行环境 | 非必选,用于描述组件功能的简端信息。 |
软件设计,从最初的面向过程设计到面向对象设计,以及后来的模块化设计。无不体现了一个原则,那就合理的隔离和分享。比如,在面向对象设计中,我们可以通过private关键字,保护对象中的私有属性或方法,不被外界所访问,也就是能够做到资源的隔离。同时也可以通过public关键字,将对象中的某些方法或属性与外界共享。面向模块设计中,我们通常将对外共享的方法,通过接口的形式对外暴露,而屏蔽掉内部的具体实现。这也是一种隔离和共享的表现形式。
在描述一个组件时,我们也需要对应的机制,去做到组件资源的隔离和共享。正如,组件元信息描述中所提到的,我们可以通过EXPORT
和IMPORT
控制组件内部包的隔离和共享。
我们知道,组件是比包更高层次的抽离。一个组件中,可能包含很多包,对于这些包,可能并不全是用户所需要的,我们需要将用户需要的包通过EXPORT
进行生命,一般来说我们会将接口包通过EXPORT
导出。而其他的包,作为组件的私有包,并不对外导出。这样一方面,即做到了资源隔离,也做到了面向接口设计的原则。
同样的,对于一个组件我们也可以通过IMPORT
导入另一组件EXPORT
出来的包。这种依赖关系,存在与编译期间,可以称作是编译期间的依赖。
组件隔离,可以做到自身私有的包不被其他组件的包所引用,因为内实现往往是不稳定的,对外导出的接口通常是稳定的,所以通过组件的隔离机制,能够使组件之间契合的更加稳定。
通过上一节,我们知道一个组件可以通过EXPORT
将所要暴露的包导出,供其他组件引用。组件的之间包的引用,虽然服用了代码,但也会代理很多问题。简单的依赖关系,如线性依赖,内核只需要沿着每条线找到最终的依赖包依次加载即可。如下图:
然而,对于环形的依赖关系,往往会造成包的循环依赖。针对这种情况,在内核加载依赖包是,我们需要通过特殊标记,标记访问过的包,这样在以后的搜索之中,就可以判断一个包是否加载过,如果加载过就不在加载,从而避免了循环依赖的问题。
组件的版本,标识了组件的一次迭代。通过,组件的版本,我们可以判断组件兼容的相关特性。在组件的元信息描述中我们可知,组件版本由主版本,次版,微版本三部分组成。微版本的变化往往代表着,组件内部细微调整,或者BUG修复。因此,对于主版本和次版本相同,但微版本不同的组件来说,多是可以兼容的。主版本相同而次版本不同的组件,一般可以做到向后兼容,也就是说version 1.1.x
的组件,包含version 1.0.x
的全部实现。而如果住版本不同的两个组件,一般是不能兼容的。我们在使用IMPORT
引入一个组件包的时候,可以附加上版本号,指明要进入的版本。对应的规则如下:
版本 | 描述 |
---|---|
[v1,v2] |
依赖版本v1<=v<=v2 |
[v1,v2) |
依赖版本v1<=v<v2 |
v1 |
依赖版本v1<=v |
综述,组件层次更关注于代码的组织形式,以及代码的隔离和共享的特性。基于标准的组件模型,开发人员按照规范,进行组件的开发,避免了因软件多次跌在,造成软件代码混乱的情况。统一的标准,也有利组件的复用。
上一章中,我们重点关注了组件的静态特性,也就是代码的组织形式和组件的规范。这一章我们会重点讨论,组件的动态特性。
一般来说,软件都会显式或隐式的受到软件声明周期的约束。典型的软件周期包含:安装,运行,更新,卸载。这种状态的变化,不仅存在与软件之中,也存在线程,进程,对象等各自的生命周期之中。
如果你正在使用一个软件,应该去从全局的视角去看待软件的生命周期。首先,你可能会需要安装软件所依赖的环境,虽然大部分软件并不需要安装依赖。当软件所有的依赖条件都满足时,下一步就是安装软件。安装完成之后,你就可以执行他,此时他需要获取各种运行所需要的资源。当你不再需要它时,就可以将其体停止,此时程序就会释放对应的资源。随着时间推移,当软件有新版本发布时,你可以更新软件到新的版本状态。如果,最终你不需要改软件了,那么可以将改软件卸载掉。对于非组件化的软件来说,生命周期只存在于整体之中。而对于组件化的软件来说,你去可以细粒度的管理软件的各个组成部分。对于传统软件来说,生命周期的控制主要通过操作系统来进行控制。对于组件化的软件来说,我们就需要实现一种机制,控制组件的生命周期。
通过细粒度的控制组件的生命周期,我们可将软件的组成部分在任意时间进行自由的组装,就想堆积积木一样,可以最大限度的提升软件的灵活程度。
除此之外,对于一个组件中的生命周期中的诸多操作(安装、更新、启动、停止和卸载),都可以安全的在框架中进行,这就意味着你不需要重启应用进程。
需要特别说明的是,正如一个进程一样,会为器分配一个进程号PID,用于标识一个正在运行的进程。当一个组件,处于生命周期之中时,软件内核也会为期分配一个ID号,根据组件的安装顺序,该ID号会从小到大的分配。该ID号和启动级别一起,可以控制一个组件的启动级别。
对于一个组件的生命周期,我们一般定义了如下6个状态:
状态 | 描述 |
---|---|
UNINSTALLED |
未安装 |
INSTALLED |
已安装 |
RESOLVED |
已解析 |
STARTING |
启动中 |
STOPPING |
停止中 |
ACTIVE |
已激活 |
对应的状态转换规则如下:
对应的含义如下
UNINSTALLED
:未安装状态。对于未安装状态的组件,组件所提供的服务和资源都是不可用的。
INSTALLED
:已安装状态。一个组处于已安装状态,说明软件已经对组件的有效性进行校验。此时,内核会为组件分配一个LONG
型的ID
作为组件的标识,这就类似操作系统的PID
,但是此时软件内核并没有加载和验证组件的依赖关系。
RESOLVED
:已解析状态。组件处已解析状态说明内核框架成功找到了组件的依赖,同该组件所提供的服务也可以被其他的组件导入使用。改阶段,软件内核会更具组件的元信息描述文件所配置的IMPORT
属性,加载对一个的依赖组件。并且已经处于解析状态的组件比未解析的组件优先级要高,版本高的组件要比版本低的组件优先级高。
STARTING
:启动中状态。组件处于启动中状态说明内核正在调用组件start方法,但是组件还没完成初始化操作。如果start
执行完毕,那么组件将转换到ACTIVE
状态。如果期间发生异常,那么组件将退回到RESOLVED
状态。
STOPPING
:停止中状态。组件处于停止中状态说明内核正在调用组件的stop
方法,但是组件还没有完成对组件的停止操作。无论,stop
方法是正常结束还是抛出异常,组件最终都会转换成RESOLVED
状态。我们一般会在stop
方法中,释放组件所申请的资源,终止由组件所启动的线程,并且由该组件注册的任何服务,在组件stop
之后,内核都会进行清除。需要特别说明的是,stop
会停掉组件所提供的服务,但是并不会因为停止一个组件而影响其他组件对其的静态依赖。
ACTIVE
:组件处于激活状态。说明组件初始化已经完成,start
调用完毕。如果没有其他动作,组件将继续维持ACTIVE
状态。
细节:http://osgi.com.cn/article/7289375
为了使用一个组件,必须将组件先安装到软件内核中。当组件安装到软件内核之中之后,软件内核会对组件进行缓存。也就是说,当组件安装到软件之后,那么软件就不再需要组件的原始拷贝。这相当于将组件持久化到了软件之中。今后,对软件进行重启操作,软件将从缓存中启动组件,而不依赖于原始拷贝。持久化组件,可以防止在软件内核重启之后,需要手动重新安装所有组件的问题。
针对组件在软件运行期间,可能发生的生命周期状态的变化。我们需要一种机制,去感知这种变化,而又不会对系统造成过度的耦合。所以,我们采用了标准的事件机制,去监听组件声明周期的变化和内核状态的变化。
这种事件处理机制,在软件开发中,非常常用,如Window
对鼠标,键盘事件的处理,Java
中对Servlet
生命周期的管理。该机制主要由事件状态对象,事件源,事件监听器三部分组成。事件状态对象,指明了系统有能力触发事件类型。事件源,指明了触发事件的源头。而监听器,则在某个事件发生之后,由事件源传递事件状态对象给监听器,监听器在内部对改事件进行处理。
组件在不同的生命周期状态发生切换时,就会触发对应的事件,软件的内核会调用事先注册了改事件的监听器,在监听器中我们可以实现一系列的操作,从某种形式来说,这为组件提供了某种Hook机制,通过这种机制,我们可以实现对组件的动态拓展。对于事件来说,系统内部维护了一个监听者的队里,当事件触发是,会逐个调用监听队里中的监听者,这也就是说由系统或组件触发的消息,是异步执行的,这种机制不会堵塞组件或内核的状态转换。
常见的几种事件如下:
组件事件
事件名称 | 描述 |
---|---|
INSTALLED |
组件安装 |
STARTED |
组件启动 |
STOPPED |
组件停止 |
UPDATED |
组件更新 |
UNINSTALLED |
组件卸载 |
RESOLVED |
组件成功解析 |
UNRESOLVED |
组件转变为未解析状态 |
STARTING |
组件启动中 |
STOPPING |
组件停止中 |
当操作系统启动时,并不是一蹴而就的,各硬件模块,软件模块都需要设定先后顺序,才能保证系统正常启动。对于操作系统来说,当系统启动时,首先需要加载启动系统内核,当内核启动完毕之后,就会启动第一个init初始化进程,当系统初始化完毕之后,将再运行开启自启动的程序,window中我们称之为服务,linux中我们称之为守护进程,最后系统会进入登录界面。
就如同操作系统一般,因为组件之间并不是完全独立的,彼此之间存在依赖关系,所以我们也需要对组件设定启动级别,来保证组件的顺序运行。
跟普通的组件相比,软件的内核也是一个特殊组件,该类组件的启动级别最低一般为0,并且不能通过接口修改系统级组件的启动级别,修改对应的启动级别,会抛出ERROR
异常。因为改类组件为整个软件提供了运行环境,就如同操作系统一般,为全体组件所依赖。而对于其他普通组件,启动级别一般大于0,如果两个组件的启动级别相同,那么内核将按照组件ID从小到大依次启动。系统默认的启动级别为6,也就是软件会运行启动级别小于6的所有组件,这其中包含内核组件(启动级别为0)和普通组件(启动级别小于6)。
启动级别不仅仅可以用于协调内核和组件的启动顺序,而且我们可以通过动态设定系统的运行级别,来达到全局控制组件的启动,停止等操作。也就是在软件运行的情况下,我们可以动态的调整内核的当下的运行级别,来控制组件的声明周期。用过Linux的用户,可能知道Linux有不同的启动级别,可以通过设定不同的启动级别,来启动不同的软件。如对于Linux来说,存在7中启动级别。其中,运行级别3表示控制台命令模式,运行级别5表示图形界面模式,运行级别0表示关机模式。假如当前位于3模式,我们可以通过命令init 5
将系统切换到图形模式。这就是动态调整运行级别的一个案例。同样的,对于运行状态下的组件,我们可以通过动态的设定内核运行级别,来控制不同级别组件的运行和停止。一般来说,系统会停止启动级别大于当前运行级别的组件而启动小于当前运行级别的组件。
例如,如果一个软件有Web
和控制台两种展现形式,对应着有2个组件。分别的启动级别为5和3。软件启动之后,默认会启动所有小于6的组件。所有,系统启动之后,默认会有Web
界面。然而,由于设备限制,肯能不能安装浏览器,这时候启动级别为5的Web
组件,就会多余,此时我们可以将软件运行级别调整为3,此时系统只运行在3模式之下,对应启动级别为5的组件,将停止不在运行。从而达到了动态调整软件运行模式的功能。
A为组件启动级别,R为系统运行级别
软件内核启动时,首先激活事件处理机制,系统组件首先进入STARTING
状态,内核更具组件启动级别和组件ID,逐个调用组件的start
方法,来完成组件的初始化。如果期间发生启动一次,那么内核将抛出ERROR
错误。软件启动成功后,系统内的组件进入ACTIVE
状态,同时由内核广播一个内核STARTED
事件,标名系统启动完毕。
当软件关闭时或强制关闭一个系统组件时,系统将进入STOPPING
状态,然后逐个调用组件的stop
方法,释放各自持有的资源和注册的服务。如果期间发生异常,那么内核将抛出ERROR
错误。最后,关闭所有事件监听程序。
内核事件用于描述软件内核状态的变化,当内核状态发生变化时,分别会触发如下事件,然后交由内核事件监听器,去处理对应的逻辑。
事件名称 | 描述 |
---|---|
STARTED |
内启成功 |
STARTING |
内启启动中 |
INFO |
内核检测到一般信息后发出 |
ERROR |
内核检测到错误信息后触发 |
WARNING |
内核检测到警告信息后发出 |
STARTLEVEL_CHANGED |
内核运行级别发生改变时触发 |
组件的声明周期,重点关注与组件的运行态,也就是组件的动态性。而通过内核的运行级别,我们可以做到对内核之上的组件进行调度和管理。
当运行一个软件是,我们就可以使用该软件提供的一系列服务。因为软件的独立性较高,所以一般情况,不会发生软件之间服务的调用。而组件化的软件却是不然,处于运行状态的组件,通常需要发生调用,也就是互相访问组件提供的服务。所以我们就需要一种模型解决组件之间调用的问题。
从软件的发展历程来看,从最初的C/S
架构到软件到B/S
架构的软件到SOA
模式,模块之间的耦合程度是从紧密到松散的,松散的耦合有利于对软件的修改。SOA
(面向服务的体系结构)是一个组件模型,它讲系统不同功能单元(服务)通过这些服务之间定义良好的接口和契约联系起来。接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构建在各种这样的系统中的服务可以以一种统一和通用的方式进行交互。
从SOA
中,我们可以获益良多,我们可以采用SOA
模型,解决组件之间通讯的问题。当组件启动的时候,我们可以将组件提供的服务同构注册接口,向服务中心注册。对于服务请求者,可以从服务注册中心,获取到服务提供者的实例。然后,通过这个实例访问相关的服务。面向服务的设计体系,能够屏蔽掉服务实现的细节,降低服务提供者和服务请求者耦合程度。
标准的SOA
模型包含服务注册中心,服务请求者,服务提供者三个部分,如下图:
服务提供者将服务发布到注册中心,然后服务请求者可以从注册中心那里查询到对应的服务,从而可以获取到服务提供者的引用,进而发生交互和调用。
【SOA】:http://www.cnblogs.com/binyue/p/5072376.html
当组件启动时,我们可以在start
方法中,向服务中心注册,组件的一个或者多个服务。在发布服务之前,我们需要对发布的服务进行描述,以便其他组件可以查找该服务。服务注册成功之后,将有内核分配一个ID号,用于标识一个服务。当一个组件停止时,这个组件对应的注册的服务也应当被注销。
服务注册属性
属性 | 描述 |
---|---|
SERVICE_ID |
服务ID由内核分配不能修改 |
SERVICE_PID |
服务PID是服务持久化的一个标识,服务PID不会随着内核的重启而重新分配,该ID由用户指定,相当于服务的名称,一般用服务接口类名当做PID |
SERVICE_DESCRIPTION |
服务描述 |
OBJECTCLASS |
服务实现的接口 |
SERVICE_RANKING |
服务等级,服务发现时会选取等级较大或者ID数值小的作为首选服务 |
SOA是面向服务的,一个服务可能存在多种实现,所以我们就需要某种机制,能够在众多的实现之中,选择一个合适的服务发起调用。这个时候,我们可以采用服务注册时,所设置的SERVICE_RANKING
来选取合适服务。服务的等级越高,服务被选取的优先级也就越高。当服务SERVICE_RANKING
相同时,内核将选取SERVICE_ID
较小的服务,作为提供者。因为SERVICE_ID
越小,意味着服务业注册时间越早。一个服务发布后,可以通过修改服务的元数据,来调整服务适用环境。
因为组件在运行期间,可能发生停止,卸载等操作,也就是说服务多半是不稳定的,我们不能保证一个服务持久的可用,而且当服务数量很多事,服务依赖关系很复杂时,我们不可能通过简单的设定组件的启动级别来解决服务的依赖问题。所以,我们就需要一种服务追踪的模式。当服务可用时,我们才发起调用。当不可用时,我们就堵塞请求服务的方法,直到服务可用。这种机制类似Socket编程的堵塞模型,只要当数据可用时socket.read()
才会返回。
内核应该有能力提供对服务的监听,以下为主要的服务监听事件
REGISTERED
:服务已被注册可以马上使用
MODIFIED
:服务的元数据发生更改
UNREGISTERING
:服务正在被注销
我们可以通过注册服务的监听者,监听这些事件。其中要说明的一点是,组件的声明周期的事件触发是异步的,而服务的事件触发是同步的。
服务释放是指,服务使用者不在使用一个服务时需要将改服务释放。但是,一个组件释放一个服务,并不代表这改服务就会终止,因为还有其他组件可能引用着服务。当我们注册一个服务时,服务的引用计数器就会加1,当服务释放时计数器就会减1。而服务注销则不同,服务注销本质上来说,是将该服务从注册中心中移除,这样所有的引用改服务的组件,将无法访问到该服务。
组件提供服务,在于支持组件之间对象基本的交互。而组件之间的IMPORT
机制,则在于提供组件包或者类之间的交互。