大牛的杰作,赞一个
读前的话:由于本文涉及内容颇多,若有地方读来不很明白,建议先跳过,整体上有个认识后,再回过头来理解。作者认识有限,若有错误,欢迎斧正:)原文地址: NetBPM工作流的架构设计及实现浅析(转载请保留)
NetBPM组件接口
NetBPM由一系列的组件构成,每一个组件都实现一个核心接口(采用Facade Pattern)。不同组件各自负责的核心功能根据WfMC规范而来。
NetBPM接口图:
- 为流程开发者提供的接口(Process Developer):该接口负责加载流程定义到NetBpm引擎。首先,流程开发者依照nPdl创建一个流程定义,并将其打包成流程定义压缩包(该包包含一个业务流程的所有信息),然后通过NetBPM Web界面或者是其他方式把流程定义压缩包加载到NetBPM引擎,在加载过程中流程定义压缩包将会被解析被保存到NetBPM数据库中。
- 为用户提供的接口(User):这里用户表示执行流程的人。流程运行主要有2种行为:开始一个流程和执行一个活动(activity)。开始一个流程将创建该流程的一个流程实例,一个流程实例对应流程定义的一次执行。一个流程实例包含了一个或多个并行的flow-of-execution(见flow)。对于处在activity-state(活动节点,见nPdl)的flow,系统一定会指派一个具体的人(或者组)或者第三方来执行活动(activity)。执行活动是Execution Interface的第二种行为。当然了,运转接口还会实现一些其他的方法如获取任务列表,获取有效流程定义列表等。
- 外部IT系统(External IT Systems)和NetBpm引擎之间的接口:外部IT系统能以2种方式实现和NetBPM的交互。
- 系统发起交互:系统直接和外部IT系统交互。当系统想要触发流程中的某一个action时,它必须使用上面提到的Execution Interface。
- 流程发起交互:对于流程发起交互这种类型来说,需要有Interactors。这些Interactors是流程定义的一部分,它包含在流程定义包内(实际上Interactors就是能够访问FlowContext的.NET程序集,也就是我们后面要说到的包含委托类的程序集合)。Interactors在FlowContext(FLOW运行上下文环境)和外部IT系统之间建立起了通信渠道。
- 组织架构数据(Organisational Datastore)和NetBpm之间的接口:仔细想想,NetBpm是不是还缺了点什么,没错,那就是组织架构的信息:比如说人、团队、部门、角色等。因为在现实情况中,对于不同的组织结构,组织架构信息可能被保存在不同类型的数据库中,如LDAP系统,关系型数据库等。为了让NetBPM能够在一个现实的组织架构中实现快速部署,NetBPM把所有的组织架构信息都聚集在一个组件(Organisation Component)中。这种做法也就是我们通常说道会话门面模式(Session Façade Pattern),它使得NetBPM访问来自不同数据源的组织架构信息变得更为简单。
下面我们逐步介绍NetBPM的各个组件,下面是NetBpm组件结构图:
定义组件(Definition Component)
该组件实现核心接口IDefinitionSessionLocal,用来解析、加载流程定义压缩包,并将其保存到数据库中。此外,它还提供获取某个流程定义,获取所有有效流程定义列表等和流程定义相关的方法。
运转组件(Execution Component)
该组件实现了核心接口IExecutionSessionLocal,它是NetBPM引擎推动核心,如前面如述,它主要实现2个方法:开始一个流程实例(Start ProcessInstance)和执行一个活动(Perform Activity)。另外,它还提供获取用户任务列表,取消流程实例等辅助流程运转的方法。
组织架构组件(Organization Component)
该组件实现接口IOrganisationSessionLocal,它把所有的组织架构信息都聚集在一起,包括Users、Groups和Memberships。运转组件在为activitie-state指定执行者时,需要有关user和group的信息。这些信息将以只读模式由组织架构组件向运转组件提供。下面是NetBPM默认的组织架构组织数据模型。在此基础上,我们可以很方便的实现自定义的适合实际项目需求的组织架构组件,以和我们的用户数据库或者是LDAP系统相关联。
NOTE:NetBPM源码中实现的只是一个简单的组织架构模型,但它提供了一种思考方向,我们可以很方便在此基础上进行扩展来实现满足切实需求的自定义组织架构组件:)
日志组件(Log Component)
该组件实现接口ILogSessionLocal,用来记录工作流引擎的操作痕迹,象属性值的更新(如用户提交的表单被上级打回重新填写,那么就会出现多次表单数据,这就是一种属性更新),委托类的调用情况等都会被记录下来保存到数据库中。
任务调度组件(scheduler component)
该组件实现接口ISchedulerSessionLocal,在现实的业务流程中,我们经常会遇到需要定时触发某个任务的需求,任务调度组件正是作用于此。引擎或者是第三方把需要在某个时刻执行的任务信息(包括任务执行环境、执行时间等)封装成Job保存到数据库中。任务调度组件将按照指定的时间间隔不停的扫描任务表,根据执行时间对比来执行定时到了的Job。
管理监控组件(Admin Component)
该组件用来对流程定义,流程实例执行情况等进行监控。(源码待完善)下面是NetBpm核心项目在Visual 该组件用来对流程定义,流程实例执行情况等进行监控。(源码待完善)
下面是NetBPM核心项目在Visual Studio解决方案中的源码结构图,所有组件都包含在Workflow文件夹下,每一个文件夹分别对应实现了一个核心组件。
NetBpm中的几个重要概念
flow
flow不知道翻译为什么好,在JBPM中叫做Token,翻译为令牌,这里我们就叫做flow吧。它代表activity-states(活动节点,见nPdl)的一个“thread of execution”,相当于是一次流程实例执行过程中在流程定义模板中的令牌(还真难描述清楚,看下面一起理解:))。前面说了,一个流程实例代表一个流程定义的一次执行。如下图所示,流程实例的状态可以看成是一颗flows树。
当开始一个流程实例后,在start-state(开始节点,见nPdl,start-state实际上可以看做是一种特殊的activity-state)引擎将自动产生一个名为root的flow。flow中包含了该流程实例的相关信息,如属性值、流程定义信息、flow所在activity-state的执行者等。root flow在遇到fork(分散节点,见nPdl)之前,将更新其带有的实时信息(如执行者、属性值等),这些实时信息随着流程的运转而变化。遇到fork后,根据ForkHandler委托类,root flow将分散成若干(>1个)forked flow(我们可以把root flow称为这些forked flow的父flow)。若是分散为多个,则此时forked flow将并行运行,父flow则暂时退隐,只至到join(汇聚节点,见nPdl)汇聚,引擎将根据join定义的JoinHandler委托类来确定激活父flow的机制。
NOTE:关于fork和join机制,请参考nPdl fork、join小节一起理解。
attributes(属性)
attribute用来表示流程实例中的变量。一个attribute-instance(属性实例,也就是属性值)代表一次流程实例执行过程中对应属性的实例。属性一般有几种,一种是activity-state(包括start-state)需要用户或者第三方来填写(更新)的属性(一般对应用户Web界面表单上要填写的值),一种是角色对应的属性,还有一些用作标识属性(用来存储某些信息以方便后面的节点运用这些信息处理逻辑判断)。
- serializer(属性的序列化):
Serializer和HtmlFormatter都是委托类,Serializer负责把属性实例在.NET-Objects和文本两种状态间进行转换,以方便把属性值存入和取出数据库。而Htmlformatter主要用来联系属性值和Web Forms显示时对应的http-text。
- attribute的范围:attribute-instances(属性实例)和flow有关。在process-definition(流程定义根节点,见nPdl)节点中定义的attribute和root-flow关联,而在concurrnet-block(并行运行块,见nPdl)中定义的局部attribute实例则和forked flow关联。一个flow能够“看见”和它关联的所有属性以及该flow的父flow的所有属性。
引擎运行时上下文环境(Execution Context)
因为图片太大,关于继承IHandlerContext的接口关系图查看点击这里
ExecutionContext(ExecutionContextImpl类型的对象,我们暂且翻译为运行时上下文环境:)) 包含了引擎在运行时和流程实例相关的所有有用信息(上文中提到的flowcontext就是一种ExecutionContext),它在委托类(包括流程定义压缩包中程序集中定义的委托类)和流程引擎之间建立起了相互联系的渠道,这是非常关键的。如上面ExectutionContext 类图所示,ExecutionContextImpl实现了下面这些接口:IAssignmentContext、IDecisionContext、IForkContext、IActionContext、IJoinContext、IProcessInvocationContext、ITaskContext,这些接口都是为匹配特定的委托类而设计,它们规范了一种特定的上下文环境,如IActionContext匹配action类型委托类,IDecisionContext匹配DecisionHandler类型委托类等,而ExectutionContext是所有这些特殊的运行时上下文环境的一个综合。当引擎在运转组件把ExectutionContext作为参数传送到具体类型的委托类时(关键:这就是委托类和流程引擎建立联系的方式),ExecutionContext对象将“拆箱”成为特殊的Context,如:把ExecutionContext对象传给action类型的委托类Run()方法时,ExecutionContext对象将拆箱为ActionContext对象以限制其能够调用的方法。
如“继承自IHandlerContext的接口”图中所示,这些接口都继承了同一个接口IHandlerContext。IHandlerContext是一个规范了最基本的委托类处理上下文环境的接口,实现该接口的继承几口也就都要实现IHandlerContext中定义的方法,当然每一种继承它的特定接口又都可以具有其特定的方法。我们先看公共接口IHanlderContext类图:如上IHandlerContext接口图所示,这些接口都继承了同一个接口IHandlerContext。IHandlerContext是一个规范了最基本的委托类处理上下文环境的接口,实现该接口继承接口的类也就都实现了IHandlerContext中定义的方法。当然,每一种继承它的特定接口又都可以具有其特定的方法。我们先看公共接口IHanlderContext类图:
大多数的方法,我们从方法名称就可以看出其具体作用了,这里重点介绍下GetAttribute()方法和GetConfiguration()方法,这是我们在写委托类实现时,要经常用到的2个方法。GetAttribute()用来获取流程实例中的属性值,包括流程前面处理者产生的属性值(如用户填写表单的值)和前面处理引擎事件中设置的表示属性值(注:IActionHandler具有SetAttribute()方法,该方法经常用来标识属性值,供后面程序逻辑用)等。而GetConfiguration()用来获取流程定义中设置的parameter(参数,请nPdlparameter小节)。
下面再来看几种典型的特定上下文环境接口:
- IAssignmentContext:为IAssignmentHandler委托类提供引擎上下文环境,该接口定义了获取组织架构信息的方法,具体实现方法见类图。
- IActionContext:为action提供引擎上下文环境,注意其具有SetAttribute的方法,可以为流程实例中的属性赋值,具体实现方法见类图。
- IForkContext:为IForkHandler委托类提供引擎上下文环境,它定义了如何从父flow分散forked flow的方法,见类图。
- IJoinContext:为IJoinHandler委托类提供引擎上下文环境,它定义了获取其他并行flows的方法,见类图。
- IProcessInvocationContext: 为IProcessInvocationHandler委托提供引擎上下文环境,实现方法见类图。
- 其他的委托类型上下文环境除了实现基本IHandlerContext方法外,没有特定的方法,请参考IHandlerContext方法。
委托类
在前面我们一直提到委托类,那么委托类到底是什么呢?这里委托的概念指的不是.NET Framework中delegate,这里可以理解它为“委托、代为处理”这样的概念就好。
NetBPM被设计成通用的流程处理引擎,NetBPM核心执行引擎只负责处理最基本的业务流程逻辑,所有不定的逻辑都被委托给一系列的接口,这些接口称作Delegation Interfaces(委托接口),而实现这些接口的类就是委托类。流程定义约定在什么场合使用什么委托类型,引擎和委托类如何关联也在流程定义中完成。
为了达到最大的可扩展性,流程开发者在流程定义时可以选择下面任意一种委托类实现方式:
- 使用已经在NetBpm引擎中实现的有效的委托类。(具有通用性的委托建议采用这种方式在引擎中定义,以便重用)
- 使用自定义的委托类,以.NET程序集的形式通过流程定义压缩包动态加载。
正是方式2这种形式给NetBPM带来了极大的灵活性,把只适合于某个特定流程的的程序逻辑(这些往往占了大多数)以.NET程序集的形式定义在流程定义压缩包中,NetBpm通过流程定义组件将其解析并保存至数据库。当引擎运转流程需要调用委托类时,引擎利用反射技术实例化出委托类对象,然后利用上文介绍的运行时上下时环境(ExecutionContext)建立起委托类和引擎之间的交互渠道,这真是一个令人兴奋的设计:)
下面是NetBpm中的委托类型(建议和ExecutionContext一节一起理解):
- AssignmentHandler:
实现接口IAssignmentHandler, 它可以和组织架构中的IT-System相互通信,用来为activty-state指定执行者。IAssignmentHandler具有唯一的方法SelectActor(IAssignmentContextassignerContext),其中的assignerContext是其和引擎联系的渠道; - ActionHandler:实现接口IActionHandler,它用来执行一段流程引擎外完成的程序逻辑,关于action在哪里执行,什么时候执行等在流程定义中被定义。actions可以被看作一系列流程执行事件的侦听接口,它具有方法Run(IActionContext actionContext),actionContext是它和引擎联系的渠道。
- DecisionHandler:实现接口IDecisionHandler,它用来选择决定走哪一条transition(边,见nPdl),具有方法Decide(IDecisionContext decisionContext),该方法返回选择要走的边的名称,decisionContext是其和引擎联系的渠道。
- ForkHandler:实现接口IForkHandler,当执行到fork节点时,Forkhandler用来决定从fork流程的边中哪些边需要“forking”。它具有方法Fork(IForkContext forkContext),forkContext是其和引擎联系的渠道。注:forkhandler可以在同一条边上分散多个forked flow。
- JoinHandler:实现接口IJoinHandler,当执行到join时,JoinHandler决定是否要激活父flow,它具有方法Join(IJoinContext joinContext),joinContext是其和引擎联系的渠道。
- Serializer:实现接口ISerializer,具有方法Serialize(Object valueObject)和Deserialize(String text),该委托接口负责用来转换属性值,把属性值在.NET-objects(.NET对象)和text-format(文本)之间转换。其中,text-format(文本)用来存储属性值到数据库中。
- HtmlFormatter:实现接口IHtmlFormatter,该委托主要用来在属性值和其相对应的web界面元素之间建立联系,同时负责解析web界面元素具有的值。若移植到ASP.NET平台,该接口建议重写。
- ProcessInvocationHandler:实现接口IProcessInvocationHandler,该委托实现收集子流程的初始化数据、收集处理结果、指定完成子流程后要流入的边等方法,processInvocationContext是其和引擎联系的渠道。
................///////////////////////////.............是否要添加委托类的例子
流程定义版本问题
流程定义的名称与版本
包含在一个流程定义压缩包中的信息叫做流程定义。NetBpm中,流程是由字段name来区分的,也就是说引擎根据流程的name来判断两个流程是否相等。在流程定义包中不能指定版本,当一个流程定义被引擎加载后,NetBpm将检查是否有该流程定义的旧版本。如果有,NetBpm将自动设置该新加载进来的流程版本为所有存在的旧版本流程定义中最高版本数目基础上加1。
流程执行与版本
当调用运转组件获取流程定义列表方法时,只能获取到每个流程的最高版本流程定义。这样做保证了用户总是从最新版本的流程定义开始一个流程实例。当新的流程版本加载到NetBpm时,所有正在运行的旧版本的流程实例将保持在原来流程定义方式下运行。
委托类与版本
关于版本的另外一个方面是委托类。不同版本流程定义的委托类不是共享的,也就是说每个流程在执行时只会“看到”它自己流程定义的委托类。
异常处理机制
NetBpm作为一个集成平台,当流程运行时,肯定会依赖公司很多其他的IT资源,一旦这些依赖导致流程执行时出现错误,NetBpm提供了3种解决机制:
- 忽略错误
- 把错误日志记录下来(默认采用的机制)
- 错误执行回滚操作(rollback)
执行回滚机制中,流程实例将会被回滚到执行activity之前的状态。如果是对NetBpm 调用Eecution Interface时发生流程错误,所有的流过的transition(边)都会执行回滚。
流程定义元素类图
关于流程定义的详细情况,参见nPdl。
NetBpm中使用的框架或组件
NetBpm中用到的框架、组件、工具比较多,它们大都是优秀的开源项目。如Castle,NHibernate,Log4Net, NVelocity,NUnit,NAnt等,不要被这些框架吓倒,实际上,它们仅仅只是“框架”而已:)
IOC容器――Castle
NetBpm使用了Castle框架,主要用它来实现IOC(控制反转或者说依赖注入),以依赖注入的方式加载核心组件,如DBClassLoader,流程定义组件,运行组件,日志组件,组织架构组件,任务调度组件等。在Web程序启动的时候,根据配置文件,所有的核心组件都将注册到Caslte IOC容器中,以后当需要使用某个组件的时候,只需利用系统提供的ServiceLocator(服务加载工具类)从容器中获取实例即可。另外,在任务调度组件中也有用到Castle的Startable Facility(注:大家把facility理解为注入性质的,对Castle IOC内核容器的功能扩充组件。Castle本身自带实现了一些faciltiy,开发者也可以自定义facility),该facitlity主要用来自动运行程序(这里用来自动间隔扫描任务表,进行任务调度)。
Castle是.NET平台下一个功能强大的优秀开源框架,关于Castle的更多信息,请看Castle官方网站。另外TerryLee的博客中关于Castle的中文资源也很丰富。
数据持久层―― NHibernate
NetBpm中NHibernate组件是作为Castle的一个facility存在的,它用来实现NetBpm数据持久层, 并方便的实现了事务支持。关于NHibernate的更多信息,请看NHibernate官方网站,关于NHibernate作为Castle的facility相关请看这里。
示例web层――MonoRail
大家在Demo演示体验的时候,一定很奇怪,没有看到熟悉的.aspx页面,而是.rails页面,为什么呢?答案就是NetBPM的Web层采用的是MonoRail框架,而不是我们熟悉ASP.NET框架。MonoRail是Caslte框架下针对web层编程的一个子框架,它从Ruby Rails获取灵感而来,采用架构清晰分工明确的MVC模式。NetBPM采用的使用NVelocity作为页面解析引擎的MonoRail,它只是对NetBPM核心API的一个Web界面演示示例,用来告诉我们该怎样从Web层调用NetBpm API。所以,虽然MonoRail有其独到之处,但是在其被普及并有好用工具支持之前,我们只需简单了解下它的运行机制,用以熟悉web层如何利用NetBpm API,不需要了解它太多。用我们熟悉的ASP.NET实现Web部分显然是更好的选择:)。关于MonoRail的更多信息,请看这里。
系统日志 ――Log4Net
log4net大家一定不陌生了,NetBpm使用它来记录系统日志,关于log4net的更多信息,请看Log4Net官方网站,网上中文资源也很丰富.
单元测试工具――NUnit
单元测试工具NUnit一直是大家用来单元测试的利器。 NetBPM源码中已经建有几个测试工程。关于NUnit的更多信息,请看NUnit官方网站。
注:移植到.NET Framework 2.0下,可能要更改其版本。
后记
NetBPM的设计无疑是巧妙的,但是现阶段的它显然还不是一个完美的工作流引擎,缺乏如JBOSS这样的强大后盾作支持,中途又遇上强敌WF,NetBPM远没有其兄弟JBPM风光,更新没有它快(JBPM已经出3.0版本了),获取的支持也少许多, 但是.NET平台下能有这样一个优秀的开源工作流项目是十分可贵的,如果您正在WF中苦苦挣扎,也许,开源的NetBPM将带给您一个惊喜:)
待写:NetBPM工作流nPdl详解,一个NetBPM现实生活中请假审批示例
NetBpm 组织架构(4)