【SQL开源代码栏目提醒】:网学会员,鉴于大家对SQL开源代码十分关注,论文会员在此为大家搜集整理了“工作流引擎入门及源码分析 - 计算机教材”一文,供大家参考学习!
引擎入门 首先 Flow的引擎模块是整个工作流管理系统的一个重要组成部分。
从
代码组成的角度的来说工作流管理系统是由定义器模块和引擎模块两部分构成的从
代码功能上说引擎是工作流管理系统中负责流转控制和数据传递的后台程序。
虽然本文的重点是在讨论引擎模块但是定义器模块和引擎模块的本质属性是相同的把握了引擎中关键的程序属性自然也就撑握了定义器的本质。
那么这种本质属性是什么呢无论引擎还是定义器从程序分类的角度上看都属于数据库应用程序的范畴。
因此了解数据库应用程序构成撑握数据库应用程序的写法熟悉数据库应用程序的工作原理是引擎入门必须要撑握的内容建议写一些小的应用例子来上手。
了解引擎的程序本质后可以再更进一步看看引擎是一种怎么样的数据库应用程序。
引擎是部署在应用服务器JBoss等中的Web应用。
这里的Web应用可以是Web Application也可以是EJB Application这些应用就是数据库应用
程序的具体实现形式。
因此入门的第二步就是学习J2EE应用服务器的用法深刻撑握Web应用程序的概念、工作原理和写法例如可以先部署一个简单的Web Application然后通过客户端访问然后再
学习简单的Web Application的写法接着是部署EJB应用程序以及EJB应用程序的写法最后可以写一个带数据库操作的Web应用程序。
第三步就是部署引擎包括Flow引擎EJB应用和Flow任务管理器Web应用并运行现有的工作流实例这时候就能体现第二步工作的作用你对应用服务器的工作过程越了解对各种应用类型的原理越熟悉也就越是能理解引擎做了什么是怎么工作的。
通过以上三个阶段的学习可以撑握引擎部分最概要的内容也是从一个抽象和全局的视角来把握引擎模块的
代码形式和功能。
从
代码形式上看引擎是一个数据库应用程序通过启动引擎运行现有工作流实例可以从逻辑上把握引擎的功能。
引擎
代码概观 第四步在了解引擎的大致工作流程后就可以浏览一下引擎
代码的全局结构。
整个引擎
代码组织在包org.flow.engine下。
在engine包下覆盖的类和子包如下 GlobalAPI EJB包括GlobalAPI.java、GlobalAPIHome.java和GlobalAPIBean.java这三个类是EJB必须的以后就用名称“EJB”来指称这三个类封装了供任务管理器页面调用的全局函数。
这些全局函数一般都是调用engine包的子包下的类的方法来实现的。
子包core封装了引擎模块的核心线程结束事件线程自动调用线程超期处理线程等等。
子包dbconnection封装了引擎模块中获取数据库连接的方法。
子包definition封装了
工作流中“定义性质”的概念。
子包instance封装了工作流中“实例性质”的概念。
子包factory封装了 增加“实例性质”的表记录的方法 和 获取“定义性质”的表记录的方法。
子包transfer封装了有关数据映射的概念。
子包util封装了一些表达式等额外的功能。
至此对以上论述中的许多概念可能还不是很明确例如什么是“定义性质”什么是“实例性质”什么是数据映射等等但这些不是此处的关键在这里只是要让你明白
代码大体由哪几部分组成当你想看某部分功能的
代码的时候应该去哪个包下找。
许多概念的细节会在以后的章节中渐渐明白。
引擎源码分析流程 在看完引擎
代码的全局视图后对于这样一份庞大的源
代码你可能会很困惑该从哪入手呢有过源码分析经验的人都应该知道要从主函数入手以此为出发点来理清整个程序的执行脉络。
但是你所看到的工作流引擎不是一个单独可以运行的应用程序尽管它可以被称为Web应用程序它是部署在应用服务器中并通过应用服务器来调用的这里有点类似的Windows的事件处理过程应用服务器就是消息循环它不停地捕获消息Web应用程序就是消息响应函数由消息循环捕获消息后调用执行。
因此应用服务器才是引擎启动的真正入口引擎只是被应用服务器调用以响应用户请求。
综上述我们可以依照工作流
管理系统的工作流程以一系列有序的用户请求为脉络来分析引擎的
代码。
整个工作流管理
系统运作的第一步就是启动应用服务器然后应用服务器会在需要的时候加载引擎模块中相应的
代码段。
因此在引擎工作的过程中机器的内存快照可分为以下几个部分从底向上操作系统OS虚拟机JVM应用服务器Jboss引擎模块。
启动应用服务器就是执行应用服务器的主线程该线程的功能是不停等待用户的请求直到收到请求时调用相应的服务来影应用户请求最后又进入等待过程。
在应用服务器启动完成后用户就可以向应用服务器发送请求了。
下表列出了请求的顺序和引擎被应用服务器调用的
代码以及
代码的执行功能。
请求序号 请求内容 被调用方法 执行功能 http://localhost:8080/FLow/startEngine.jsp Org.flow.engine.core中类FlowEngine的startEngine方法 启动引擎的各种线程结束处理线程等 http://localhost:8080/Flow/index.html 应用服务器返回index.html页面 返回登陆页 填入用户名和密码点击Org. flow.engine中类GlobalAPI的用户登陆验提交请求log.jsp userLogin方法 证 登陆成功后点击任务管理器中的“新建任务”链接请求tasklb.jap Org. flow.engine中类GlobalAPI的getAllManualStartProcs4User方法 返回所有可以由当前
登陆用户启动的流程定义 . 点击流程定义列表中的某一流程链接请求tasklist.jsp Org. flow.engine.factory中类FlowFactory的createStartProcIns方法以及 Org. flow.engine.instance中类ProcessInstance的start方法 新建流程实例并启动该实例 . 自动调用AllWorkItem.jsp Org. flow.engine中类GlobalAPI的getAllNewWorkitems4User方法 返回当前用户的所有任务项 点击某一任务项链接请求DoWorkItem.jsp Org. flow.engine.definition中类Acitivity的getApplication方法 转到当前任务项所对应的具体应用页面 点击应用页面中的提交按钮 Org. flow.engine.instance中类WorkItem的finish方法 结束该任务项并结束该任务项对应的活动实例 撑握以上
代码分析流程后如果深入的看其中某一方法的内容你发现里面有大量操作数据库的
SQL语句要想理解这些
SQL语句的含义就必须知道数据库中有哪些表以及这些表所要表达的逻辑含义。
下一章节就将重点讲述这方面的内容。
引擎数据库表结构和工作流基本概念 目前工作流管理系统后台数据库中的表有50多张要想理解每张表的含义就变得非常困难。
本节力图以从顶向下的方式有层次地来分析每张表的含义。
在现版本的工作流管理系统中每个工作流应用对应一个工程。
工程是对工作流应用的最高层次的封装。
每个工程包含若干数据、流程和执行者。
并且每个工程都有一个启动流程。
工程概念对应的表为FlowPackage表说明可以参考数据库设计
文档以下同。
引擎
代码微观 结合前面章节中提到的
代码分析流程以及各种表所表达的概念现在可以对引擎
代码进行更细致的分析。
在分析过程中对于包含中当前被分析方法中的子方法一方面我们将它们列于关联方法一栏便于查看另一方法在对当前方法的分析中对于这些子方法我们不会作深入分析只用其概要性的功能来分析当前方法这样可以避免因分析深度太大而影响整体的分析线索。
主线方法分析 GlobalAPI EJB 方法private boolean localUserLoginString userID String pwd 概述输入参数userID代表用户从登陆窗口输入的用户名pwd代表从登陆窗输入的密码该方法比较输入的用户名和密码 与 数据库中的用户名和密码记当是否一致一致则返回true否则返回false。
细述该方法主要是执行了
SQL语句select from Flow_User where IDuserID and PWDpwd该语句从表FLow_User中选取了与输入参数一致的记录。
如果表中存在这样的记录则用户名和密码合法否则不合法。
方法public ArrayList getAllManualStartProcs4UserString userID boolean isLocal String DBInfo 概述根据输入参数userID所指定的用户得到所有可以由该用户启动的流程定义。
细述首先执行
SQL语句select StartProcID from FlowPackage该语句从表FlowPackage中选取StartProcID字段下的所有记录也就是得到了所有能够让用户手动启动的流程定义号。
然后针对每一条记录每一个可手工启动流程执行
SQL语句select StarterID PackageID from Process where ID’startProcessID’该语句从Process表中选取当前启动流程的启动者StarterID再用人员表达式解析后判断指定用户userID是否在当前启动流程的启动者列表中如果在其中则将当前流程添加到返回
列表中。
方法public ArrayList getAllNewWorkitems4UserString userID boolean isLocal String DBInfo 概述根据输入参数UserID所指定的用户得到所有可以由该用户执行的任务项。
细述首先执行
SQL语句select ID Ower Executor ProcInsID from WorkItem where StateState.STARTTING该语句从表WorkItem中选取所有状态为开始状态的任务项记录。
然后针对这样的每一个任务项循环执行下行操作1选取当前任务项的Owner字段2如果当前任务项的Owner字段为空表示该任务项未经转发得到当前任务项所有执行者信息针对其中的每一条执行者信息都要先判断其执行条件是否成立在执行条件成立的情况下解析执行者表达式得到执行者列表如果UserID所指定用户属于该执行者列表则将当前任务项添加到返回结果中3如果当前任务项的Owner字段不为空表示会签投票任务或者经转发任务从Executor字段中获取执行者信息。
关联方法WorkItemBean.getExecutorDefinition得到当前任务项的执行者定义信息 FlowFactory EJB 方法public long createStartProInsString processID String starterID 概述根据输入的流程定义IDprocessID在数据库中创建该流程实例。
细述首先执行
SQL语句select from Process where ID’processID’查看数据库中是否存在该流程定义信息如果不存在则出错返回。
然后执行
SQL语句select ID StartProcID Name from FlowPackage where IDselect packageID from Process where ID’processiD’查看该流程定义所对应的工程定义信息如果该流程定义是所属工程定义的启动流程则先创建工程实例。
最后在ProcessInstance表中新增一条流程实例记录方法结束。
ProcessInstance EJB 方法synchronized public void start 概述启动流程实例更新数据库中流程实例的状态。
同时启动流程的开始节点。
细述下图描述了启动流程实例的过程 其中2中的启动工程实例是通过调用工程实例PackageInstance的start函数完成的。
3中检查流程实例是否为Ready状态目的在于防止重复启动一个已经启动状态不是Ready的流程。
流程实例状态的保存在表ProcessInstance中因此3、4分别是对该表的select和update操作。
步骤5在Activity表中查询该流程的开始节点同时通过调用FlowFactory EJB来创建该节点的实例StartActInstance的一个对象然后调用该实例的start函数来启动该实例由于开始节点在
设计时并没有实际作用只有简单的象征作用所以开始节点的start函数只是简单的把实例的状态设为FINISHED。
步骤6在Transition表中查询开始节点的第一个后续节点然后通过FlowFactory EJB来创建该节点的实例同时启动该实例调用其start函数。
关联方法FlowBean.getActIns PackageInstanceBean.start FlowBean.createActInslong procInsID String actID int actType ActInstanceBean.startlong sourceActInsID FlowFactory EJB 方法private long createManualActInslong procInsID String actID 概述创建人工节点实例同时创建该节点的任务项实例。
细述首先创建人工节点的实例主要是在ActivityInstance表中插入一条新的记录同时设置节点ID其对应的流程实例的ID设置其初始状态为STARTTING等。
然后创建任务项的实例。
创建任务实例是在表WorkItem中插入一条记录根据人工节点定义时的定制信息不同记录中对应项的值不同。
ManualActInstance EJB 方法synchronized public void startlong sourceActInstance 概述启动一个人工节点的实例。
更新节点的状态同时创建任务项 细述下图描述了启动人工节点实例的过程 步骤1中从表ActivityInstance中
查询该节点实例的状态如果状态为STARTING创建活动实例的初始状态则标明该活动实例已经启动则结束该操作否则继续start活动实例。
步骤2中得到该节点所有的已经实例化的前趋节点和所有的入线以及定义的路由模式。
具体判断过程如下如果路由模式为XOR则符合路由规则因为活动实例的创建和start函数的调用都是在该节点的前趋节点的finish函数进行的已经调用start函数说明该节点至少有一个前趋节点已经执行结束即已经有至少一条入线为通路如果路由模式为AND首先判断是否为循环线如果是循环线的话则符合路由规则因为循环线是由后续节点指向当前节点的这说明该节点至少已经执行过一次如果不是循环线再判断已经实例化的节点数目与该节点的一般入线不包括循环线的数目是否相等不相等则不符合路由规则。
符合的话继续判断每个前趋的实例是否已经执行结束如果所有前趋实例都执行结束则符合路由规则否则不符合路由规则。
步骤3更新ActivityInstance中该节点实例的状态。
步骤4更新WorkItem表中属于该节点的工作项的状态。
WorkItem EJB 方法synchronized private int preFinish 概述用于人工活动的后续处理检查人工活动是否能够结束。
如果不能结束是否需要动态设定执行者、期限、路由、任务数量等。
返回值含义如下3表示活动超期1表示出错0表示无需动态设置可以正常结束1表示需要动态设置执行者10表示需要动态设置期限100表示需要动态设置路由1000表示需要动态设置任务数量。
细述该方法的主要流程如下 步骤1中检查任务是否可以预结束如果任务项不存在、任务已经超期或者任务已经结束则不能进行预结束。
步骤2判断是否需要设置执行者判断过程如下从表ActivityExecutor读取该节点的类型为dynamicsetter的执行者记录数目。
从表DynamicBatchActExecutor中读取已经设置的动态执行者记录数目。
如果二者相等则不需要动态设置执行者否则需要动态设置执行者。
步骤4判断是否需要设置动态时间限制判断过程如下从表Activity中读取LimitType类型为Dynamic同时动态时间的设置者为当前人工节点的节点计算符合条件的项的数目。
然后在表DynamicSet读取该节点已经设置过的节点计算数目如果二者相同不需要动态设置时间限制二者不同需要设置时间限制。
步骤6判断是否需要动态设置路由。
当路由方式为XOR同时符合条件的连接线多于一条时则需要设置动态路由。
WorkItem EJB 方法synchronized private int finishManualTask 概述检查人工节点任务的完成情况根据任务完成情况来判断人工节点的执行是否结束。
细述该方法首先调用preFinish方法检查是否还要进行人工设置参见对preFinish方法的描述如果返回值为0则无需进行人工设置可以结束当前人工节点的执行否则返回相应的值。
如果不需要进行人工设置活动那么把所有状态为preFinish的任务项的状态标记为FINISH。
最后统计状态为FINISH的任务项的数目如果该数目与任务项的数目一致则表示可以正常结束任务生成实例结束事件插入FinishEvent表中。
如果数目不一致则则不能结束当前节点的执行返回1。
ManualActInstance EJB 方法synchronized public void finish 概述结束节点的活动实例同时判断能否生成后续节点的活动实例能的话根据出线模式生成后续节点的活动实例。
细述下图描述了结束人工节点活动实例的过程 步骤1从表ActivityInstance中读取这个Activity实例的状态如果实例状态为FINISHED或者TERMINATED说明节点实例已经结束。
步骤2更新表ActivityInstance中对应的记录将其状态改为FINISHED。
步骤3根据不同的出线方式创建后续节点的实例具体方式如下如果出线方式为XOR从ActivityInstance中读取NextStartActID根据动态路由设置的结果然后创建相应的活动实例。
如果出线方式为AND则给所有的后续节点创建实例不考虑连接线上的条件。
ProcessInstance EJB 方法 synchronized public void finish 概述结束流程实例。
最后一个节点在执行结束时会产生FinishEvent轮寻线程FinishEventHandler发现FinishEvent后会自动调用该函数。
细述 下图描述了结束流程实例的过程 步骤1、2分别是将表WorkItem和表ActivityInstance中状态不是结束的项设置为结束。
步骤6调用工程实例PackageInstance的finish函数完成的。
步骤7的参数传递过程比较复杂以后再论述。
关联方法PackageInstanceBean.finish FinishEventHandler 方法public void run 概述创建一个独立的线程来轮询FinishEvent表如果有Finish事件进行相应的处理 细述首先通过对表FinishEvent的查询得到Finish事件。
然后对该事件进行处理。
处理的基本过程如下判断该事件的类型是节点结束事件还是流程结束事件。
如果是节点结束事件调用对应节点的finish函数否则调用流程的finish函数。