Activiti进行时——企业工作流生命周期贯通

Activiti进行时——企业工作流生命周期贯通_第1张图片

图1:一个典型的审批工作流程

最近做了一次对企业/云平台级工作流引擎Activiti的调查:

  • TA,系出名门——由JBoss公司jBPM4引擎的原作者创立(JBoss公司无奈地已在jBPM5中改用了Drools内核、走了其他路线);
  • TA,植根于Java开源社区,拥有SpringSource、MuleSoft、Signavio等公司以及全球大量拥护者基于最新开发实践的养分支持;
  • TA,“出场费”要比IBM、Oracle等大厂的同类低很多,却同样支持BPMN 2.0国际标准、各大流行数据库、集群扩展等,包括对云平台所需的多租户特性也已有原生支持。

InfoQ上2012年4月就已有过一篇推荐Activiti的文章,本文则是笔者近2个月实战Activiti的一篇简书&共享。

背景概述

目标 ------------------------------------

  1. 用户能方便地修改流程定义的细节(例如改分歧条件、复制添加已有的类似节点),只要不涉及新的表单/服务/编程、就无需委托开发人员出马。
  2. 能以REST服务方式调用,与现有业务系统衔接。比如这次的业务系统是.Net架构,而Activiti则是Java开源引擎,但因为提供了REST API接口(且易于扩展)、能以JSON/XML方式完成轻便的数据交互。
  3. 支持BPMN 2.0国际标准(具体后述)。标准统一之争虽然往往夹杂着企业各自的便利/利益,但毕竟有其行业合理性与权威性(例如繁杂的医疗/金融/物流领域),在行业出现新的需求而升级标准时、只需升级符合新标准的引擎就能适应需求。

成果 ------------------------------------

  • 打通基于REST API接口的 工作流生命周期(不足部分做定制补充)

    1. 流程设计模型:一览、编辑(有模型编辑器)、部署(+允许启动者)-->流程定义
    2. 流程定义:一览、挂起/激活、启动-->流程实例
    3. 流程实例:一览、挂起/激活、查看(当前状态)
    4. 任务:一览(我的任务)、签收/完成(表单变量-->流程变量)
    5. 监听器:触发、处理(e.g.发送任务通知邮件)
  • 不包括

    1. 表单编辑器:Activiti并不带表单编辑器(收费版中有),内置+外置两种表单支持方式能满足入门级业务需求,但多数需集成开发。
    2. 邮件模板编辑器:邮件模板的可视化编辑、也不在工作流引擎的负责范围之内,需根据业务需求定制开发。

资料 ------------------------------------

调研、定制中以下一些资料较为有用,(感谢相关作者&)已有相关基础的读者可据此推断本文的参考价值。

  • 官方文档(含REST API):http://www.activiti.org/userguide/
    汉化文档:http://www.mossle.com/docs/activiti
  • 中文社区::咖啡兔:http://www.kafeitu.me/activiti.html
  • 引擎应用项目范例
    官方:activiti-explorer(一分钟入门), activiti-rest(REST API)
    咖啡兔:kft-activiti-demo
  • 开发环境构筑:
    使用Eclipse构建Maven项目 (step-by-step)
    Maven + Eclipse + Tomcat - 开启项目调试之旅
  • 引擎源码:https://github.com/Activiti/Activiti
    引擎类库API:http://www.activiti.org/javadocs/index.html

工作流概念+BPMN标准

如果希望直接将Demo跑起来边看边理解的话,可以跳到下一节,否则就先随我把概念部分混个眼熟。

工作流概念

制作了一张工作流在生命周期中不同阶段/环节间切换的关系图:

 

Activiti进行时——企业工作流生命周期贯通_第2张图片

图2:工作流生命周期

  • REST API:在此特指Activiti引擎的RESTful接口、以及另行扩展的接口,提供所有工作流解析/处理的核心功能
  • 模型编辑器:在此特指Activiti Modeler,也可选用其他来做定制
  • 流程设计模型:工作流在此处于设计环节,可被可视化地编辑调整(如图1)。其中重要的元素,包括用户任务(与表单画面衔接,接受用户的选择/承认/否决之类的输入;是活动节点中最主要的一种,参见BPMN2.0标准)、监听器(响应各种任务/活动节点上的事件(e.g.已被签收/已被办理)、进行不同处理(e.g.按照特定模板发送通知邮件))。
  • 流程定义:工作流在此处于“已部署”环节,以一个“请假流程”为例的话、一旦被部署、有权限的用户就可以启动/发起该流程申请请假了(诞生一个“流程实例”、见后),流程定义的内容已不可被编辑(只可被挂起/激活),如需做修改、只能对流程设计模型做编辑、并再次部署。换句话说,流程定义、是处于流程从设计环节到运行阶段之间、相对稳定的中间环节。
  • 流程实例:工作流至此已进入“运行中”阶段。一个已经被启动、进入“运行中”阶段的工作流,就好比一个“状态机”,可能处在不同活动节点的不同状态上(还可能并发)、直至结束。当工作流运行到一个用户任务节点(比如“总经理/副总经理承认”)、而该任务的候选办理者有多人时,其中之一(比如副总经理)就可以“签收”,然后“办理”该任务(也可以在界面上隐匿“签收”、一律只显示成“办理”)。
  • 流程状态跟踪器:对“运行中”阶段的工作流当前所处的活动节点、状态做出可视化标识。使用官方组件Diagram Viewer会很方便,也可使用其他方法。
  • 历史流程实例:已经结束的流程,仍可被查询到(包括其中的用户任务、选项输入),这会有助于事后分析统计。
    注意:5.16版本开始还新增了“事件日志机制”,可采用大数据存储分析(参见官方文档)。

BPMN 2.0标准

先上一张诺贝尔颁奖盛典筹备工作的设计模型图:

 

Activiti进行时——企业工作流生命周期贯通_第3张图片

图3:The Nobel Prize Process Diagram

 

基于工业标准的工作流设计,是满足不同领域客户复杂业务需求的有力保障。

BPMN2.0标准工作流的模型编辑与保存,可以在浏览器App形式的Activiti Modeler中进行(并可以嵌入你的业务系统中),在此直接对照着Modeler的图标对主要的BPMN元素做简单介绍:

Activiti进行时——企业工作流生命周期贯通_第4张图片

图4:Activiti Modeler Shape Repository -- BPMN2.0

  • 开始事件(Start Event)
    <提醒>

    • 启动者变量(initiator):一个开始事件的initiator属性中所保存的、只是一个“用于记录启动者user_id的变量名称”,可以算是个“变量声明”属性。当一个用户、例如"kermit"(官方范例用户)、启动了这一工作流时(在流程设计模型被部署、成为一个“流程定义”后、它就能被某一用户启动成为一个“流程实例”),"kermit"就会被记录到在initiator中声明的变量、比如"starter"中,那样就会有一个流程变量starter被赋值为"kermit",并能在后续的活动节点中(e.g.用户任务)被用于任务办理人(assignee)属性的赋值表达式(e.g.${starter})、或是其他的例如分歧条件表达式中(${starter=="kermit"})。详见官方文档。
      开始事件的initiator属性是唯一用于保存“变量名称”的,在整个流程级别、或是任务级别、都存在着类似于candidateUsers/candidateStarterUsers的属性,它们保存的直接就是允许启动流程/办理任务的用户ID(允许多个)。详见官方文档。
      注意:Activiti Modeler的当前版本还不支持设置流程级别的candidateStarterUsers/candidateStarterGroups(允许启动者),Eclipse插件Activiti Designer中可以,但仅供开发者使用;因此需要通过扩展REST API、在“流程设计模型::部署”、或是“流程定义::设置”环节进行设置。
  • 活动节点(Activity)
    <提醒>

    • 用语:Activity通常被翻译成“节点”、包括了各种任务(Task)、顺序流(Sequence Flow)、子流程(Sub-Process)等,一切具备生命周期状态的元素。国内也有使用“步骤”(Step)的,但其实Step的定义似乎应是“节点(Activity)+网关(Gateway)”(@ultimus.com)
    • humanPerformer vs activiti:candidateUsers:这里将引入BPMN标准属性与Activiti扩展属性的对比与互换的话题。
      1.语法上的不同:Activiti扩展确有其化繁为简的优势,请比较两段定义("activiti:"打头的即为扩展),
      a. BPMN标准属性
    
        ...
        
            
              
                user(kermit), group(management)
              
            
        
    
    

    b. Activiti扩展属性

    
        ...
        
        
        
    
    

    2.标准与扩展冲突的解决:详见官方文档
    a.Activiti扩展的前提是:总有简单的方法、转换成标准方法
    b.Activiti扩展的目标是:最终把它们加入到下一版本的BPMN规范中, 或者至少可以引起对特定BPMN结构的讨论

  • 结构(Structural)
    <非重点>

  • 网关(Gateway)
    <非重点>

  • 结束事件(End Event)
    <非重点>

  • 连接(Connecting Object)

    • 顺序流(sequence flow):用于连接各种事件、活动节点、网关的连线。除了网关会对流出的顺序流有特殊限制之外(例如排他网关)、BPMN缺省的流出顺序流(若有复数个的话)是“并发”的、即同时发出。可以对顺序流做条件式限制,即添加一个conditionExpression元素:
    
      
         100 && order.price < 250}]]>
      
    
    

    注意:目前条件表达式只能使用UEL(统一表达式语言),详见官方文档。

开发环境构筑:Eclipse + Maven + Tomcat + Activiti

我所使用的开发环境:

  • Eclipse Java EE IDE for Web Developers, Luna
  • Apache Maven 3.2.3
  • Apache Tomcat 7
  • Activiti 5.16.3(5.16.4起设置方法有变化且官方文档未同步更新):请下载开源代码,而非其运行版

<提醒>

  • Maven的基本配置对初学者来说会多有挫折
    Maven本身是个好东西,开源项目很多对其他开源项目有大量的依赖引用,难免存在交叉引用、版本覆盖、重复下载等繁琐问题,而Maven工具&标准就是为了应对这个而产生的(并成为Apache顶级项目)。
    可是,主要陷阱在于:

    • 国内糟糕的网络访问情况...很多中央仓库镜像会出现超时
    • 部分依赖库会不存在于中央仓库中,需手动下载后放入本地仓库中
    • Maven可以用命令行方式运行、也可用Eclipse插件方式来使用,但有时两者的结果会不尽相同...

    解决方法:

    • 找到好文档,从新建/导入一个Maven项目、编译&运行它开始
      使用Eclipse构建Maven项目 (step-by-step)
      注意: 其中有些章节为【必须读】有些【不必读】
      2.Maven的安装和配置
      2.2.配置
      2.2.1.修改默认的本地仓库位置:必须读 重新设置本地仓库位置,使得你可以更方便地 a.将需要手动下载的依赖库放置其中 b.备份已经成功构建的本地仓库、用于环境移植
      2.2.2.修改默认的中央仓库镜像:不必读 通常是根据项目不同,修改项目pom.xml文件中的配置
      4.使用Maven来构建Web项目
      4.1.创建Maven的web项目:不必读 我们需要的是导入Activiti官方项目、而非新建,具体方法见后“导入已有项目”。
      4.2.使用Maven添加项目依赖包:不必读 导入项目的pom.xml中已经配置好了,除非你有新的依赖包需求。
    • 使用国内经验者已有的“私服”:比如咖啡兔的Maven私服,具体POM设置见后“导入已有项目”。
    • 小组开发时,可考虑在局域网里搭一个“私服”(自己的中央仓库):参考Nexus。
    • 导入已有项目:Activiti-activiti-5.16.3\modules\activiti-webapp-explorer2项目
      • Eclipse中导入的方法:File > Import... > Maven::Existing Maven Projects > Next > 选中项目根目录下的pom.xml文件即可。

      • Maven配置修改(pom.xml文件编辑):m2eclipse插件已经装好的情况下,pom.xml会自动被在表单编辑器中打开(不过通常只需在文本方式下编辑、表单方式下辅助阅读)
        a. 添加中央仓库镜像,比如咖啡兔的Maven私服(不过有时也会断线、需添加其他镜像)

         
             ......
             
                 kafeitu
                 http://maven.kafeitu.me/nexus/content/groups/public
             
         
        

        b. 添加依赖库,比如数据库驱动(这里不需要修改,使用默认h2数据库即可、且无需下载安装h2(已自带,否则反而要改版本号))

         
             ......
             
                 com.h2database
                 h2
             
         
        
      • 数据库连接修改:src/main/resources/db.properties
        (可选)给出一个改用Microsoft SQL Server的例子:

         db=mssql
         jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
         jdbc.url=jdbc:sqlserver://localhost:1433;databaseName=activiti
         jdbc.username=sa
         jdbc.password=
        

        不过相应的,pom.xml中的数据库驱动依赖库就也需要添改:

         
             ......
             
                 com.microsoft.sqlserver
                 sqljdbc4
             
         
        
      • 工作流引擎配置修改:src/main/webapp/WEB-INF/activiti-standalone-context.xml

         
             ......
             
             
             
         
         
             ......
             
         
        

        Activiti工作流引擎配置、目前(5.16.3)采用Spring框架方式加载,以实现不同扩展的灵活配置。上手阶段所需做的修改,主要就是如上属性(分别用于自动生成Demo数据,以及比对引擎与数据库之间的版本、自动新建/升级数据库),更多可参考官方文档。

      • Maven项目刷新:Project右键菜单 > Maven > Update Project...,对于Maven项目来说,单单F5在文件系统层面刷新同步是不够的。

      • Maven项目编译:

        • 命令行方式:较为简便,在你设置了PATH(包含%MAVEN_HOME%\bin)之后,便可在命令行窗口中、进入到Maven项目的根目录(pom.xml所在地)、然后执行Maven的系列命令了,如:
          mvn compile
        • m2eclipse插件方式:更为简便,不过建议先搭建起调试环境(不同于.Net中attach到进程的方式,见后),然后直接在运行/调试前自动编译即可。
    • 导入已有项目:咖啡兔Demo,有了导入官方范例的基础,导入其他例子就很简单。
      • 完整步骤(推荐用Maven方式):咖啡兔Demo Wiki
  • 基于Maven的Tomcat Web项目的断点调试也会有一番周折
    解决方法:

    • 找到好文档,比如:Maven + Eclipse + Tomcat - 开启项目调试之旅,这篇是可行的。网上其他还有很多走弯路的方法。

定制补充REST API接口打通生命周天(周期)

经历过之前的准备,便可以导入这1.5个人月调研开发的成果项目了,上图,

Activiti进行时——企业工作流生命周期贯通_第5张图片

图5:测试入口界面(Web客户端)

如果说activiti-explorer和kft-activiti-demo都是界面友好、前端与后台并重的话,这次项目则纯粹集中在后台(封装Activiti的引擎功能),因为前端界面(包括表单编辑器、邮件末班编辑器)将由.Net页面(一个企业内部门户网站)完成,衔接关系可参见图2(生命周期)。

项目的完成可划分为6个步骤:

  • Step1. 在官方范例activiti-explorer中嵌入activiti-rest

    前文推荐了导入官方范例项目activiti-explorer作为入门(一分钟入门+导入已有项目),在此第一步,我们就是在其基础上、加上对REST API的支持,也就是嵌入另一个官方范例activiti-rest。
    对于缺少J2EE/Spring项目经验的人可能会稍有障碍,其实两个官方范例项目合并的主要破解点如下:

    • web.xml 中的Listener配置:src\main\webapp\WEB-INF\web.xml
      activiti-webapp-explorer2(activiti-explorer的项目名)中的Listener配置,分别是Spring框架的ContextLoaderListener和RequestContextListener;而当你打开activiti-webapp-rest2(activiti-rest的项目名)则会发现,Listener只有org.activiti.rest.common.servlet.ActivitiServletContextListener,这是一个位于activiti-common-rest依赖包中的类、只用来启动和回收一个ProcessEngine类的实例。

      结论:我们无需合并该Listener,因为activiti-explorer已经采用Spring的Bean方式来加载ProcessEngine实例。

    • web.xml 中的Servlet配置:src\main\webapp\WEB-INF\web.xml
      官方范例(5.16.3为止)采用的是Restlet方式的REST实现,因此用到的servlet是org.restlet.ext.servlet.ServerServlet。我们将来扩展REST API将采用Spring MVC方式(后续),但对已有的REST API(5.16.3为止)、我们只能保留现有的Restlet方式入口,不过需注意,activiti-explorer范例与activiti-rest范例所用的org.restlet.application(Restlet方式下的入口类)并不相同,前者用的是一个只做了简单Router绑定的自定义类org.activiti.rest.explorer.application.ExplorerRestApplication、仅供modeler和diagram使用,后者(activiti-rest)用的则是包含较严格认证机制的org.activiti.rest.service.application.ActivitiRestServicesApplication,也因此如官方文档所述、标准REST API的调用都是需要经过Basic认证的。

      结论:合并两个官方范例的org.restlet.application类,即使得activiti-explorer中的Restlet Application类、继承org.activiti.rest.service.application.ActivitiRestServicesApplication,覆盖其createInboundRoot()方法、补充入activiti-rest所需的RestServicesInit.attachResources(router);

  • Step2. 再加入kft-activiti-demo的配置
    如从“资料”中文章可以看到,咖啡兔的范例,贡献了很多国内项目级别的成熟经验,比如:

    • 中文化/国际化UTF-8
    • 对官方标准REST API的调用、和不足之处的补充范例
    • Spring MVC方式的REST API扩展(尽管其结果也多以Spring MVC的ModelView方式返回、但入口与内部处理都足以借鉴)

    这一步,我们所需做的就是把咖啡兔Demo中可借鉴的配置部分嵌入自己的项目中:

    • pom.xml
      最有价值的一段,就是借用咖啡兔的Maven私服(参见“导入已有项目”),否则在国内网络现状下甚至可能寸步难行。
      其次便是对于Spring框架、持续层的支持等等,值得注意的是,activiti-explorer作为activiti官方范例、是继承了其“上层”activiti-root的Maven配置的,而我们自己的项目、应该并不会希望建立这种继承,这种情况下,参照kft-activiti-demo的POM便是个safe的选择。
    • web.xml
      咖啡兔Demo同样使用Spring Bean方式加载,因此不需修改;同时因为咖啡兔Demo启用了Spring MVC架构、而这是我们将来扩展REST API时同样可以用的,因此需要搬运如下这一段:
      
        springServlet
        org.springframework.web.servlet.DispatcherServlet
        
          contextConfigLocation
          /WEB-INF/spring-mvc.xml
        
        1
      
      
        springServlet
        /
      
      
      其中spring-mvc.xml将负责具体的Spring MVC配置(后述)。
    • activiti-context.xml
      Spring框架缺省调用的上下文配置文件是applicationContext.xml,咖啡兔Demo是直接把Activiti引擎所需要的配置写在其中的,activiti-explorer范例则是使用把后台、前端的配置分在两个不同的文件中(activiti-standalone-context.xmlactiviti-ui-context.xml)。作为我们自己的项目,只需合并在一个activiti-context.xml中即可。
    • spring-mvc.xml
      不长,直接留用,只需把中的base-package改成自己的项目包就可以;定制扩展的REST API将以一个个Spring MVC Controller的形式实现,这里的配置将实现对项目包下带有@Controller标签的类的自动扫描、实例化。
  • Step3. REST API学习
    RESTful服务的好处,如前所述、主要是能够使得业务系统所依赖的技术与工作流引擎所依赖的技术(Java)之间解耦。当Activiti引擎作为一个RESTful接口的Web服务运行时(输入输出可以都是JSON数据),浏览器、桌面、移动客户端、基于Java、.Net、Python都可与之交互。

    • URL风格
      仅以获取“流程设计模型一览”为例,这是一个标准REST API中就有的功能(官方文档),如果加上Spring Servlet的映射({servlet_mapping},在web.xml中已配置)等就形成完整的HTTP请求路径:
      GET http://{host:port}/{app_name}/{servlet_mapping}/repository/models
      
      配置时则只需{servlet_mapping}之后的部分,例如
      GET repository/models              //表示获取“一览”
      GET repository/models/{modelId}    //表示获取“某个个体”
      POST repository/models             //表示“新建”一个个体
      PUT repository/models/{modelId}    //表示“修改”某个个体
      
    • 官方的Restlet方式REST实现
      官方的REST API实现(后台)就在activiti-rest范例中。URL模式与实现方法之间的映射,位于src\main\java\org\activiti\rest\service\application\RestServicesInit.java中:
      router.attach("/repository/models", ModelCollectionResource.class);
      router.attach("/repository/models/{modelId}", ModelResource.class);
      ......
      
      进一步打开ModelCollectionResource类,可见Restlet方式的注释(Annotation)声明:
      @Get("json")
      public DataResponse getModels() {......}
      
      @Post
      public ModelResponse createModel(ModelRequest request) {......}
      
      Restlet本身我们将不再沿用,但从映射所关联到的各个方法体中我们可以查找到各个功能的幕后实现。
  • Step4. REST API扩展
    对于标准REST API无法满足的需求,就需要自行扩展来实现了。扩展会涉及到将来Activiti引擎升级时的兼容问题,不过实际开发中发现、REST接口层的代码量相对来说是很薄的、即便其所用到的表层API发生了变化修改量也不会很大。

    • URL风格
      扩展API的{servlet-mapping}部分是与标准API有所区别的(比如一个是service另一个就是service-ext);其他URL风格方面则尽量与官方REST API保持一致。
    • Spring MVC方式REST实现
      在Spring MVC方式中,REST URL模式与实现方法之间的映射,直接位于各个实现方法所在的类中,包括位于类的声明、方法的声明之上的注释标签(@RequestMapping(...))。
      以“流程设计模型部署”为例,
      @Controller
      @RequestMapping(value = "/service-ext/repository/models")
      public class ModelController {
          ......
          @RequestMapping(value = "deploy/{modelId}")
          public String deploy(@PathVariable("modelId") String modelId, 
                  @RequestParam(value="starterUser", required=false) String starterUserId, 
                  @RequestParam(value="starterGroup", required=false) String starterGroupId, 
                  RedirectAttributes redirectAttributes)
          {......}
      }
      
      可以看到,deploy()方法所对应的URL模式是/service-ext/repository/models/deploy/{modelId},其中{modelId}部分将被直接解析成方法参数modelId,另外有两个方法参数starterUserIdstarterUserGroup则对应的是URL中的QueryString部分(且是可选的),一个有效的HTTP请求可能是:
      GET http://localhost:8080/app/service-ext/repository/models/deploy/1111?starterUser=kermit
      
      更完整的Spring MVC语法文档可参考这个链接,包括@ResponseBody的使用(方便的将对象返回值转成JSON等格式)、produces="application/json"consumes="application/json"等等。
    • 官方代码借鉴
      (略述)可根据与需求近似的REST API、找到与相关URL模式相映射的实现方法。activiti-engine核心包下的org.activiti.engine.impl.RepositoryServiceImpl等类是关键类,org.activiti.engine.impl.interceptor.CommandExecutor则是引擎采用的“Command设计模式”的执行者,各种工作流操作都会被归结为相应的某种Command、在设置好特定的操作上下文CommandContext后、就由CommandExecutor负责执行。一般我们并不需要修改到Command设计模式的领域,而只需参考activiti-rest包中是如何调用其他包的代码就够。
    • Step5. 测试界面(客户端)
      如上,我们在后台每定制扩展一个REST API(e.g.“流程设计模型部署”),相应都需要在前端(Web)测试页面(如图5)中增加一组测试。
      注意:测试页面其实相当于是“业务系统”的角色,是可以与后台(工作流引擎)分隔为两个WebApp项目的。
      测试界面并未使用jquery-ui、blueprint等前端技术,只是简单的JSP(其实纯HTML/JS都完全可以,因为只需Ajax通信+JSON包装/解析)。给出一段调用“流程设计模型部署”API的HTML链接:

      
           流程设计模型部署
      
      
    • 再给出一段部署后、已进入工作流运行时阶段(runtime)时、模拟用户提交某份表单完成某个用户任务的JS代码:

       

      
      
      • Step6. 贯通生命周期
        根据你所服务的“业务系统”的不同,会对REST服务的接口提出不同的需求,甚至会影响到工作流生命周期的责任人划分:例如,流程设计模型究竟是能从官方Modeler直接兼容地保存到数据库、只需一个id即可部署,还是需要采用其他的Modeler编辑、然后以BAR/ZIP包方式直接部署、因此参数也就变成了文件流;或者说流程实例被启动后,某个用户名下的“我的(用户)任务”、究竟是点击后就“签收+办理”并弹出表单、还是分成两步进行。
        当我们逐一完成如图5的若干项 扩展或标准REST API调用,图2中展示的工作流生命周期也就能被打通了。工作流运行阶段、根据用户输入的不同、会进行不同的流转,可以通过官方范例中已有的流程状态跟踪器“Diagram Viewer”查看进度如下:

        Activiti进行时——企业工作流生命周期贯通_第6张图片

        图6:运行时阶段的工作流状态查看


        上图红线部分显示了,“填写申请”确认提交后经过了“直属上级审批”、但在“人事审批”环节被否决,又重新进入了“填写申请”环节。


      参考:

      https://www.cnblogs.com/zhao1949/p/5976296.html

       

      你可能感兴趣的:(Java,activiti)