EOS工作流引擎工作原理

EOS工作流引擎工作原理  

2011-08-02 23:56:05|  分类: 工作流 |  标签: |字号 订阅

转载于: http://lanhy2000.blog.163.com/blog/static/43678608201172115428592/

1. 工作流基础知识

……

2. EOS工作流引擎工作原理

  本文是我在工作之余写的一点我对EOS工作流的了解,我的理解不一定全是对的,可能会与引擎的真正的面目有出入。所以只能提供给大家一点参考。

2.1. EOS工作流引擎核心调度算法

  EOS工作流最重要的组成部分是它的核心调度算法,在我们没有深入研究它的工作原理之前我们认为它的工作原理是在工作项,活动和流程实例对象上加了一些标志位来驱动流程的运转。认为其引擎完全是个由数据库来驱动流程的引擎(安徽二期的工作流平台好象就是以库表来驱动流程的运转),其实它是由事件来驱动流程运转的引擎,数据库只是把引擎运转前后的状态持久化。在我近来在工作之余对其引擎的工作原理进行跟踪才弄明白在EOS帮助文档上介绍的“事件驱动”的工作流引擎。

2.1.1. EOS工作流引擎的事件类型

 

  以上的每个事件都是原子的不可分割的。其中一系列事件的集合通过EOS引擎事件调度机制实现我们平时在工作中经常遇到的如启动流程,结束工作项等等。(在事件类型类中EOS定义了29种事件,但在事件工厂类中EOS定义了26种类型。)

1.1.1. EOS工作流事件调度机制

  EOS事件的调度服务是在工作流引擎初始化时通过服务工厂类加载到内存中(ServiceFactory.initEventService())。用户可以通过服务工厂类(ServiceFactory)取得JVM的唯一事件服务实例进行事务调度。所有的事件程序入口都是事件类(EventService),这个类其实是个接口,其有两个实现类,一个是单线程的实现类SingleThreadEventService(在实现代码中其实不是单线程,而是单例的对象),一个是多线程的实现类MulThreadThreadSvc,(其实现方式不在这里详细说明,多线程的类后面又跟了一大堆的线程池实现代码),在事件服务类中有一个属性类是WFEventDisposer,这个类包含了事件的注册,事件的发布,事件的注册是一个静态代码块实现的。注册了上节描述的29种事件,其实就是把相应的事件代码注册到相应的处理类,事件处理类共用5个(ProcessScheduler,ActivityExecuter,ExceptionHandler,WorkItemHandler,ApplicationHandler),对应事件代码的前5个数字;共有事件的发布有两种,一种是正常发布,一种是无异常的发布(即在具体执行事件时关闭了异常处理)。所谓的事件发布是给事件服务类传递一个事件对象(WFEvent类),这个事件对象包含了事件类型,线程名,事件ID,流程定义ID,活动定义ID,活动实例ID,和工作项ID等等。

  以上简要的描述了事件模型,下面来拿我们平时用的最多的一个构件:结束工作项来详细跟踪它的事件处理。结束工作项可能是最具有代表性的一个流程动作,因为在做这个时间后遍历了整个流程实例的流程:

  1, 用户通过引擎的API调用WorkItemManager类的finishWorkItem方法,该方法通过服务工厂取得持久层的数据访问服务,并根据workitemID取得WFWorkItem对象。做相关的判断后通过事件工厂类的createFinishWorkItemEvent方法创建个事件代码为3004的事件对象(WFEvent)。然后通过服务工厂类取得事件服务类把该事件对象发布给事件处理服务。从此刻就开始了EOS事件调度服务的运转。

  2, 事件服务类(拿单线程事件服务类做例子)拿到这个事件类后把该事件通过WFEventDisposer发布该事件。具体的发布过程很简单,即判断该事件类型是否已注册,如果已经注册则取到改事件代码的注册类。该代码是3004,则应取WorkItemHandler。然后调用WorkItemHandler的invoke()方法,

  3, WorkItemHandler类invoke()中写到:if(event.getType() == 30004) {finishWorkItem(event);}则找到该方法,该方法开始做了相关的判断后做相关标志位的修改:置当前工作项的状态为12,然后判断当前活动是否结束。(大概的算法是取得已经结束的工作项和该活动总的工作项,取得活动定义的多工作项是否启动。如果是多工作项则判断完成个数策略:是按百分比还是按操作员个数等等,做一系列的判断后得到应该结束的工作项,如果小于等于已经结束的工作项则该活动结束,没有启动多工作项则相应的处理要简单点),如果该活动已完成,则调用事件服务的结束活动实例事件createFinishActivityEvent;如果没有结束则判断工作项启动的策略是“at_the_same_time”还是“one_by_one”,如果是“one_by_one”则找本活动实例下的工作项状态为1的工作并启动它。

  4, 结束活动实例是调用事件工厂的方法createFinishActivityEvent,新建一个事件代码为2004的事件。用createFinishWorkItemEvent的方法发布该事件。到ActivityExecuter类中找到finishActivity,该方法修改活动实例状态为7,填写活动结束时间。如果该活动注册了时限则取消活动时限的注册。如果该活动实例定义了结束活动的触发动作则触发该动作(通过WFAppCaller调用)。最后由事件工厂产生一个事件代码为1002的createScheduleNextActivityEvent事件。由事件服务发布事件。

  5, 启动下个活动实例的事件动作是事件工厂调用scheduleNextActivity方法,该方法通过流程定义找到下个环节的转移条件,并根据转移条件和分支模式(全部分支:AND;多路分支:XOR;单一分支:OR)生成一个环节定义列表。引擎首先把未启动的活动实例和挂起的活动实例找到,如果没有则生成一个活动实例。然后生成一个转移对象(WFTransition),最后把待启动的活动实例对象放到一个列表中。根据该列表中的活动定义的启动策略(直接启动,待激活,由规则逻辑指定)来启动活动实例;如果是直接启动活动实例则由事件工厂新建一个事件代码为2001的事件startActivity,如果待激活策略则由事件工厂产生事件代码为2000的事件preStartActivity。同样如果在流程定义中定义了创建活动实例触发的事件则触发该事件,scheduleNextActivity方法做了很多业务处理的事情,所以比较复杂。

  6, 事件服务调用startActivity方法,修改当前活动状态位为2,并向时限管理服务注册时限,然后通过活动执行类的帮助类分派工作项,分派工作项的过程是判断是否是多工作项,如果不是则按参与人员分派,如果是则判断多工作项的启动策略,启动工作项业务处理比较复杂,并没有相应的事件代码对应,在这里不详细介绍。
以上的六个步骤完成了我们平时最常用的完成工作项的方法。综上所述应该能够对EOS工作流的事件调度机制有个清楚的认识,比如结束工作项的事件调度有3004->2004->1002->2001这几种事件的触发。同样还有我们平时比较常用的启动流程实例方法首先是创建一个流程实例,然后开始事件调度:10001->10002->2001,最后是分派工作项。
OSWorkflow里也有自己的调度机制,但在业务上要比EOS简单的多,准确的讲OSWorkflow只有两个概念:steps (步骤) 和 actions (动作)。一个简单的调度过程它可能从一个步骤流转到另外一个步骤(或者有时候还是停留在一样的步骤)。它的调度其实就是一个类:AbstractWorkflow,这个类里面有两个方法:doAction 和transitionWorkflow基本实现了所有的调度(其实也不能算是调度,只能算是状态的迁移)。OSWorkflow最大的优点是在执行调度过程中执行的一系列的Function(在SOA里叫服务模型,在EOS里叫展现逻辑),它在执行客户端的服务时的机制时还是比较复杂的,如果感兴趣在工作之余可以看一下。
还有个最近比较流行的开源的引擎,JBpm,我没看过这个,好象现在又整合到JBOSS下去了,好象很复杂。

1.2. 时限管理服务

1.2.1. 时限的分类

EOS工作流引擎工作原理_第1张图片 

  时限类型有两种:一种是一次触发完成时限,还有一种是循环触发(譬如隔多长时间进行一次提醒)并可设置触发的次数。

1.2.2. 时限计算器 

  在工作流引擎启动时就启动一个JVM唯一实例的时限计算器,该类可以使用引擎默认的。也可以自己去实现一个自定义的计算方法,在配置文件中注册要重写的类名即可。引擎的时限计算器只有两个方法,一个是计算结束时间,还有一个是计算提醒时间。其实是个静态类。

1.2.3. 时限服务的启动

  在引擎中的时限服务有两个,一个是引擎启动的时候启动的时限服务,该服务初始化了时限对象列表;一个是在引擎启动后启动的服务,该服务是对列表中的时限对象进行轮询,触发超时的时限对象对应的触发事件,并移除该对象时限。时限的线程处理用了大量的过程化程序的结构,在这里还是比较绕人的。

1.2.4. 时限的注册和移除

  在流程引擎中的时限服务其实就是在维护一个时限对象的列表,该列表记载了处于运行状态的活动的时限对象。

  在启动一个环节或启动一个流程时判断该活动或该流程的时限,如果该活动或该流程定义了时限则向时限服务注册该时限;在TimerManager类中的注册方法的实现是调用时限服务类的registeTimer方法,往时限对象列表(Vector)追加一条记录。

  在结束活动事件时或结束流程时如果是超时的操作则时限对象列表中没有该活动的时限对象,因为该对象已被时限触发器触发并移除。如果没有超时则要把这个向量列表中的那条时限对象给去掉。在TimerManager类中的注册移除方法的实现是调用时限服务类的unregisteTimer方法,往时限对象列表(Vector)移除一条记录。

1.2.5. 时限事件的触发

  时限的触发完全是后台的线程做的事情。该线程对时限服务所维护的时限对象列表进行轮询,如果发现有超时的对象则触发已定义好的动作,该动作就是我们平时在studio中设的如果超时则干什么事的触发动作。

  对时限的处理是通过java.util.Timer这个类来实现的。是通过新建一个时限任务(MyTimerTask)让Timer来执行。并向该类传递一个OnceTimerHandler对象实例。该对象有个方法timerTrigged就是到了预定时限时触发的方法。该方法首先调用timerHandler类的handlerTimer方法,即如果有触发事件的话就调用上节讨论的事件代码以4开头的事件。然后修改时限类的当前状态为3,完成一次时限触发动作。

2. 流程同步服务

  流程同步服务是引擎自定义的一个对流程实例和流程定义的锁的定义,譬如在做指定下一个环节的参与人(WFAppointParticipantManager中的appointNextActParticipant方法)时先把当前的流程实例给琐住(ServiceFactory.getLockService().lockProcInstance)。然后在方法结束后再把流程实例的锁给释放(ServiceFactory.getLockService().releaseProcInstance)。在同步服务中定义了两种类型的锁,一种是流程定义锁,一种是流程实例琐(两个list),在加琐时检查改ID(流程实例ID或流程定义ID)是否已经在琐列表中,如果在则加琐。在加琐与解锁之间是通过一个线程来操作锁列表(waitingList)实现的。其实现方法大概是在加锁的时候向waitingList添加一个锁对象,然后把线程wait();在解锁的时候向waitingList减去一个锁对象,并把线程notify()。流程同步服务的实现方式还是比较复杂的。尽管只用了七八个类。

3. 组织机构管理

  EOS提供了一套自己的组织机构模型,我们在安徽服务保障三期中引用了该模型。该组织机构模型的服务会话面的实现是在配置文件中配置的,然后引擎采用java的反射机制加载配置类(在引擎中有个叫做“服务定位器”来实现,该服务定位器和我们平时用的一样,只是它是从文件中读取服务定义,隐藏了具体寻址细节)。如果不用EOS提供的组织机构模型可以实现WFOMService接口,并实现里面的方法。估计EOS的原意是提供组织机构模型和引擎服务的松偶合,但在其引擎的实现上好象并没有做到。

  OMServiceImpl类是引擎默认加载的组织机构模型会话面类。该类定义了人员,角色,机构等等,OMServiceImpl2类包含岗位的组织机构模型(目前的引擎的还不支持,没有搞清楚EOS没有把岗位纳入组织机构模型中),但EOS提供的开源的组织机构模型中并不支持。

4. 审计服务

  该服务记载了所有流程模板的变更和对流程实例的操作历史。引擎共定义了39种审计类型,包括模板变更,启动流程,完成工作项等等。由于审计的类型代码和引擎的事件代码,所以引擎在中间做了一层映射,把事件代码和审计代码一一对应(审计代码多于事件代码)。在审计过程中其实是往审计表中加一条历史记录。

5. 日志服务

  引擎的LOG服务很简单,和我们平时用的LOG差不多。在打日志的时候传入JVM唯一实例的日志上下文WFLogContext,在该类中定义了一条日志所需要的日志头,比如等级(@level),操作员(@operator),sql(@sql),时间戳(@timestamp)等等。然后在具体打某一条日志的时候把日志头和日志内容拼装起来形成一条日志。
引擎的日志实现了日志的读写,引用了log4j的RollingFileAppender和PatternLayout。并提供了类似AOP的方法前后拦截打日志的服务(不知道方法前后的拦截日志是在代码中人工加上的还是由AOP代理自动加载的,因为采用了AOP时在编译的时候就把代码插入到要拦截的切入点中去)。

6. 持久层服务

  引擎的持久层和studio里的持久层是采用一样的设计。大概是把数据库的字段和持久层的XML定义一一对应,没有采用像hibernate或者EJB的CMP或者BMP那样很复杂的OR_mapping。由JDBC驱动持久层在系统中显的很高效,但采用数据库和持久层的XML描述文件一一对应所以没有把关系数据的对象化做的很别致。(没有深入的看过代码,可能我理解的不对)。

7. 引擎的缓存

  工作流引擎的缓存是通过一个HashMap来维护的。用有以下几类缓存:流程实例的缓存;活动实例的缓存;工作项的缓存;相关数据的缓存;相关数据Dom的缓存;流程属性数据的缓存,以上几类的实例缓存个数是通过在配置文件中配置的,还有一类流程模板的缓存是在引擎启动的时候就解析流程模板的XML文的定义(因为流程定义是通过XML文来存储在数据库中),解析成流程定义对象并加载到内存中。

7.1. 缓存的配置

  工作流缓存的配置是在wfconfig.xml文件中配置的。共有以下几种配置:

  1. <!-- 对数据库的访问是否使用Cache -->
  2. <configValue key="enabled">false</configValue>
  3. <!-- 流程实例Cache个数 -->
  4. <configValue key="processCacheCount">1000</configValue>
  5. <!-- 活动实例Cache个数 -->
  6. <configValue key="activityInstCacheCount">5000</configValue>
  7. <!-- 工作项Cache个数 -->
  8. <configValue key="workItemCacheCount">10000</configValue>
  9. <configValue key="workItemViewCacheCount">10000</configValue>
  10. <!-- 相关数据Cache个数 -->
  11. <configValue key="relatDataCacheCount">1000</configValue>
  12. <!-- 相关数据DomCache个数 -->
  13. <configValue key="relatDataDomCacheCount">1000</configValue>
  14. <!-- 流程属性数据Cache个数 -->
  15. <configValue key="procInstAttrDomCacheCount">1000</configValue>

7.2. 缓存的实现

  在引擎启动的时候取得工作流配置信息,如果允许使用缓存则初始化上面所述的六类缓存。在初始化的时候引擎默认缓存的存活时间为0x1499700L。缓存的大小为配置文件所配。这样则生成在JVM里的六个Cache类的实例。每个Cache都有一个HashMap的属性,这里面存储了要缓存的对象。在CacheFactory类中又有一个HashMap对象属性,这个对象存储的是Cache对象的集合。就是上面所述的六个Cache类实例的集合。普元的开发人员把该对象起名为Caches。那么在取某个活动实例时就先中缓存中读取,如果找到则直接返回,如果没有则从数据库中加载。

  引擎的缓存并不是直接把从数据库中取得的对象put到map中,而是做了一层优化,把从数据库中找到的对象封装成CacheObject对象,该对象有个链表(LinkedList)的属性对象。该对象有前驱和后继节点,其节点就是我们要put的对象。其具体的优化策略和大多数缓存一样,采用最近最多访问策略,共有两个列表维护,一个是最近访问对象位于首位,一个是最多访问对象位于首位。

8. EOS引擎的精彩之处

  EOS引擎设计上最精彩的地方应该是基于多线程的事件调度机制,在事件处理是下层有个线程池处理事件,其中有个线程拿到了一个事件后就开始他的事件的发布和事件的迁移。(多线程协同工作的程序设计我觉得应该是程序设计中最复杂的地方,不但要屏弃所谓“万物皆是对象”的看法,可能还需要用到大量的goto语句来解决一个面向对象这种思想不好解决的问题,还有在JAVA中线程的调度可能还依赖于程序所跑的操作系统和硬件环境,有很多的不可预料性的存在。我怀疑这个线程池肯定有问题,或者单线程已经足够解决所有问题,不然EOS引擎为什么不把多线程处理事件作为默认的处理方式而是单线程作为默认的呢?)

? 多线程调度的初始化:引擎根据配置文件(wfconfig.xml)里的线程数(event_thread_num)新建了那么多数量的线程。并把这些线程放到已经定义好的线程组里。这些线程对象有个对象属性:EventList,这里装载的是该线程该处理的所有事件对象。然后把这些线程都start();该线程轮询EventList里的事件对象列表,如果对象列表为空则把自己阻塞(wait()),如果有事件对象则取第一个事件然后发布该事件,对该事件的处理还是和单线程是一样的。

  线程池对客户端动作的响应:客户要向引擎发布一个事件首先找到线程组的activeCount,然后根据取得的数量在新建这么多的线程然后enumerate(复制到线程组中)它们。引擎把这些线程对象转换成事件处理线程(EventThread),然后遍历这些线程,然后根据“找到这些线程对象中的EventList里的事件最少的一个线程“这种策略,把要发布的事件对象加到找到的这个线程对象的EventList里,同时唤醒这个线程,完成了一次事件的发布,完成了线程自己和自己的协同工作。

9. EOS引擎的不足

9.1. 组织机构模型

  EOS提供的组织机构模型无法解决按岗位或按行政级别分派工作项。但这是很常见的一种情景(安徽二期的工作流平台好象实现了按行政级别来提交工作项)。网上有一种比较流行的组织机构的模型。

EOS工作流引擎工作原理_第2张图片 

  如果能把这种组织机构模型经演化应用到引擎中去可能会解决按岗位,按行政级别,按职务来设参与人的问题,甚至可以解决一个机构只有一个流程的问题而不是机构的下属机构拥有一套和其他下属机构一样的流程(只是参与人不同而已)。而我们的应用中只需要一套共性的流程和下属机构个性化流程的实施。由于客户在管理可能会有不足或把依靠系统来管理,而我们希望我们系统的不足能在管理上弥补,所以能在这中间达到一种平衡需要找到这中间的平衡点。

9.2. 不能应用在分布式系统中

  由于在引擎启动的时候引擎把流程定义XML文解析成流程定义对象,然后加载到内存中,而发布流程的动作只能来自一台集群节点,固无法把修改后的流程定义同步到其他的集群节点。现在的同步方法是发布流程的时候同时调用其他节点的一个远程方法来实现同步。虽然通过外挂式的解决方案解决了这个问题但没有从根本弥补引擎的缺陷。

  EOS的业务字典的管理也是存在同样的缓存问题,在一个节点建了一个业务字典没办法同步到其他节点中去。

  环节的时限也是用了缓存,一个环节的时限注册是在环节启动时如果定义了时限则向时限服务注册,并在内存中产生一个时限对象,有个后台线程在维护它,当环节结束时取消该环节的注册,移除该环节在内存中的时限对象并删除数据库中的时限表的那条数据。 有一种极端的场景(不可能发生):所有的用户在结束自己的环节后启动了下一个环节(假设该环节的完成时限为一个月)。这个动作是在节点A进行的,则节点A的内存中有了所有下个环节的时限对象。节点A的后台线程在维护该对象,不停的判断是否超时。假设下个环节的所有用户在没有超时的情况下完成自己的工作项,并且操作的发生在节点B,用户操作自己的环节后结束环节并取消时限注册并删除时限表,但节点A的时限对象并没有移除。节点A的后台线程一直在维护着它直到它超时才从内存中删除,造成了内存里的垃圾数据.

  我认为引擎层还是应该有一个类似缓存同步服务(可能我说的不对),譬如在流程未发布和发布成功的状态位的迁移过程中加一个待发布的状态,当发布流程的时候把该流程定义的发布状态定义为待发布,然后有两个线程协同工作,一个线程定时(譬如20S)来轮询待发布的流程定义并加载到一个待发布流程列表(该列表类似操作系统中的信号量)中,如果该列表非空则唤醒另一个流程发布线程,该线程sleep一定的时间(该时间要大于等于轮询的时间保证所有节点都已经把待发布的流程定义都加载到内存中)后发布该流程并把流程置为发布状态,然后将自己阻塞。

9.3. 子流程的设计

  EOS的子流程是作为一个主流程的环节来实现的,我觉得不应该有子流程和父流程的概念,也不应该把一个流程作为一个环节来实现。流程与流程之间的通信可以已一定的接口定义和通信规则来实现,父子关系只是其中的一种(属于工作流协同工作的网状模型),还有链状模型,端到端的模型,并行同步模型。这样如果有了接口的标准既解决了父子关系的流程通信,同时也解决了EOS工作流不同实例的交叉操作,甚至解决了不同工作流产品之间的流程通信(只要遵循了接口定义标准,该接口为WFMC定义的接口4,该接口定义了一系列的互操作层次(好象是8个),但可能并不能满足像EOS的引擎具有中国特色(譬如自由流,抄送……)工作流需求)。

10. 一种理论上的引擎原理

  Petri Net是离散并行系统的数学表示,它的数学表述我搞不明白,只能明白他的表面上的一些东西。它不是为工作流而产生的,但如果能把Petri Net和XPDL结合起来去构造一个引擎不一定是符合实际需求的,但我相信它一定是很有前景的。也是很具有竞争力的。
在Petri Net中主要有四个元素:
1. Place:
Place是一种状态,譬如马路上的红绿灯,他的Place可以是红灯,绿灯。
2. Transition
Transition是从一个状态转变到另一个状态的过程。
3. Arc
Arc是连接Place和Transition的一个有向弧,可以从Transition指向Place,也可以从Place指向Transition,但不能从Place指向Place或从Transition指向Transition,中间一定要有个状态变迁的过程。
4. Token
Token是一个物件,他可以代表任何东西,当Place或Transition拥有足够的Token时才可以从一种状态边成另一中状态。

Petri Net的运作方式
图元定义: 

EOS工作流引擎工作原理_第3张图片 
EOS工作流引擎工作原理_第4张图片 

上图的enter经过fire会变成下面的状态:

EOS工作流引擎工作原理_第5张图片 

  使enter可以fire必须消耗free和wait的各一个token,enter就可以个before和occupied各一个token。依次类推的方式推动流程的运转。

  至此我们可以给PN网这样的过程调度算法这样的定义:如果一个变迁的每个输入库所(input place)都拥有令牌,该变迁即为被允许(enable)。一个变迁被允许时,变迁将发生(fire),输入库所(input place)的令牌被消耗,同时为输出库所(output place)产生令牌。

  使用这种算法的工作流引擎有开源的YAWL,还有BOSSA,大家在茶余饭后可以研究一下。

你可能感兴趣的:(EOS工作流引擎工作原理)