1. 简介

1.1. 协议

Activiti使用 Apache V2 协议开源。 Activiti Modeler(Web设计器)使用了另一个开源协议 LGPL 2.1

1.3. 源码

Activiti的发布包里包含了大部分源码,这些源码是以jar压缩文件提供的。Activiti的源码可以通过以下链接获得: https://github.com/Activiti/Activiti

1.4. 必要的软件

1.4.1. JDK 6+

Activiti需要JDK 6或以上版本。访问 Oracle Java SE 下载,点击“下载JDK”按钮。这个页面上有安装指导。安装完成后,执行 java -version 校验安装是否成功。能看到JDK的版本信息就说明安装成功了。

1.4.2. Eclipse Indigo 和 Juno

the Eclipse 下载页面选择Eclipse版本并下载。解压下载的文件,然后执行+eclipse+目录下的eclipse文件。手册后续有专门一章介绍如何安装我们的Eclipse Designer插件

1.5. 反馈问题

每一个自重的开发者都应该先看看 提问的智慧

看完提问的智慧,你可以在 用户论坛 提问和评论,也可以在我们的 JIRA 问题追踪系统创建问题。

虽然Activiti托管在GitHub上,但是不建议使用GitHub的问题追踪系统。如果你想报告问题,不要创建GitHub问题(issue),应该使用我们的JIRA

1.6. 试验性功能

标记[EXPERIMENTAL]的章节介绍的功能还不够稳定。

.impl.包下的类都是内部实现类,不保证稳定。但是,在用户手册中,作为配置参数介绍的类,则是被官方支持的,可以保证稳定。

1.7. 内部实现类

在jar文件中,所有包名中包含.impl.的类(比如,org.activiti.engine.impl.pvm.delegate)都是内部实现类。实现类中的所有类或接口都不保证稳定。

2. 开始

2.1. 一分钟入门

Activiti website下载Activiti Explorer的WAR文件后,按照以下步骤使用默认设置运行demo。你需要已经安装Java runtimeApache Tomcat(事实上,鉴于我们只使用servlet功能,任何web容器都可以运行。但我们主要在Tomcat上进行测试)。

  • 将下载的activiti-explorer.war复制到Tomcat的webapps文件夹下。

  • 运行Tomcat的bin文件夹下的startup.bat或者startup.sh脚本启动Tomcat。

  • Tomcat启动后,打开浏览器访问http://localhost:8080/activiti-explorer。使用kermit/kermit登录。

就是这样!Activiti Explorer应用默认使用H2内存数据库。如果你想使用其他数据库配置,请阅读较长版

2.2. Activiti安装

要安装Activiti, 你需要已经安装Java runtimeApache Tomcat。同时确认 JAVA_HOME 环境变量已经设置正确。该环境变量的设置方法取决于你的操作系统。

要运行Activiti Explorer与REST web应用,将你从Activiti下载的WAR文件,复制到Tomcat安装目录下的 webapps 文件夹中。Explorer 应用默认使用内存数据库,示例流程、用户与组。

下表列出demo用户:

用户账号 密码 安全角色

kermit

kermit

admin

gonzo

gonzo

manager

fozzie

fozzie

user

现在你可以访问如下web应用:

Webapp名称 URL 说明

Activiti Explorer

http://localhost:8080/activiti-explorer

流程引擎的用户操作台。使用这个工具来启动新流程、分配任务、查看与接收任务等。这个工具同时可以管理Activiti引擎。

请注意Activiti Explorer示例配置只是用尽可能简单快捷的方式展现Activiti的能力与功能。这*并不*意味着只有这一种使用Activiti的方式。Activiti只是一个jar,它可以被嵌入到任何Java环境中:swing、Tomcat、JBoss、 WebSphere,等等。你也可以将Activiti作为典型的、独立运行的的BPM服务器。任何在Java里可以做的事情,都可以在Activiti中做!

2.3. Activiti数据库配置

就像在一分钟示例配置中介绍的,Activiti Explorer默认运行在H2内存数据库上。要让Activiti Explorer使用独立运行的H2数据库或其他数据库,需要修改Activiti Explorer web应用下,WEB-INF/classes目录中的db.properties。

另外,请注意Activiti Explorer默认自动生成演示用户、组、流程定义与模型。要禁用这些设置,需要修改WEB-INF/classes目录下的engine.properties文件。要完全禁用示例设置,可以将所有设置项设为false。你也可以单独禁用或启用其中的某些设置。

# demo data properties
create.demo.users=true
create.demo.definitions=true
create.demo.models=true
create.demo.reports=true

2.4. 引入Activiti jar与依赖

我们建议使用Maven(或者Ivy)来引入Activiti的jar与依赖库,因为它简化了我们之间的依赖管理。参考http://www.activiti.org/community.html#maven.repository中的介绍来将必要的jar引入你的项目。

如果不想使用Maven,你也可以自行将jar引入你的项目。从Activiti下载的zip包中的 libs 文件夹,包含所有Activiti的jar(包括源码jar)。依赖并没有通过这种方式发布。Activiti引擎的依赖列表如下(使用mvn dependency:tree生成):

org.activiti:activiti-engine:jar:5.17.0
+- org.activiti:activiti-bpmn-converter:jar:5.17.0:compile
|  \- org.activiti:activiti-bpmn-model:jar:5.17.0:compile
|     +- com.fasterxml.jackson.core:jackson-core:jar:2.2.3:compile
|     \- com.fasterxml.jackson.core:jackson-databind:jar:2.2.3:compile
|        \- com.fasterxml.jackson.core:jackson-annotations:jar:2.2.3:compile
+- org.activiti:activiti-process-validation:jar:5.17.0:compile
+- org.activiti:activiti-image-generator:jar:5.17.0:compile
+- org.apache.commons:commons-email:jar:1.2:compile
|  +- javax.mail:mail:jar:1.4.1:compile
|  \- javax.activation:activation:jar:1.1:compile
+- org.apache.commons:commons-lang3:jar:3.3.2:compile
+- org.mybatis:mybatis:jar:3.2.5:compile
+- org.springframework:spring-beans:jar:4.0.6.RELEASE:compile
|  \- org.springframework:spring-core:jar:4.0.6.RELEASE:compile
+- joda-time:joda-time:jar:2.6:compile
+- org.slf4j:slf4j-api:jar:1.7.6:compile
+- org.slf4j:jcl-over-slf4j:jar:1.7.6:compile

注意:只有使用了邮件任务才必须引入邮件依赖jar。

所有依赖可以在Activiti source code的模块中使用mvn dependency:copy-dependencies下载。

2.5. 下一步

使用Activiti Explorer web应用是一个熟悉Activiti概念与功能的好办法。然而,Activiti的主要目的是用来为你自己的应用添加强大的BPM与工作流功能。下面的章节会帮助你熟悉如何在你的环境中编程使用Activiti:

  • 配置章节会教你如何设置Activiti,如何获得ProcessEngine类的实例,他是所有Activiti引擎功能的中心入口。

  • API章节会带你了解构成Activiti API的服务。这些服务用简便但强大的方式提供了Activiti引擎的功能,可以使用在任何Java环境下。

  • 对深入了解Activiti引擎中流程的编写格式,BPMN 2.0,感兴趣吗?请继续浏览BPMN 2.0章节

3. 配置 Configuration

3.1. 创建ProcessEngine Creating a ProcessEngine

Activiti流程引擎通过名为activiti.cfg.xml的XML文件进行配置。请注意这种方式与使用Spring创建流程引擎一样。

获取ProcessEngine,最简单的方式是使用org.activiti.engine.ProcessEngines类:

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine()

这样会从classpath寻找activiti.cfg.xml,并用这个文件中的配置构造引擎。下面的代码展示了一个配置的例子。后续章节会对配置参数进行详细介绍。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">

    <property name="jdbcUrl" value="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000" />
    <property name="jdbcDriver" value="org.h2.Driver" />
    <property name="jdbcUsername" value="sa" />
    <property name="jdbcPassword" value="" />

    <property name="databaseSchemaUpdate" value="true" />

    <property name="jobExecutorActivate" value="false" />
    <property name="asyncExecutorEnabled" value="true" />
    <property name="asyncExecutorActivate" value="false" />

    <property name="mailServerHost" value="mail.my-corp.com" />
    <property name="mailServerPort" value="5025" />
  </bean>

</beans>

请注意这个配置XML文件实际上是一个Spring配置文件。但这并不意味着Activiti只能用于Spring环境!我们只是简单利用Spring内部的解析与依赖注入功能来构造引擎。

也可以通过编程方式使用配置文件,来构造ProcessEngineConfiguration对象。也可以使用不同的bean id(例如第3行)。

ProcessEngineConfiguration.createProcessEngineConfigurationFromResourceDefault();
ProcessEngineConfiguration.createProcessEngineConfigurationFromResource(String resource);
ProcessEngineConfiguration.createProcessEngineConfigurationFromResource(String resource, String beanName);
ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(InputStream inputStream);
ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(InputStream inputStream, String beanName);

也可以不使用配置文件,基于默认创建配置(参考不同的支持类获得更多信息)。

ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();
ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration();

所有的ProcessEngineConfiguration.createXXX()方法都返回ProcessEngineConfiguration,并可以继续按需调整。调用buildProcessEngine()后,生成一个ProcessEngine

ProcessEngine processEngine = ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration()
  .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_FALSE)
  .setJdbcUrl("jdbc:h2:mem:my-own-db;DB_CLOSE_DELAY=1000")
  .setAsyncExecutorEnabled(true)
  .setAsyncExecutorActivate(false)
  .buildProcessEngine();

3.2. ProcessEngineConfiguration bean

activiti.cfg.xml文件中必须包含一个id为'processEngineConfiguration'的bean。

 <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">

这个bean被用于构建ProcessEngine。有多个类可以用于定义processEngineConfiguration。这些类用于不同的环境,并各自设置一些默认值。最佳实践是选择(最)匹配你环境的类,以便减少配置引擎需要的参数。下面列出目前可以使用的类(后续版本会提供更多):

  • org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration:流程引擎独立运行。Activiti自行处理事务。在默认情况下,数据库检查只在引擎启动时进行(如果Activiti表结构不存在或表结构版本不对,会抛出异常)。

  • org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration:这是一个便于使用单元测试的类。Activiti自行处理事务。默认使用H2内存数据库。数据库会在引擎启动时创建,并在引擎关闭时删除。使用这个类时,很可能不需要更多的配置(除了使用任务执行器或邮件功能等时)。

  • org.activiti.spring.SpringProcessEngineConfiguration:在流程引擎处于Spring环境时使用。查看Spring集成章节获得更多信息。

  • org.activiti.engine.impl.cfg.JtaProcessEngineConfiguration:用于引擎独立运行,并使用JTA事务的情况。

3.3. 数据库配置 Database configuration

有两种方式配置Activiti引擎使用的数据库。第一种方式是定义数据库的JDBC参数:

  • jdbcUrl: 数据库的JDBC URL。

  • jdbcDriver: 特定数据库类型的驱动实现。

  • jdbcUsername: 用于连接数据库的用户名。

  • jdbcPassword: 用于连接数据库的密码。

通过提供的JDBC参数构造的数据源,使用默认的MyBatis连接池设置。可用下列属性调整这个连接池(来自MyBatis文档):

  • jdbcMaxActiveConnections: 连接池能够容纳的最大活动连接数量。默认值为10.

  • jdbcMaxIdleConnections: 连接池能够容纳的最大空闲连接数量。

  • jdbcMaxCheckoutTime: 连接从连接池“取出”后,被强制返回前的最大时间间隔,单位为毫秒。默认值为20000(20秒)。

  • jdbcMaxWaitTime: 这是一个底层设置,在连接池获取连接的时间异常长时,打印日志并尝试重新获取连接(避免连接池配置错误造成的永久沉默失败。默认值为20000(20秒)。

数据库配置示例:

<property name="jdbcUrl" value="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000" />
<property name="jdbcDriver" value="org.h2.Driver" />
<property name="jdbcUsername" value="sa" />
<property name="jdbcPassword" value="" />

也可以使用javax.sql.DataSource的实现(例如来自Apache Commons的DBCP):

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" >
  <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  <property name="url" value="jdbc:mysql://localhost:3306/activiti" />
  <property name="username" value="activiti" />
  <property name="password" value="activiti" />
  <property name="defaultAutoCommit" value="false" />
</bean>

<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">

    <property name="dataSource" ref="dataSource" />
    ...

请注意Activiti发布时不包括用于定义数据源的库。需要自行把库(例如来自DBCP)放在你的classpath中。

无论使用JDBC还是数据源方式配置,下列参数都可以使用:

  • databaseType: 通常不需要专门设置这个参数,因为它可以从数据库连接信息中自动分析得出。只有在自动检测失败时才需要设置。可用值:{h2, mysql, oracle, postgres, mssql, db2}。不使用默认的H2数据库时需要设置这个参数。这个选项会决定创建、删除与查询时使用的脚本。查看“支持的数据库”章节了解我们支持哪些类型的数据库。

  • databaseSchemaUpdate: 用于设置流程引擎启动关闭时使用的数据库表结构控制策略。

    • false (默认): 当引擎启动时,检查数据库表结构的版本是否匹配库文件版本。版本不匹配时抛出异常。

    • true: 构建引擎时,检查并在需要时更新表结构。表结构不存在则会创建。

    • create-drop: 引擎创建时创建表结构,并在引擎关闭时删除表结构。

3.4. JNDI数据源配置 JNDI Datasource Configuration

默认情况下,Activiti的数据库配置保存在每个web应用WEB-INF/classes目录下的db.properties文件中。有时这样并不合适,因为这需要用户修改Activiti源码中的db.properties文件并重新编译war包,或者在部署后解开war包并修改db.properties文件。

通过使用JNDI(Java Naming and Directory Interface,Java命名和目录接口)获取数据库连接,连接完全由Servlet容器管理,配置也可以在war部署之外进行管理。同时也比db.properties提供了更多控制连接的参数。

3.4.1. 使用 Usage

要将Activiti Explorer与Activiti Rest web应用从db.properties配置切换至JNDI数据源配置,请打开Spring主配置文件(activiti-webapp-explorer2/src/main/webapp/WEB-INF/activiti-standalone-context.xml与activiti-webapp-rest2/src/main/resources/activiti-context.xml),并删除名为"dbProperties" 与"dataSource"的bean。然后增加下列bean:

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="java:comp/env/jdbc/activitiDB"/>
</bean>

接下来我们需要新增context.xml文件,其中包含默认的H2配置。也可以用你自己的的JNDI配置覆盖它。对于Activiti Explorer,用下列文件替换activiti-webapp-explorer2/src/main/webapp/META-INF/context.xml:

<Context antiJARLocking="true" path="/activiti-explorer2">
    <Resource auth="Container"
              name="jdbc/activitiDB"
              type="javax.sql.DataSource"
              scope="Shareable"
              description="JDBC DataSource"
              url="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000"
              driverClassName="org.h2.Driver"
              username="sa"
              password=""
              defaultAutoCommit="false"
              initialSize="5"
              maxWait="5000"
              maxActive="120"
              maxIdle="5"/>
</Context>

对于Activiti REST web应用,新增activiti-webapp-rest2/src/main/webapp/META-INF/context.xml文件,包含下列配置:

<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/activiti-rest2">
    <Resource auth="Container"
              name="jdbc/activitiDB"
              type="javax.sql.DataSource"
              scope="Shareable"
              description="JDBC DataSource"
              url="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=-1"
              driverClassName="org.h2.Driver"
              username="sa"
              password=""
              defaultAutoCommit="false"
              initialSize="5"
              maxWait="5000"
              maxActive="120"
              maxIdle="5"/>
</Context>

可选步骤,可以删除Activiti Explorer与Activiti REST web应用中无用的db.properties文件。

3.4.2. 配置 Configuration

根据你使用的servlet容器应用不同,配置JNDI数据源的方式也不同。下面的介绍用于Tomcat,对于其他容器应用,请参考对应的文档。

Tomcat的JNDI资源配置在$CATALINA_BASE/conf/[enginename]/[hostname]/[warname].xml (对于Activiti Explorer通常会是$CATALINA_BASE/conf/Catalina/localhost/activiti-explorer.xml)。当应用第一次部署时,默认会从Activiti war包中复制context.xml。所以如果存在这个文件则需要替换。例如,如果需要将JNDI资源修改为应用连接MySQL而不是H2,按照下列修改文件:

<?xml version="1.0" encoding="UTF-8"?>
    <Context antiJARLocking="true" path="/activiti-explorer2">
        <Resource auth="Container"
            name="jdbc/activitiDB"
            type="javax.sql.DataSource"
            description="JDBC DataSource"
            url="jdbc:mysql://localhost:3306/activiti"
            driverClassName="com.mysql.jdbc.Driver"
            username="sa"
            password=""
            defaultAutoCommit="false"
            initialSize="5"
            maxWait="5000"
            maxActive="120"
            maxIdle="5"/>
        </Context>

3.5. 支持的数据库 Supported databases

下面列出Activiti指定的数据库类型(区分大小写!)。

Activiti数据库类型 示例JDBC URL 备注

h2

jdbc:h2:tcp://localhost/activiti

默认配置的数据库

mysql

jdbc:mysql://localhost:3306/activiti?autoReconnect=true

已使用mysql-connector-java数据库驱动测试

oracle

jdbc:oracle:thin:@localhost:1521:xe

postgres

jdbc:postgresql://localhost:5432/activiti

db2

jdbc:db2://localhost:50000/activiti

mssql

jdbc:sqlserver://localhost:1433;databaseName=activiti (jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver) OR jdbc:jtds:sqlserver://localhost:1433/activiti (jdbc.driver=net.sourceforge.jtds.jdbc.Driver)

已使用Microsoft JDBC Driver 4.0 (sqljdbc4.jar)与JTDS Driver测试

3.6. 创建数据库表 Creating the database tables

在你的数据库中创建标的最简单方法是:

  • 在classpath中增加activiti-engine jar

  • 增加合适的数据库驱动

  • 在classpath中增加Activiti配置文件(activiti.cfg.xml),指向你的数据库(参考数据库配置)

  • 执行DbSchemaCreate类的main方法

然而,通常只有数据库管理员可以在数据库中执行DDL语句,在生产环境中这也是最明智的选择。DDL的SQL脚本可以在Activiti下载页面或Activiti发布目录中找到,位于database子目录。引擎jar (activiti-engine-x.jar)的org/activiti/db/create包中也有一份(drop目录存放删除脚本)。SQL文件的格式为:

activiti.{db}.{create|drop}.{type}.sql

db支持的数据库,而type

  • engine: 引擎执行所需的表,必需。

  • identity: 存储用户、组、用户与组关系的表。这些表是可选的,但在使用引擎自带的默认身份管理时需要使用。

  • history: 存储历史与审计信息的表。当历史级别设置为none时不需要。请注意不使用这些表会导致部分使用历史数据的功能失效(如任务备注)。

MySQL用户请注意:低于5.6.4的MySQL版本不支持timestamps或包含毫秒精度的日期。更糟的是部分版本会在创建类似的列时抛出异常,而另一些版本则不会。当使用自动创建/升级时,引擎在执行时会自动修改DDL语句。当使用DDL文件方式建表时,可以使用通用版本,或使用文件名包含mysql55的特殊版本(用于5.6.4以下的任何版本)。特殊版本的文件中不会使用毫秒精度的列类型。

具体地说,对于MySQL的版本:

  • <5.6: 不支持毫秒精度。可以使用DDL文件(使用包含mysql55的文件)。可以使用自动创建/升级。

  • 5.6.0 - 5.6.3: 不支持毫秒精度。可以使用自动创建/升级。建议升级为较新版本的数据库。如果确实需要,可以使用包含mysql55的DDL文件。

  • 5.6.4+: 支持毫秒精度。可以使用DDL文件(默认的包含mysql的文件)。可以使用自动创建/升级。

请注意在Activiti表已经创建/升级后,更新MySQL数据库,则需要手工修改列类型!

3.7. 数据库表名说明 Database table names explained

Activiti的所有数据库表都以ACT_开头。第二部分是说明表用途的两字符标示符。服务API的命名也大略符合这个规则。

  • ACT_RE_*: 'RE’代表repository。带有这个前缀的表包含“静态”信息,例如流程定义与流程资源(图片、规则等)。

  • ACT_RU_*: 'RU’代表runtime。这些表存储运行时信息,例如流程实例(process instance)、用户任务(user task)、变量(variable)、作业(job)等。Activiti只在流程实例运行中保存运行时数据,并在流程实例结束时删除记录。这样保证运行时表小和快。

  • ACT_ID_*: 'ID’代表identity。这些表包含身份信息,例如用户、组等。

  • ACT_HI_*: 'HI’代表history。这些表存储历史数据,例如已完成的流程实例、变量、任务等。

  • ACT_GE_*: 通用数据。用于不同场景下。

3.8. 数据库升级 Database upgrade

在升级前,请确保你已经(使用数据库的备份功能)备份了数据库。

默认情况下,每次流程引擎创建时会进行版本检查,通常是在你的应用或者Activiti web应用启动的时候。如果Activiti库发现库版本与Activiti数据库表版本不同,会抛出异常。

要进行升级,首先需要将下列配置参数放入你的activiti.cfg.xml配置文件:

<beans >

  <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
    <!-- ... -->
    <property name="databaseSchemaUpdate" value="true" />
    <!-- ... -->
  </bean>

</beans>

同时,在classpath中加上合适的数据库驱动。升级你应用中的Activiti库,或者启动一个新版本的Activiti,并将它指向旧版本的数据库。将databaseSchemaUpdate设置为true。当Activiti发现库与数据库表结构不同步时,会自动将数据库表结构升级至新版本。

你还可以直接运行升级DDL语句,也可以从Activiti下载页面获取升级数据库脚本并运行。

3.9. 作业执行器与异步执行器(从5.17.0版本起) Job Executor and Async Executor (since version 5.17.0)

从5.17.0版本开始,在作业执行器之外,Activiti还提供了异步执行器。Activiti引擎可以通过它,以性能更好,也对数据库更友好的方式执行异步作业。

此外,如果在Java EE 7下运行,容器还可以使用符合JSR-236标准的ManagedJobExecutorManagedAsyncJobExecutor来管理线程。要启用这个功能,需要在配置中如下加入线程工厂:

<bean id="threadFactory" class="org.springframework.jndi.JndiObjectFactoryBean">
   <property name="jndiName" value="java:jboss/ee/concurrency/factory/default" />
</bean>

<bean id="customJobExecutor" class="org.activiti.engine.impl.jobexecutor.ManagedJobExecutor">
   <!-- ... -->
   <property name="threadFactory" ref="threadFactory" />
   <!-- ... -->
</bean>

如果没有设置线程工厂,上述两个managedxx类都会退化为默认实现(非managed版本)。

3.10. 启用作业执行器 Job executor activation

JobExecutor是管理一组线程的组件,这些线程用于触发定时器(包括后续的异步消息)。在单元测试场景下,使用多线程会很笨重。因此API提供ManagementService.createJobQuery用于查询,以及ManagementService.executeJob用于执行作业。这样作业的执行就可以在单元测试内部控制。为了避免作业执行器的干扰,可以将它关闭。

默认情况下,JobExecutor在流程引擎启动时激活。当你不希望JobExecutor随流程引擎启动时,设置:

<property name="jobExecutorActivate" value="false" />

3.11. 启用异步执行器 Async executor activation

AsyncExecutor是管理线程池的组件,这个线程池用于触发定时器与异步任务。

默认情况下,由于历史原因,当使用JobExecutor时,AsyncExecutor不生效。然而我们建议使用新的AsyncExecutor代替JobExecutor,通过定义两个参数实现

<property name="asyncExecutorEnabled" value="true" />
<property name="asyncExecutorActivate" value="true" />

asyncExecutorEnabled参数用于启用异步执行器,代替老的作业执行器。 第二个参数asyncExecutorActivate命令Activiti引擎在启动时启动异步执行器线程池。

3.12. 配置邮件服务器 Mail server configuration

配置邮件服务器是可选的。Activiti支持在业务流程中发送电子邮件。发送电子邮件需要配置有效的SMTP邮件服务器。查看电子邮件任务了解配置选项。

3.13. 配置历史 History configuration

可以选择自定义历史存储的配置。你可以通过调整配置影响历史功能。查看历史配置了解细节。

<property name="history" value="audit" />

3.14. 配置在表达式与脚本中暴露的bean Exposing configuration beans in expressions and scripts

默认情况下,所有通过activiti.cfg.xml或你自己的Spring配置文件声明的bean,都可以在表达式与脚本中使用。如果你希望限制配置文件中bean的可见性,可以使用流程引擎配置的beans参数。ProcessEngineConfiguration中的beans参数是一个map。当你配置这个参数时,只有在这个map中声明的bean可以在表达式与脚本中使用。bean会使用你在map中指定的名字暴露。

3.15. 配置部署缓存 Deployment cache configuration

鉴于流程定义信息不会改变,为了避免每次使用流程定义时都读取数据库,所有的流程定义都会(在解析后)被缓存。默认情况下,这个缓存没有限制。要限制流程定义缓存,加上如下的参数

<property name="processDefinitionCacheLimit" value="10" />

设置这个参数,会将默认的hashmap替换为LRU缓存,以进行限制。当然,参数的“最佳”取值,取决于总的流程定义数量,以及实际使用的流程定义数量。

你也可以注入自己的缓存实现。它必须是一个实现了org.activiti.engine.impl.persistence.deploy.DeploymentCache接口的bean:

<property name="processDefinitionCache">
  <bean class="org.activiti.MyCache" />
</property>

配置规则缓存(rules cache)可以使用类似的名为knowledgeBaseCacheLimitknowledgeBaseCache的参数。只有在流程中使用规则任务(rules task)时才需要设置。

3.16. 日志 Logging

自Activiti 5.12版本起,使用SLF4J作为日志框架,替代了之前使用的java.util.logging。所有日志(activiti, spring, mybatis, …​)通过SLF4J路由,并允许你自行选择日志实现。

默认情况下,Activiti引擎依赖不会提供SFL4J绑定jar。你需要自行将其加入你的项目,以便使用所选的日志框架。如果没有加入实现jar,SLF4J会使用NOP-logger。这时除了一条警告外,任何日志都不会记录。可以从http://www.slf4j.org/codes.html#StaticLoggerBinder获取关于绑定的更多信息。

使用Maven可以添加类似这样(这里使用log4j)的依赖,请注意你还需要加上版本:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
</dependency>

activiti-explorer与activiti-rest web应用配置为使用Log4j绑定。所有的activiti-*模块运行测试时也会使用Log4j。

重要提示:当使用classpath中带有commons-logging的容器时:为了将spring的日志路由至SLF4j,需要使用桥接(参考http://www.slf4j.org/legacy.html#jclOverSLF4J)。如果你的容器提供了commons-logging实现,请按照http://www.slf4j.org/codes.html#release页面的指示来保证稳定性。

使用Maven的示例(省略了版本):

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
</dependency>

3.17. 映射诊断上下文 Mapped Diagnostic Contexts

从5.13版本开始,Activiti支持SLF4J的映射诊断上下文特性。与需要日志记录的信息一起,下列基本信息也会传递给底层日志记录器:

  • processDefinition Id 作为 mdcProcessDefinitionID

  • processInstance Id 作为 mdcProcessInstanceID

  • execution Id 作为 mdcExecutionId

默认情况下这些信息都不会被日志记录,但可以通过配置日志记录器,以使用想要的格式,与其他日志信息一起显示。例如在log4j中进行如下简单的布局定义,就可以让日志记录器显示上述信息:

 log4j.appender.consoleAppender.layout.ConversionPattern=ProcessDefinitionId=%X{mdcProcessDefinitionID} executionId=%X{mdcExecutionId} mdcProcessInstanceID=%X{mdcProcessInstanceID} mdcBusinessKey=%X{mdcBusinessKey} %m%n

在系统任务很关键的情况下这很有用,可以通过例如日志分析器进行日志的严格检查。

3.18. 事件处理器 Event handlers

Activiti 5.15引入了事件机制。它可以让你在引擎中发生多种事件的时候得到通知。查看所有支持的事件类型了解可用的事件。

可以只为特定种类的事件注册监听器,而不是在任何类型的事件发送时都被通知。可以通过配置增加引擎全局的事件监听器,在运行时通过API增加引擎全局的事件监听器,也可以 在BPMN XML文件为个别流程定义增加事件监听器。.

所有被分发的事件都是org.activiti.engine.delegate.event.ActivitiEvent的子类。事件(在可用时)提供type, executionId, processInstanceIdprocessDefinitionId。部分事件含有关于发生事件的上下文信息。关于事件包含的附加信息,请参阅所有支持的事件类型

3.18.1. 事件监听器实现 Event listener implementation

对事件监听器的唯一要求,是要实现org.activiti.engine.delegate.event.ActivitiEventListener接口。下面是一个监听器实现的例子,它将接收的所有事件打印至标准输出,并对作业执行相关的事件特别处理。:

public class MyEventListener implements ActivitiEventListener {

  @Override
  public void onEvent(ActivitiEvent event) {
    switch (event.getType()) {

      case JOB_EXECUTION_SUCCESS:
        System.out.println("A job well done!");
        break;

      case JOB_EXECUTION_FAILURE:
        System.out.println("A job has failed...");
        break;

      default:
        System.out.println("Event received: " + event.getType());
    }
  }

  @Override
  public boolean isFailOnException() {
    // onEvent方法中的逻辑并不重要,日志失败异常可以被忽略……
    return false;
  }
}

isFailOnException()方法决定了当事件分发后,onEvent(..)方法抛出异常时的行为。若返回false,忽略异常;返回true,异常不会被忽略而会被上抛,使当前执行的命令失败。如果事件是API调用(或其他事务操作,例如作业执行)的一部分,事务将被回滚。如果事件监听器中并不是重要的业务操作,建议返回false

Activiti提供了少量基础实现,以简化常用的事件监听器用例。它们可以被用作监听器的示例或基类:

  • org.activiti.engine.delegate.event.BaseEntityEventListener: 事件监听器基类,可用来监听实体(entity)相关事件,特定或所有实体的事件都可以。它隐藏了类型检测,提供了4个需要覆盖的方法:onCreate(..), onUpdate(..)onDelete(..)在实体创建、更新及删除时调用;对所有其他实体相关事件,onEntityEvent(..)会被调用。

3.18.2. 配置与安装 Configuration and setup

在流程引擎中配置的事件监听器会在流程引擎启动时生效,引擎重启后也会保持有效。

eventListeners参数配置为org.activiti.engine.delegate.event.ActivitiEventListener实例的列表(list)。与其他地方一样,你可以声明内联bean定义,也可以用ref指向已有的bean。下面的代码片段在配置中增加了一个事件监听器,无论任何类型的事件分发时,都会得到通知:

<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
    ...
    <property name="eventListeners">
      <list>
         <bean class="org.activiti.engine.example.MyEventListener" />
      </list>
    </property>
</bean>

要在特定类型的事件分发时得到通知,使用typedEventListeners参数,取值为map。map的key为逗号分隔的事件名字列表(或者一个事件的名字),取值为org.activiti.engine.delegate.event.ActivitiEventListener实例的列表。下面的代码片段在配置中增加了一个事件监听器,它会在作业执行成功或失败时得到通知:

<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
    ...
    <property name="typedEventListeners">
      <map>
        <entry key="JOB_EXECUTION_SUCCESS,JOB_EXECUTION_FAILURE" >
          <list>
            <bean class="org.activiti.engine.example.MyJobEventListener" />
          </list>
        </entry>
      </map>
    </property>
</bean>

事件分发的顺序由加入监听器的顺序决定。首先,所有普通(eventListeners参数定义的)事件监听器按照他们在list里的顺序被调用;之后,如果某类型的事件被分发,则该类型(typedEventListeners 参数定义的)监听器被调用。

3.18.3. 在运行时增加监听器 Adding listeners at runtime

可以使用API(RuntimeService)为引擎增加或删除额外的事件监听器:

/**
 * 新增一个监听器,分发器会在所有事件分发时通知。
 * @param listenerToAdd 要新增的监听器
 */
void addEventListener(ActivitiEventListener listenerToAdd);

/**
 * 新增一个监听器,在给定类型的事件发生时被通知。
 * @param listenerToAdd 要新增的监听器
 * @param types 监听器需要监听的事件类型
 */
void addEventListener(ActivitiEventListener listenerToAdd, ActivitiEventType... types);

/**
 * 从分发器中移除给定监听器。该监听器不再被通知,无论该监听器注册为监听何种类型。
 * @param listenerToRemove 要移除的监听器
 */
 void removeEventListener(ActivitiEventListener listenerToRemove);

请注意,运行时新增的监听器在引擎重启后不会保持。

3.18.4. 为流程定义增加监听器 Adding listeners to process definitions

可以为某一流程定义增加监听器。只有与该流程定义相关,或使用该流程定义启动的流程实例相关的事件,才会调用这个监听器。监听器实现可以用完全限定类名(fully qualified classname)定义;也可以定义为表达式,该表达式能被解析为实现监听器接口的bean;也可以配置为抛出消息(message)/信号(signal)/错误(error)的BPMN事件。

执行用户定义逻辑的监听器 Listeners executing user-defined logic

下面的代码片段为流程定义增加了2个监听器。第一个监听器接收任何类型的事件,使用完全限定类名定义。第二个监听器只在作业成功执行或失败时被通知,使用流程引擎配置中beans参数定义的bean作为监听器。

<process id="testEventListeners">
  <extensionElements>
    <activiti:eventListener class="org.activiti.engine.test.MyEventListener" />
    <activiti:eventListener delegateExpression="${testEventListener}" events="JOB_EXECUTION_SUCCESS,JOB_EXECUTION_FAILURE" />
  </extensionElements>

  ...

</process>

实体相关的事件也可以在流程定义中增加监听器,只有在特定实体类型的事件发生时得到通知。下面的代码片段展示了如何设置。可以使用实体的所有(第一个例子)事件,或只使用实体的特定类型(第二个例子)事件。

<process id="testEventListeners">
  <extensionElements>
    <activiti:eventListener class="org.activiti.engine.test.MyEventListener" entityType="task" />
    <activiti:eventListener delegateExpression="${testEventListener}" events="ENTITY_CREATED" entityType="task" />
  </extensionElements>

  ...

</process>

entityType可用的值有:attachment(附件), comment(备注), execution(执行), identity-link(认证关系), job(作业), process-instance(流程实例), process-definition(流程定义), task(任务)。

抛出BPMN事件的监听器 Listeners throwing BPMN events

处理分发的事件的另一个方法,是抛出BPMN事件。请牢记在心,只有特定种类的Activiti事件类型,抛出BPMN事件才合理。例如,在流程实例被删除时抛出BPMN事件,会导致错误。下面的代码片段展示了如何在流程实例中抛出信号,向外部流程(全局)抛出信号,在流程实例中抛出消息事件,以及在流程实例中抛出错误事件。这里不使用classdelegateExpression,而要使用throwEvent属性,以及一个附加属性,用于指定需要抛出的事件类型。

<process id="testEventListeners">
  <extensionElements>
    <activiti:eventListener throwEvent="signal" signalName="My signal" events="TASK_ASSIGNED" />
  </extensionElements>
</process>
<process id="testEventListeners">
  <extensionElements>
    <activiti:eventListener throwEvent="globalSignal" signalName="My signal" events="TASK_ASSIGNED" />
  </extensionElements>
</process>
<process id="testEventListeners">
  <extensionElements>
    <activiti:eventListener throwEvent="message" messageName="My message" events="TASK_ASSIGNED" />
  </extensionElements>
</process>
<process id="testEventListeners">
  <extensionElements>
    <activiti:eventListener throwEvent="error" errorCode="123" events="TASK_ASSIGNED" />
  </extensionElements>
</process>

如果需要使用额外的逻辑判断是否需要抛出BPMN事件,可以扩展Activiti提供的监听器类。通过在你的子类中覆盖isValidEvent(ActivitiEvent event),可以阻止抛出BPMN事件。相关的类为org.activiti.engine.test.api.event.SignalThrowingEventListenerTest, org.activiti.engine.impl.bpmn.helper.MessageThrowingEventListenerorg.activiti.engine.impl.bpmn.helper.ErrorThrowingEventListener.

关于流程定义监听器的说明 Notes on listeners on a process-definition
  • 事件监听器只能作为extensionElements的子元素,声明在process元素上。不能在个别节点(activity)上定义(事件)监听器。

  • delegateExpression中的表达式,与其他表达式(例如在网关中的)不一样,不可以访问执行上下文。只能够引用在流程引擎配置中beans参数定义的bean,或是使用spring(且没有定义beans参数)时,引用任何实现了监听器接口的spring bean。

  • 使用监听器的class属性时,只会创建唯一一个该类的实例。请确保监听器实现不依赖于成员变量,或确保多线程/上下文的使用安全。

  • 如果events属性使用了不合法的事件类型,或者使用了不合法的throwEvent值,会在流程定义部署时抛出异常(导致部署失败)。如果classdelegateExecution指定了不合法的值(不存在的类,不存在的bean引用,或者代理类没有实现监听器接口),在流程启动(或该流程定义的第一个有效事件分发给这个监听器)时,会抛出异常。请确保引用的类在classpath中,并且保证表达式能够解析为有效的实例。

3.18.5. 通过API分发事件 Dispatching events through API

我们通过API提供事件分发机制,可以向任何在引擎中注册的监听器分发自定义事件。建议(但不强制)只分发类型为CUSTOMActivitiEvents。可以使用RuntimeService分发事件:

/**
 * 将给定事件分发给所有注册监听器。
 * @param event 要分发的事件。
 *
 * @throws ActivitiException 当分发事件发生异常,或者{@link ActivitiEventDispatcher}被禁用。
 * @throws ActivitiIllegalArgumentException 当给定事件不可分发
 */
 void dispatchEvent(ActivitiEvent event);

3.18.6. 支持的事件类型 Supported event types

下表列出引擎中的所有事件类型。每种类型对应org.activiti.engine.delegate.event.ActivitiEventType中的一个枚举值。

Table 1. Supported events
Event name Description Event classes

ENGINE_CREATED

本监听器附着的流程引擎已经创建,并可以响应API调用。

org.activiti…​ActivitiEvent

ENGINE_CLOSED

本监听器附着的流程引擎已经关闭,不能再对该引擎的进行API调用。

org.activiti…​ActivitiEvent

ENTITY_CREATED

新的实体已经创建。该实体包含在本事件里。

org.activiti…​ActivitiEntityEvent

ENTITY_INITIALIZED

新的实体已经创建并完全初始化。如果任何子实体作为该实体的一部分被创建,本事件会在子实体创建/初始化后触发,与 ENTITY_CREATE 事件相反。

org.activiti…​ActivitiEntityEvent

ENTITY_UPDATED

实体已经更新。该实体包含在本事件里。

org.activiti…​ActivitiEntityEvent

ENTITY_DELETED

实体已经删除。该实体包含在本事件里。

org.activiti…​ActivitiEntityEvent

ENTITY_SUSPENDED

实体已经挂起。该实体包含在本事件里。会为ProcessDefinitions(流程定义), ProcessInstances(流程实例)与Tasks(任务)分发本事件。

org.activiti…​ActivitiEntityEvent

ENTITY_ACTIVATED

实体已被激活。该实体包含在本事件里。会为ProcessDefinitions, ProcessInstances与Tasks分发本事件。

org.activiti…​ActivitiEntityEvent

JOB_EXECUTION_SUCCESS

作业已经成功执行。该作业包含在本事件里。

org.activiti…​ActivitiEntityEvent

JOB_EXECUTION_FAILURE

作业执行失败。该作业与异常包含在本事件里。

org.activiti…​ActivitiEntityEvent and org.activiti…​ActivitiExceptionEvent

JOB_RETRIES_DECREMENTED

作业重试次数已经由于执行失败而减少。该作业包含在本事件里。

org.activiti…​ActivitiEntityEvent

TIMER_FIRED

定时器已经被触发。

org.activiti…​ActivitiEntityEvent

JOB_CANCELED

作业已经被取消。该作业包含在本事件里。作业会由于API调用取消,任务完成导致关联的边界定时器取消,也会由于新流程定义的部署而取消。

org.activiti…​ActivitiEntityEvent

ACTIVITY_STARTED

节点开始执行

org.activiti…​ActivitiActivityEvent

ACTIVITY_COMPLETED

节点成功完成

org.activiti…​ActivitiActivityEvent

ACTIVITY_CANCELLED

节点将要取消。节点的取消有三个原因(MessageEventSubscriptionEntity, SignalEventSubscriptionEntity, TimerEntity)。

org.activiti…​ActivitiActivityCancelledEvent

ACTIVITY_SIGNALED

节点收到了一个信号

org.activiti…​ActivitiSignalEvent

ACTIVITY_MESSAGE_RECEIVED

节点收到了一个消息。事件在节点接收消息前分发。消息接收后,会为该节点分发 ACTIVITY_SIGNALACTIVITY_STARTED 事件,取决于其类型(边界事件,或子流程启动事件)。

org.activiti…​ActivitiMessageEvent

ACTIVITY_ERROR_RECEIVED

节点收到了错误事件。在节点实际处理错误前分发。该事件的activityId含有处理错误的节点的引用。如果错误被成功传递,后续会为节点发送 ACTIVITY_SIGNALLEDACTIVITY_COMPLETE 消息。

org.activiti…​ActivitiErrorEvent

UNCAUGHT_BPMN_ERROR

抛出了未捕获的BPMN错误。流程没有该错误的处理器。该事件的activityId为空。

org.activiti…​ActivitiErrorEvent

ACTIVITY_COMPENSATE

节点将要被补偿。该事件包含将要执行补偿的节点id。

org.activiti…​ActivitiActivityEvent

VARIABLE_CREATED

创建了流程变量。本事件包含变量名、取值与关联的执行和任务(若有)。

org.activiti…​ActivitiVariableEvent

VARIABLE_UPDATED

更新了已有变量。本事件包含变量名、取值与关联的执行和任务(若有)。

org.activiti…​ActivitiVariableEvent

VARIABLE_DELETED

删除了已有变量。本事件包含变量名、最后取值与关联的执行和任务(若有)。

org.activiti…​ActivitiVariableEvent

TASK_ASSIGNED

任务分派给了用户。该任务包含在本事件里。

org.activiti…​ActivitiEntityEvent

TASK_CREATED

任务已经创建。本事件在 ENTITY_CREATE 事件之后分发。若该任务是流程的一部分,本事件会在任务监听器执行前触发。

org.activiti…​ActivitiEntityEvent

TASK_COMPLETED

任务已经结束。本事件在 ENTITY_DELETE 事件前分发。若该任务是流程的一部分,本事件会在流程前进之前触发,并且会跟随一个 ACTIVITY_COMPLETE 事件,指向代表该任务的节点。

org.activiti…​ActivitiEntityEvent

PROCESS_COMPLETED

流程完成。在最后一个节点的 ACTIVITY_COMPLETED 事件后分发。当流程实例没有任何路径可以继续时,流程结束。

org.activiti…​ActivitiEntityEvent

PROCESS_CANCELLED

流程已经被取消。在流程实例从运行时删除前分发。流程实例使用API调用RuntimeService.deleteProcessInstance取消。

org.activiti…​ActivitiCancelledEvent

MEMBERSHIP_CREATED

用户加入了一个组。本事件包含了相关的用户和组的id。

org.activiti…​ActivitiMembershipEvent

MEMBERSHIP_DELETED

用户从一个组中移出。本事件包含了相关的用户和组的id。

org.activiti…​ActivitiMembershipEvent

MEMBERSHIPS_DELETED

组的所有用户将被移出。本事件在用户移出前抛出,因此关联关系仍然可以访问。因为性能原因,不会再为每个被移出的用户抛出 MEMBERSHIP_DELETED 事件。

org.activiti…​ActivitiMembershipEvent

引擎中所有的 ENTITY_\* 事件都与实体关联。下表列出每个实体分发的实体事件:

  • ENTITY_CREATED, ENTITY_INITIALIZED, ENTITY_DELETED: Attachment(附件), Comment(备注), Deployment(部署), Execution(执行), Group(组), IdentityLink(身份关联), Job(作业), Model(模型), ProcessDefinition(流程定义), ProcessInstance(流程实例), Task(任务), User(用户).

  • ENTITY_UPDATED: Attachment, Deployment, Execution, Group, IdentityLink, Job, Model, ProcessDefinition, ProcessInstance, Task, User.

  • ENTITY_SUSPENDED, ENTITY_ACTIVATED: ProcessDefinition, ProcessInstance/Execution, Task.

3.18.7. 附加信息 Additional remarks

监听器只会被通知所在引擎分发的事件。因此如果你使用不同的引擎,在同一个数据库上运行,只有该监听器注册的引擎生成的事件,会分发给该监听器。其他引擎生成的事件不会分发给这个监听器,不论这些引擎是否运行在同一个JVM下。

某些事件类型(与实体相关)暴露了目标实体。按照事件类型的不同,有时实体不能被更新(例如实体已经被删除)。如果可能的话,请使用事件暴露的EngineServices安全操作引擎。即使这样,更新、操作事件中暴露的实体仍然需要小心。

历史不会分发实体事件,因为它们都有对应的运行时实体分发事件。

4. The Activiti API

4.1. 流程引擎API与服务 The Process Engine API and services

引擎API是与Activiti交互的最常用手段。中心入口是ProcessEngine,像配置章节中介绍的一样,可以使用多种方式创建。使用ProcessEngine,可以获得包含工作流/BPM方法的多种服务。ProcessEngine与服务对象都是线程安全的,因此可以在整个服务器中保存一份引用。

api.services
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();

RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();
ManagementService managementService = processEngine.getManagementService();
IdentityService identityService = processEngine.getIdentityService();
HistoryService historyService = processEngine.getHistoryService();
FormService formService = processEngine.getFormService();

ProcessEngines.getDefaultProcessEngine()在第一次被调用时将初始化并构建流程引擎,在之后的调用都会返回相同的流程引擎。流程引擎的创建通过ProcessEngines.init()实现,关闭由ProcessEngines.destroy()实现。

ProcessEngines会扫描所有activiti.cfg.xmlactiviti-context.xml文件。对于所有的activiti.cfg.xml文件,流程引擎会以标准Activiti方式构建:ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(inputStream).buildProcessEngine()。对于所有的activiti-context.xml文件,流程引擎会以Spring的方式构建:首先构建Spring应用上下文,然后从该上下文中获取流程引擎。

所有的服务都是无状态的。这意味着你可以很容易的在集群环境的多个节点上运行Activiti,使用同一个数据库,而不用担心上一次调用实际在哪台机器上执行。不论在哪里执行,对任何服务的任何调用都是幂等(idempotent)的。

RepositoryService很可能是使用Activiti引擎要用的第一个服务。这个服务提供了管理与控制deployments(部署)与process definitions(流程定义)的操作。在这里简单说明一下,流程定义是BPMN 2.0流程的Java等价副本,展现流程中每一步的结构与行为。deployment是Activiti引擎中的包装单元,一个部署中可以包含多个BPMN 2.0 xml文件,以及其他资源。开发者可以决定在一个部署中包含的内容,可以是单各流程的BPMN 2.0 xml文件,也可以包含多个流程及其相关资源(如’hr-processes’部署可以包含所有与人力资源流程相关的的东西)。RepositoryService可用于deploy(部署)这样的包。部署意味着将它上传至引擎,引擎将在储存至数据库之前检查与分析所有的流程。从这里开始,系统知道了这个部署,部署中包含的所有流程都可以启动。

此外,这个服务还可以:

  • 查询引擎已知的部署与流程定义。

  • 暂停或激活部署中的某些流程,或整个部署。暂停意味着不能再对它进行操作,激活是其反操作。

  • 读取各种资源,比如部署中保存的文件,或者引擎自动生成的流程图。

  • 读取POJO版本的流程定义。使用它可以用Java而不是xml的方式检查流程。

RepositoryService提供的是静态信息(也就是不会改变,至少不会经常改变的信息),而RuntimeService就完全相反。它可以启动流程定义的新流程实例。前面介绍过,process definition(流程定义)定义了流程中不同步骤的结构与行为。流程实例则是流程定义的实际执行。同一时刻,一个流程定义通常有多个运行中的实例。RuntimeService也用于读取与存储process variables(流程变量)。流程变量是给定流程持有的数据,可以在流程的许多构造中使用(例如排他网关exclusive gateway 经常使用流程变量决定流程下一步要选择的路径)。RuntimeService还可以用于查询流程实例与执行(execution)。执行代表了BPMN 2.0中的 'token' 概念。通常执行是指向流程实例当前位置的指针。最后,RuntimeService还可以在流程实例等待外部触发时使用,以便流程可以继续运行。流程有许多wait states(暂停状态),RuntimeService服务提供了许多操作用于“通知”流程实例,告知已经接收到外部触发,使流程实例可以继续运行。

对于像Activiti这样的BPM引擎来说,核心是需要人类用户实际操作的任务。所有任务相关的东西都组织在TaskService中,例如

  • 查询分派给用户或组的任务

  • 创建standalone(独立运行)任务。这是一种没有关联到流程实例的任务。

  • 决定任务的执行用户(assignee),或者将用户通过某种方式与任务关联。

  • 认领(claim)与完成(complete)任务。认领是指某人决定成为任务的执行用户,也即他将会完成这个任务。完成任务是指“做这个任务要求的工作”,通常是填写某种表单。

IdentityService很简单。它用于管理(创建,更新,删除,查询……)组与用户。请重点注意,Activiti实际上在运行时并不做任何用户检查。例如任务可以分派给任何用户,而引擎并不会验证系统中是否存在该用户。这是因为Activiti有时要与LDAP、Active Directory等服务结合使用。

FormService是可选服务。也就是说Activiti没有它也能很好地运行,而不必牺牲任何功能。这个服务引入了start form(开始表单)task form(任务表单)的概念。 开始表单是在流程实例启动前显示的表单,而任务表单是用户完成任务时显示的表单。Activiti可以在BPMN 2.0流程定义中定义这些表单。表单服务通过简单的方式暴露这些数据。再次重申,表单不一定要嵌入流程定义,因此这个服务是可选的。

HistoryService暴露所有Activiti引擎收集的历史数据。当执行流程时,引擎会保存许多数据(可以配置),例如流程实例启动时间,谁在执行哪个任务,完成任务花费的事件,每个流程实例的执行路径,等等。这个服务主要提供查询这些数据的能力。

ManagementService通常在用Activiti编写用户应用时不需要使用。它可以用于读取数据库表与表原始数据的信息,也提供了对作业(job)的查询与管理操作。Activiti中很多地方都使用作业,例如定时器(timer),异步操作(asynchronous continuation),延时暂停/激活(delayed suspension/activation)等等。后续会详细介绍这些内容。

参考javadocs了解服务操作与引擎API的更多信息。

4.2. 异常策略 Exception strategy

Activiti的异常基类是org.activiti.engine.ActivitiException,是未检查异常(unchecked exception)。在任何API操作时都可能会抛出这个异常,javadoc记录了每个方法可能发生的异常。例如,从TaskService中摘录:

/**
 * 当任务成功执行时调用。
 * @param taskId 需要完成的任务id,不能为null。
 * @throws ActivitiObjectNotFoundException 若给定id找不到任务。
 */
 void complete(String taskId);

在上例中,如果传递的id找不到任务,会抛出异常。并且,由于javadoc中明确要求taskId不能为null,因此如果传递了null值,会抛出ActivitiIllegalArgumentException异常

尽管我们想避免过大的异常层次结构,我们还是添加了下述在特定情况下抛出的异常子类。所有流程执行与API调用中发生的错误,如果不符合下面列出的异常,会统一抛出ActivitiExceptions

  • ActivitiWrongDbException: 当Activiti引擎检测到数据库表结构版本与引擎版本不匹配时抛出。

  • ActivitiOptimisticLockingException: 当对同一数据实体的并发访问,导致数据存储发生乐观锁时抛出。

  • ActivitiClassLoadingException: 当需要载入的类(如JavaDelegates, TaskListeners, …​)无法找到,或载入时发生错误时抛出。

  • ActivitiObjectNotFoundException: 当请求或要操作的对象不存在时抛出。

  • ActivitiIllegalArgumentException: 这个异常说明调用Activiti API时使用了不合法的参数。可能是引擎配置中的不合法值,或者是API调用传递的不合法参数,也可能是流程定义中的不合法值。

  • ActivitiTaskAlreadyClaimedException: 当调用taskService.claim(…​),而该任务已经被认领时抛出。

4.3. 使用Activiti services(Working with the Activiti services)

如前所述,与Activiti引擎交互的方式,是使用org.activiti.engine.ProcessEngine类实例暴露的服务。下面的示例假定你已经有可运行的Activiti环境,也就是说,你可以访问有效的org.activiti.engine.ProcessEngine。如果你只是简单地想尝试下面的代码,可以下载或克隆Activiti单元测试模板,导入你的IDE,在org.activiti.MyUnitTest单元测试中增加一个testUserguideCode()方法。

这段小教程的最终目标是生成一个业务流程,模拟公司中简单的请假流程:

api.vacationRequest

4.3.1. 部署流程 Deploying the process

所有有关“静态”数据(例如流程定义)的东西,都可以通过RepositoryService访问。从概念上说,所有这种静态数据,都是Activiti引擎“仓库(repository)”中的内容。

src/test/resources/org/activiti/test资源目录(如果没有使用单元测试模板,也可以是其他任何地方)下创建名为VacationRequest.bpmn20.xml的xml文件,写入下列内容。请注意这个章节不会解释例子中用到的xml的结构。如果需要,请先阅读BPMN 2.0章节 the BPMN 2.0 chapter了解这种结构。

<?xml version="1.0" encoding="UTF-8" ?>
<definitions id="definitions"
             targetNamespace="http://activiti.org/bpmn20"
             xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:activiti="http://activiti.org/bpmn">

  <process id="vacationRequest" name="Vacation request">

    <startEvent id="request" activiti:initiator="employeeName">
      <extensionElements>
        <activiti:formProperty id="numberOfDays" name="Number of days" type="long" value="1" required="true"/>
        <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" datePattern="dd-MM-yyyy hh:mm" type="date" required="true" />
        <activiti:formProperty id="vacationMotivation" name="Motivation" type="string" />
      </extensionElements>
    </startEvent>
    <sequenceFlow id="flow1" sourceRef="request" targetRef="handleRequest" />

    <userTask id="handleRequest" name="Handle vacation request" >
      <documentation>
        ${employeeName} would like to take ${numberOfDays} day(s) of vacation (Motivation: ${vacationMotivation}).
      </documentation>
      <extensionElements>
         <activiti:formProperty id="vacationApproved" name="Do you approve this vacation" type="enum" required="true">
          <activiti:value id="true" name="Approve" />
          <activiti:value id="false" name="Reject" />
        </activiti:formProperty>
        <activiti:formProperty id="managerMotivation" name="Motivation" type="string" />
      </extensionElements>
      <potentialOwner>
        <resourceAssignmentExpression>
          <formalExpression>management</formalExpression>
        </resourceAssignmentExpression>
      </potentialOwner>
    </userTask>
    <sequenceFlow id="flow2" sourceRef="handleRequest" targetRef="requestApprovedDecision" />

    <exclusiveGateway id="requestApprovedDecision" name="Request approved?" />
    <sequenceFlow id="flow3" sourceRef="requestApprovedDecision" targetRef="sendApprovalMail">
      <conditionExpression xsi:type="tFormalExpression">${vacationApproved == 'true'}</conditionExpression>
    </sequenceFlow>

    <task id="sendApprovalMail" name="Send confirmation e-mail" />
    <sequenceFlow id="flow4" sourceRef="sendApprovalMail" targetRef="theEnd1" />
    <endEvent id="theEnd1" />

    <sequenceFlow id="flow5" sourceRef="requestApprovedDecision" targetRef="adjustVacationRequestTask">
      <conditionExpression xsi:type="tFormalExpression">${vacationApproved == 'false'}</conditionExpression>
    </sequenceFlow>

    <userTask id="adjustVacationRequestTask" name="Adjust vacation request">
      <documentation>
        Your manager has disapproved your vacation request for ${numberOfDays} days.
        Reason: ${managerMotivation}
      </documentation>
      <extensionElements>
        <activiti:formProperty id="numberOfDays" name="Number of days" value="${numberOfDays}" type="long" required="true"/>
        <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" value="${startDate}" datePattern="dd-MM-yyyy hh:mm" type="date" required="true" />
        <activiti:formProperty id="vacationMotivation" name="Motivation" value="${vacationMotivation}" type="string" />
        <activiti:formProperty id="resendRequest" name="Resend vacation request to manager?" type="enum" required="true">
          <activiti:value id="true" name="Yes" />
          <activiti:value id="false" name="No" />
        </activiti:formProperty>
      </extensionElements>
      <humanPerformer>
        <resourceAssignmentExpression>
          <formalExpression>${employeeName}</formalExpression>
        </resourceAssignmentExpression>
      </humanPerformer>
    </userTask>
    <sequenceFlow id="flow6" sourceRef="adjustVacationRequestTask" targetRef="resendRequestDecision" />

    <exclusiveGateway id="resendRequestDecision" name="Resend request?" />
    <sequenceFlow id="flow7" sourceRef="resendRequestDecision" targetRef="handleRequest">
      <conditionExpression xsi:type="tFormalExpression">${resendRequest == 'true'}</conditionExpression>
    </sequenceFlow>

     <sequenceFlow id="flow8" sourceRef="resendRequestDecision" targetRef="theEnd2">
      <conditionExpression xsi:type="tFormalExpression">${resendRequest == 'false'}</conditionExpression>
    </sequenceFlow>
    <endEvent id="theEnd2" />

  </process>

</definitions>

你必须首先部署(deploy)流程,以使Activiti引擎可以识别它。部署意味着引擎会将BPMN 2.0 xml文件解析为可执行的东西,并为部署中包含的每个流程定义创建新的数据库记录。这样,引擎重启后,仍能获取已部署的流程:

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = processEngine.getRepositoryService();
repositoryService.createDeployment()
  .addClasspathResource("org/activiti/test/VacationRequest.bpmn20.xml")
  .deploy();

Log.info("Number of process definitions: " + repositoryService.createProcessDefinitionQuery().count());

部署章节阅读更多部署相关信息。

4.3.2. 启动流程实例 Starting a process instance

向Activiti引擎部署流程定义后,可以用它启动流程实例。每个流程定义都可以有多个流程实例。流程定义就像是“蓝图”,而流程实例在运行时执行它。

所有与流程运行时状态相关的东西都可以在RuntimeService中找到。启动流程实例有多种不同的方法。在下列代码片段中,使用流程定义xml中定义的key启动流程实例。在启动流程实例时,我们也设置了一些流程变量(process variables),因为第一个用户任务(user task)的描述(description)中的表达式(expression)需要用到它们。流程变量的使用很普遍,因为它们为流程定义的流程实例赋予了意义。流程变量使每个流程实例与其他实例不同。

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("employeeName", "Kermit");
variables.put("numberOfDays", new Integer(4));
variables.put("vacationMotivation", "I'm really tired!");

RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("vacationRequest", variables);

// Verify that we started a new process instance
Log.info("Number of process instances: " + runtimeService.createProcessInstanceQuery().count());

4.3.3. 完成任务 Completing tasks

流程启动时,第一步是一个用户任务。这个步骤必须由系统用户操作。一般会提供“待办任务”列出所有需要该用户处理的任务。下面的代码片段展示如何进行这种列表的查询:

// 获取management组的所有任务
TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
for (Task task : tasks) {
  Log.info("Task available: " + task.getName());
}

我们需要结束这个任务才能使流程实例继续运行。对于Activiti引擎来说,就是complete(完成)这个任务。下面的代码片段展示了如何操作:

Task task = tasks.get(0);

Map<String, Object> taskVariables = new HashMap<String, Object>();
taskVariables.put("vacationApproved", "false");
taskVariables.put("managerMotivation", "We have a tight deadline!");
taskService.complete(task.getId(), taskVariables);

现在流程实例会继续向下一步进行。在这个例子里,下一步允许雇员填写一个表单,用来修改提交的请假申请。雇员可以重新提交请假申请,这会使流程从开始任务重新开始运行。

4.3.4. 暂停与激活流程 Suspending and activating a process

可以暂停流程定义。当流程定义暂停后,不能再创建新的流程实例(会抛出异常)。使用RepositoryService暂停流程定义:

repositoryService.suspendProcessDefinitionByKey("vacationRequest");
try {
  runtimeService.startProcessInstanceByKey("vacationRequest");
} catch (ActivitiException e) {
  e.printStackTrace();
}

要重新激活流程定义,可以调用repositoryService.activateProcessDefinitionXXX方法。

也可以暂停流程实例。当流程实例暂停后,不能进行流程操作(例如完成任务会抛出异常),作业(如定时器)也不会执行。可以调用runtimeService.suspendProcessInstance暂停流程实例。调用runtimeService.activateProcessInstanceXXX重新激活流程实例。

4.3.5. 扩展阅读 Further reading

在前面的章节,我们大致介绍了Acvtiviti的功能。我们会在未来扩展这些内容,覆盖更多的Activiti API。当然,与其他开源项目一样,最好的学习方法是研究代码与阅读Javadocs!

4.4. 查询API (Query API)

从引擎中查询数据有两种方式:查询API与原生(native)查询。查询API可以使用链式API,通过编程方式进行类型安全的查询。你可以在查询中增加各种条件(所有条件都用做AND逻辑),也可以明确指定排序。下面是示例代码:

List<Task> tasks = taskService.createTaskQuery()
    .taskAssignee("kermit")
    .processVariableValueEquals("orderId", "0815")
    .orderByDueDate().asc()
    .list();

有时你需要更强力的查询,例如使用OR操作符查询,或者使用查询API不能满足查询条件要求。我们为这种需求提供了原生查询,可以自己写SQL查询。返回类型由使用的查询对象决定,数据也会映射到正确的对象中,如Task, ProcessInstance, Execution…​.查询会在数据库中进行,因此你需要使用数据库中定义的表名与列名。这需要了解内部数据结构,因此建议小心使用原生查询。数据库表名可以通过API读取,这样可以将依赖关系减到最小。

List<Task> tasks = taskService.createNativeTaskQuery()
  .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T WHERE T.NAME_ = #{taskName}")
  .parameter("taskName", "gonzoTask")
  .list();

long count = taskService.createNativeTaskQuery()
  .sql("SELECT count(*) FROM " + managementService.getTableName(Task.class) + " T1, "
    + managementService.getTableName(VariableInstanceEntity.class) + " V1 WHERE V1.TASK_ID_ = T1.ID_")
  .count();

4.5. 变量 Variables

流程实例按步骤执行时,需要同时也使用一些数据。在Activiti中,这些数据称作variables(变量),并会存储在数据库中。变量可以用在表达式中(例如在排他网关中用于选择正确的出口路径),用在java服务任务(java service task)中用于调用外部服务(例如为服务调用提供输入或结果存储),等等。

流程实例可以拥有变量(称作process variables,流程变量),执行(executions)——流程当前活动节点的指针,以及用户任务也可以拥有变量。流程实例可以持有任意数量的变量,每个变量都存储在ACT_RU_VARIABLE数据库表的一行中。

任何startProcessInstanceXXX方法都有一个可选参数,用于在流程实例创建并启动时设置变量。例如,在RuntimeService中:

ProcessInstance startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables);

也可以在流程执行中加入变量。例如(RuntimeService):

void setVariable(String executionId, String variableName, Object value);
void setVariableLocal(String executionId, String variableName, Object value);
void setVariables(String executionId, Map<String, ? extends Object> variables);
void setVariablesLocal(String executionId, Map<String, ? extends Object> variables);

请注意可以为给定执行(请记住流程实例由一颗执行的树tree of executions组成)设置local(局部)变量。局部变量将只在该执行中可见,而对执行树的上层则不可见。这可以用于 数据不应该在流程实例级别传播,或者变量在流程实例的不同路径中有不同的值(例如使用并行路径时)的情况。

像下面展示的,可以读取变量。请注意TaskService中有类似的方法。这意味着任务与执行一样,可以持有局部变量,其生存期为任务持续的时间。

Map<String, Object> getVariables(String executionId);
Map<String, Object> getVariablesLocal(String executionId);
Map<String, Object> getVariables(String executionId, Collection<String> variableNames);
Map<String, Object> getVariablesLocal(String executionId, Collection<String> variableNames);
Object getVariable(String executionId, String variableName);
<T> T getVariable(String executionId, String variableName, Class<T> variableClass);

变量通常用于Java代理(Java delegates), 表达式(expressions), 执行(execution),任务监听器(tasklisteners),脚本(scripts)等等。在这些结构中,提供了当前的executiontask对象,可用于变量的设置、读取。简单示例如下:

execution.getVariables();
execution.getVariables(Collection<String> variableNames);
execution.getVariable(String variableName);

execution.setVariables(Map<String, object> variables);
execution.setVariable(String variableName, Object value);

请注意也可以使用上例中方法的local(局部变量)版本。

由于历史(与向后兼容的)原因,当调用上述任何方法时,引擎实际上会从数据库中取出所有变量。也就是说,如果你有10个变量,使用getVariable("myVariable")获取其中的一个,实际上其他9个变量也会从数据库取出并缓存。这并不坏,因为后续的调用可以不必再读取数据库。比如,你的流程定义包含三个连续的服务任务service task(因此它们在同一个数据库事务里),在第一个服务任务里通过一次调用获取全部变量,也许比在每个服务任务里分别获取需要的变量要好。请注意对读取与设置变量都是这样

当然,如果使用大量变量,或者你希望精细控制数据库查询与流量,上述做法并不合适。从Activiti 5.17版本起,引入了可以更精细控制的方法。这个方法有一个可选的参数,告诉引擎是否需要在幕后将所有变量读取并缓存:

Map<String, Object> getVariables(Collection<String> variableNames, boolean fetchAllVariables);
Object getVariable(String variableName, boolean fetchAllVariables);
void setVariable(String variableName, Object value, boolean fetchAllVariables);

fetchAllVariables参数为true时,行为与上面描述的完全一样:读取或设置一个变量时,所有的变量都将被读取并缓存。

而在参数值为false时,会使用明确的查询,其他变量不会被读取或缓存。只有指定的变量的值会被缓存,用于后续使用。

4.6. 表达式 Expressions

Activiti使用UEL进行表达式解析。UEL代表Unified Expression Language,是EE6规范的一部分(查看EE6规范了解更多信息)。为了在所有环境上支持UEL标准的所有最新特性,我们使用JUEL的修改版本。

表达式可以用于例如Java服务任务 Java Service tasks, 执行监听器 Execution Listeners, 任务监听器 Task Listeners条件流 Conditional sequence flows。尽管有值表达式与方法表达式两种表达式,通过Activiti的抽象,使它们都可以在需要expression(表达式)的地方使用。

  • 值表达式 Value expression: 解析为一个值。默认情况下,所有流程变量都可以使用。(若使用Spring)所有的Spring bean也可以用在表达式里。例如:

${myVar}
${myBean.myProperty}
  • 方法表达式 Method expression: 注入一个方法,可以带或不带参数。当注入不带参数的方法时,要确保在方法名后添加空括号(以避免与值表达式混淆)。传递的参数可以是字面值(literal value),也可以是表达式,它们会被自动解析。例如:

${printer.print()}
${myBean.addNewOrder('orderName')}
${myBean.doSomething(myVar, execution)}

请注意,表达式支持解析(包括比较)原始类型(primitive)、bean、list、array(数组)与map。

除了所有流程变量外,还有一些默认对象可在表达式中使用:

  • execution: 持有进行中执行(execution)额外信息的DelegateExecution

  • task: 持有当前任务(task)额外信息的DelegateTask请注意:只在任务监听器的表达式中可用。

  • authenticatedUserId: 当前已验证的用户id。如果没有已验证的用户,该变量不可用。

4.7. 单元测试 Unit testing

业务流程是软件项目的必要组成部分,也需要使用测试一般应用逻辑的方法,也就是单元测试,对它们进行测试。Activiti是嵌入式的Java引擎,因此为业务流程编写单元测试就与编写一般的单元测试一样简单。

Activiti支持JUnit版本3与4的单元测试风格。按照JUnit 3的风格,必须扩展(extended)org.activiti.engine.test.ActivitiTestCase。它通过保护(protected)成员变量提供对ProcessEngine与服务的访问。在测试的setup()中,processEngine会默认使用classpath中的activiti.cfg.xml资源初始化。如果要指定不同的配置文件,请覆盖getConfigurationResource()方法。当使用相同的配置资源时,流程引擎会静态缓存,用于多个单元测试。

通过扩展ActivitiTestCase,你可以使用org.activiti.engine.test.Deployment注解测试方法。在测试运行前,会部署与测试类在同一个包下的格式为testClassName.testMethod.bpmn20.xml的资源文件。在测试结束时,会删除这个部署,包括所有相关的流程实例,任务,等等。也可以使用Deployment注解显式指定,资源位置。查看该类以获得更多信息。

综上所述,JUnit 3风格的测试看起来类似:

public class MyBusinessProcessTest extends ActivitiTestCase {

  @Deployment
  public void testSimpleProcess() {
    runtimeService.startProcessInstanceByKey("simpleProcess");

    Task task = taskService.createTaskQuery().singleResult();
    assertEquals("My Task", task.getName());

    taskService.complete(task.getId());
    assertEquals(0, runtimeService.createProcessInstanceQuery().count());
  }
}

要使用JUnit 4的风格书写单元测试并达成同样的功能,必须使用org.activiti.engine.test.ActivitiRule Rule。这样能够通过它的getter获得流程引擎与服务。对于ActivitiTestCase(上例),包含Rule就可以使用org.activiti.engine.test.Deployment注解(参见上例解释其用途及配置),并且会自动在classpath中寻找默认配置文件。当使用相同的配置资源时,流程引擎会静态缓存,用于多个单元测试。

下面的代码片段展示了JUnit 4风格的测试与ActivitiRule的用法。

public class MyBusinessProcessTest {

  @Rule
  public ActivitiRule activitiRule = new ActivitiRule();

  @Test
  @Deployment
  public void ruleUsageExample() {
    RuntimeService runtimeService = activitiRule.getRuntimeService();
    runtimeService.startProcessInstanceByKey("ruleUsage");

    TaskService taskService = activitiRule.getTaskService();
    Task task = taskService.createTaskQuery().singleResult();
    assertEquals("My Task", task.getName());

    taskService.complete(task.getId());
    assertEquals(0, runtimeService.createProcessInstanceQuery().count());
  }
}

4.8. Debug单元测试(Debugging unit tests)

当使用H2内存数据库进行单元测试时,下面的介绍可以让你在debug过程中容易地检查Activiti数据库中的数据。截图来自Eclipse,但原理应该与其他IDE相似。

假设我们的单元测试的某处放置了breakpoint(断点)。在Eclipse里可以通过在代码左侧条上双击实现:

api.test.debug.breakpoint

如果我们在debug模式(在测试类中右键,选择“Run as”,然后选择“JUnit test”)下运行单元测试,测试进程会在断点处暂停,这样我们就可以在右上窗口中查看测试中的变量。

api.test.debug.view

要检查Activiti的数据,打开Display窗口(如果没有找到这个窗口,打开 Window→Show View→Other,然后选择Display),并键入(可以使用代码补全)org.h2.tools.Server.createWebServer("-web").start()

api.test.debug.start.h2.server

选中刚键入的行并右键点击。然后选择’Display'(或者用快捷方式执行)

api.test.debug.start.h2.server.2

现在打开浏览器并访问http://localhost:8082,填入内存数据库的JDBC URL(默认为jdbc:h2:mem:activiti),然后点击connect按钮。

api.test.debug.h2.login

现在你可以看到Activiti的数据,可以用来理解你的单元测试执行流程的方式是什么,以及为什么这样。

api.test.debug.h2.tables

4.9. Web应用中的流程引擎 The process engine in a web application

ProcessEngine是线程安全的类,可以很容易地在多个线程间共享。在web应用中,这意味着可以在容器启动时创建引擎,并在容器关闭时关闭引擎。

下面的代码片段展示了如何在纯Servlet环境中,简单的通过ServletContextListener初始化与销毁流程引擎。

public class ProcessEnginesServletContextListener implements ServletContextListener {

  public void contextInitialized(ServletContextEvent servletContextEvent) {
    ProcessEngines.init();
  }

  public void contextDestroyed(ServletContextEvent servletContextEvent) {
    ProcessEngines.destroy();
  }

}

contextInitialized方法委托给ProcessEngines.init()。它会在classpath中查找activiti.cfg.xml资源文件,并为每个配置分别创建ProcessEngine(例如多个jar都包含配置文件)。如果在classpath中有多个这样的资源文件,请确保它们都使用不同的名字。需要使用流程引擎时,可以获取通过

ProcessEngines.getDefaultProcessEngine()

或者

ProcessEngines.getProcessEngine("myName");

当然,就像配置章节 configuration section中介绍的,还可以使用各种不同的方式创建流程引擎。

context-listener的contextDestroyed方法委托给ProcessEngines.destroy()。它会妥善关闭所有已初始化的流程引擎。

5. 集成Spring (Spring integration)

尽管完全可以脱离Spring使用Activiti,我们仍提供了很多非常好的集成特性,将在这一章节介绍。

5.1. ProcessEngineFactoryBean

ProcessEngine可以被配置为普通的Spring bean。入口是org.activiti.spring.ProcessEngineFactoryBean类。这个bean处理流程引擎配置,并创建流程引擎。这意味着在Spring中,创建与设置参数与配置章节 configuration section中介绍的一样。集成Spring的配置与引擎bean为:

<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
    ...
</bean>

<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
  <property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>

请注意processEngineConfiguration bean现在使用org.activiti.spring.SpringProcessEngineConfiguration类。

5.2. 事务 Transactions

我们会一步一步地解释(Activiti)发行版里,Spring示例中的SpringTransactionIntegrationTest。下面是我们示例中使用的Spring配置文件(SpringTransactionIntegrationTest-context.xml)。下面的小节包含了dataSource(数据源),transactionManager(事务管理器),processEngine(流程引擎)与Activiti引擎服务。

将DataSource传递给SpringProcessEngineConfiguration(使用“dataSource”参数)时,Activiti会在内部使用org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy对得到的数据源进行包装(wrap)。这是为了保证从数据源获取的SQL连接与Spring的事务可以协同工作。也就是说不需要在Spring配置中对数据源进行代理(proxy)。尽管仍然可以将TransactionAwareDataSourceProxy传递给SpringProcessEngineConfiguration——在这种情况下,不会再进行包装。

请确保如果自行在Spring配置中声明了TransactionAwareDataSourceProxy,不会将它用在已经配置Spring事务的资源上(例如DataSourceTransactionManager与JPATransactionManager就需要未代理的数据源)。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
                           http://www.springframework.org/schema/tx      http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

  <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
    <property name="driverClass" value="org.h2.Driver" />
    <property name="url" value="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000" />
    <property name="username" value="sa" />
    <property name="password" value="" />
  </bean>

  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>

  <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
    <property name="dataSource" ref="dataSource" />
    <property name="transactionManager" ref="transactionManager" />
    <property name="databaseSchemaUpdate" value="true" />
    <property name="jobExecutorActivate" value="false" />
  </bean>

  <bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
    <property name="processEngineConfiguration" ref="processEngineConfiguration" />
  </bean>

  <bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService" />
  <bean id="runtimeService" factory-bean="processEngine" factory-method="getRuntimeService" />
  <bean id="taskService" factory-bean="processEngine" factory-method="getTaskService" />
  <bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService" />
  <bean id="managementService" factory-bean="processEngine" factory-method="getManagementService" />

...

这个Spring配置文件的余下部分包含了在这个示例中要用到的bean与配置:

<beans>
  ...
  <tx:annotation-driven transaction-manager="transactionManager"/>

  <bean id="userBean" class="org.activiti.spring.test.UserBean">
    <property name="runtimeService" ref="runtimeService" />
  </bean>

  <bean id="printer" class="org.activiti.spring.test.Printer" />

</beans>

使用任何Spring的方式创建应用上下文(application context)。在这个例子中,可以使用classpath中的XML资源配置来创建Spring应用上下文:

ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
	"org/activiti/examples/spring/SpringTransactionIntegrationTest-context.xml");

或者在单元测试中:

@ContextConfiguration("classpath:org/activiti/spring/test/transaction/SpringTransactionIntegrationTest-context.xml")

现在就可以获取服务bean,并反射调用(invoke)它们的方法。ProcessEngineFactoryBean会为服务加上额外的拦截器(interceptor),为Activiti服务方法设置Propagation.REQUIRED事务语义(transaction semantics)。因此,我们可以像这样使用repositoryService部署流程:

RepositoryService repositoryService =
  (RepositoryService) applicationContext.getBean("repositoryService");
String deploymentId = repositoryService
  .createDeployment()
  .addClasspathResource("org/activiti/spring/test/hello.bpmn20.xml")
  .deploy()
  .getId();

还有另一种方法也可以使用。在这个例子中,userBean.hello()方法被Spring事务包围,Activiti服务方法的调用会加入这个事务。

UserBean userBean = (UserBean) applicationContext.getBean("userBean");
userBean.hello();

UserBean看起来像这样。请记着在上面的Spring bean配置中,我们已经将repositoryService注入了userBean。

public class UserBean {

  /** 已经由Spring注入 */
  private RuntimeService runtimeService;

  @Transactional
  public void hello() {
    // 这里可以在你的领域模型(domain model)中进行事务操作,
    // 它会与Activiti RuntimeService的startProcessInstanceByKey
    // 合并在同一个事务里
    runtimeService.startProcessInstanceByKey("helloProcess");
  }

  public void setRuntimeService(RuntimeService runtimeService) {
    this.runtimeService = runtimeService;
  }
}

5.3. 表达式 Expressions

当使用ProcessEngineFactoryBean时,默认BPMN流程中所有的表达式 expressions都可以“看见”所有的Spring bean。通过可以配置的map,可以限制表达式能使用的bean,甚至可以完全禁止表达式使用bean。下面的例子只暴露了一个bean(printer),可以使用“printer”作为key访问。要完全禁止表达式使用bean,可以将SpringProcessEngineConfiguration的‘beans’参数设为空list。如果不设置‘beans’参数,则上下文中的所有bean都将可以使用。

<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
  ...
  <property name="beans">
    <map>
      <entry key="printer" value-ref="printer" />
    </map>
  </property>
</bean>

<bean id="printer" class="org.activiti.examples.spring.Printer" />

现在可以在表达式中使用这个暴露的bean了:例如,SpringTransactionIntegrationTest hello.bpmn20.xml展示了如何通过UEL方法表达式(method expression)注入Spring bean:

<definitions id="definitions">

  <process id="helloProcess">

    <startEvent id="start" />
    <sequenceFlow id="flow1" sourceRef="start" targetRef="print" />

    <serviceTask id="print" activiti:expression="#{printer.printMessage()}" />
    <sequenceFlow id="flow2" sourceRef="print" targetRef="end" />

    <endEvent id="end" />

  </process>

</definitions>

其中Printer为:

public class Printer {

  public void printMessage() {
    System.out.println("hello world");
  }
}

Spring bean配置(上面已经展示过)为:

<beans>
  ...

  <bean id="printer" class="org.activiti.examples.spring.Printer" />

</beans>

5.4. 自动部署资源 Automatic resource deployment

集成Spring还提供了部署资源的特殊方式。在流程引擎配置中,可以指定一组资源。当流程引擎被创建时,这些资源都会被扫描并部署。有过滤器用于阻止重复部署。只有当资源确实发生变化时,才会重新部署至Activiti数据库。在Spring容器经常重启(例如测试时)的时候,这很有用。

这里有个例子:

<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
  ...
  <property name="deploymentResources"
    value="classpath*:/org/activiti/spring/test/autodeployment/autodeploy.*.bpmn20.xml" />
</bean>

<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
  <property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>

默认情况下,这个配置会将符合这个过滤器的所有资源组织在一起,作为Activiti引擎的一个部署。重复检测过滤器将作用于整个部署,避免重复地部署未改变资源。有时这不是你想要的。例如,如果用这种方式部署了一组资源,即使只有其中的一个资源发生了改变,整个部署都会被认为已经改变,因此这个部署中所有的所有流程定义都会被重新部署。这将导致每个流程定义都会刷新版本号(流程定义id会变化),即使实际上只有一个流程发生了变化。

可以使用SpringProcessEngineConfiguration中的额外参数+deploymentMode+,定制部署的选择方式。这个参数定义了在一组符合过滤器的资源中,组织部署的方式。默认这个参数有3个可用值:

  • default: 将所有资源组织在一个部署中,整体用于重复检测过滤。这是默认值,在未设置这个参数时也会用这个值。

  • single-resource: 为每个资源创建一个单独的部署,并用于重复检测过滤。当你希望单独部署每一个流程定义,并且在它发生变化时创建新的流程定义版本,应该使用这个值。

  • resource-parent-folder: 为同一个目录下的资源创建一个单独的部署,并用于重复检测过滤。这个参数值可以为大多数资源创建独立的部署。同时仍可以通过将部分资源放在同一个目录下,将它们组织在一起。这里有一个将deploymentMode设置为single-resource的例子:

<bean id="processEngineConfiguration"
    class="org.activiti.spring.SpringProcessEngineConfiguration">
  ...
  <property name="deploymentResources" value="classpath*:/activiti/*.bpmn" />
  <property name="deploymentMode" value="single-resource" />
</bean>

如果上述deploymentMode的参数值不能满足要求,还可以自定义组织部署的行为。创建SpringProcessEngineConfiguration的子类,并覆盖getAutoDeploymentStrategy(String deploymentMode)方法。这个方法决定了对于给定的deploymentMode参数值,使用何种部署策略。

5.5. 单元测试 Unit testing

与Spring集成后,业务流程可以非常简单地使用标准的 Activiti测试工具 Activiti testing facilities进行测试。下面的例子展示了如何通过典型的基于Spring的单元测试,对业务流程进行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:org/activiti/spring/test/junit4/springTypicalUsageTest-context.xml")
public class MyBusinessProcessTest {

  @Autowired
  private RuntimeService runtimeService;

  @Autowired
  private TaskService taskService;

  @Autowired
  @Rule
  public ActivitiRule activitiSpringRule;

  @Test
  @Deployment
  public void simpleProcessTest() {
    runtimeService.startProcessInstanceByKey("simpleProcess");
    Task task = taskService.createTaskQuery().singleResult();
    assertEquals("My Task", task.getName());

    taskService.complete(task.getId());
    assertEquals(0, runtimeService.createProcessInstanceQuery().count());

  }
}

请注意要让这个例子可以正常工作,需要在Spring配置中定义org.activiti.engine.test.ActivitiRule bean(在上面的例子中通过auto-wiring注入)。

<bean id="activitiRule" class="org.activiti.engine.test.ActivitiRule">
  <property name="processEngine" ref="processEngine" />
</bean>

5.6. 通过Hibernate 4.2.x使用JPA (JPA with Hibernate 4.2.x)

要在Activiti引擎的服务任务或者监听器逻辑中使用Hibernate 4.2.x JPA,需要添加Spring ORM的额外依赖。对Hibernate 4.1.x或更低则不需要。需要添加的依赖为:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-orm</artifactId>
  <version>${org.springframework.version}</version>
</dependency>

5.7. Spring Boot

Spring Boot是一个应用框架,按照官网的介绍,可以轻松地创建独立运行的,生产级别的,基于Spring的应用,并且可以“直接运行”。坚持使用Spring框架与第三方库,使你可以轻松地开始使用。大多数Spring Boot应用只需要很少的Spring配置

要获得更多关于Spring Boot的信息,请查阅http://projects.spring.io/spring-boot/

Activiti与Spring Boot的集成目前只是试验性的。我们已经与Spring的提交者共同开发,但为时尚早。我们欢迎试用并提供反馈。

5.7.1. 兼容性 Compatibility

Spring Boot需要JDK 7运行时环境。可以通过调整配置,在JDK6下运行。请查阅Spring Boot的文档。

5.7.2. 开始 Getting started

Spring Boot提倡约定大于配置。要开始工作,简单地在你的项目中添加spring-boot-starters-basic依赖。例如在Maven中:

<dependency>
	<groupId>org.activiti</groupId>
	<artifactId>activiti-spring-boot-starter-basic</artifactId>
	<version>${activiti.version}</version>
</dependency>

就这么简单。这个依赖会自动向classpath添加正确的Activiti与Spring依赖。现在你可以编写Spring Boot应用了:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

Activiti需要数据库存储数据。如果你运行上面的代码,会得到提示性的异常信息,指出需要在classpath中添加数据库驱动依赖。现在添加H2数据库依赖:

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<version>1.4.183</version>
</dependency>

应用这次可以启动了。你会看到类似这样的输出:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.6.RELEASE)

MyApplication                            : Starting MyApplication on ...
s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@33cb5951: startup date [Wed Dec 17 15:24:34 CET 2014]; root of context hierarchy
a.s.b.AbstractProcessEngineConfiguration : No process definitions were found using the specified path (classpath:/processes/**.bpmn20.xml).
o.activiti.engine.impl.db.DbSqlSession   : performing create on engine with resource org/activiti/db/create/activiti.h2.create.engine.sql
o.activiti.engine.impl.db.DbSqlSession   : performing create on history with resource org/activiti/db/create/activiti.h2.create.history.sql
o.activiti.engine.impl.db.DbSqlSession   : performing create on identity with resource org/activiti/db/create/activiti.h2.create.identity.sql
o.a.engine.impl.ProcessEngineImpl        : ProcessEngine default created
o.a.e.i.a.DefaultAsyncJobExecutor        : Starting up the default async job executor [org.activiti.spring.SpringAsyncExecutor].
o.a.e.i.a.AcquireTimerJobsRunnable       : {} starting to acquire async jobs due
o.a.e.i.a.AcquireAsyncJobsDueRunnable    : {} starting to acquire async jobs due
o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
MyApplication                            : Started MyApplication in 2.019 seconds (JVM running for 2.294)

只是在classpath中添加依赖,并使用@EnableAutoConfiguration注解,就会在幕后发生很多事情:

  • 自动创建了内存数据库(因为classpath中有H2驱动),并传递给Activiti流程引擎配置

  • 创建并暴露了Activiti ProcessEngine bean

  • 所有的Activiti服务都暴露为Spring bean

  • 创建了Spring Job Executor

并且,processes目录下的任何BPMN 2.0流程定义都会被自动部署。创建processes目录,并在其中创建示例流程定义(命名为one-task-process.bpmn20.xml):

<?xml version="1.0" encoding="UTF-8"?>
<definitions
        xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
        xmlns:activiti="http://activiti.org/bpmn"
        targetNamespace="Examples">

    <process id="oneTaskProcess" name="The One Task Process">
        <startEvent id="theStart" />
        <sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
        <userTask id="theTask" name="my task" />
        <sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
        <endEvent id="theEnd" />
    </process>

</definitions>

然后添加下列代码,以测试部署是否生效。CommandLineRunner是一个特殊的Spring bean,在应用启动时执行:

@Configuration
@ComponentScan
@EnableAutoConfiguration
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Bean
    public CommandLineRunner init(final RepositoryService repositoryService,
                                  final RuntimeService runtimeService,
                                  final TaskService taskService) {

        return new CommandLineRunner() {
            @Override
            public void run(String... strings) throws Exception {
                System.out.println("Number of process definitions : "
                	+ repositoryService.createProcessDefinitionQuery().count());
                System.out.println("Number of tasks : " + taskService.createTaskQuery().count());
                runtimeService.startProcessInstanceByKey("oneTaskProcess");
                System.out.println("Number of tasks after process start: " + taskService.createTaskQuery().count());
            }
        };

    }

}

会得到这样的输出:

Number of process definitions : 1
Number of tasks : 0
Number of tasks after process start : 1

5.7.3. 更换数据源与连接池 Changing the database and connection pool

上面也提到过,Spring Boot的约定大于配置。默认情况下,如果classpath中只有H2,就会创建内存数据库,并传递给Activiti流程引擎配置。

可以简单地通过提供Datasource bean来覆盖默认配置,来更换数据源。我们在这里使用DataSourceBuilder类,这是Spring Boot的辅助类。如果classpath中有Tomcat, HikariCP 或者 Commons DBCP,就会(按照这个顺序,先是Tomcat)选择一个(作为连接池)。例如,要切换到MySQL数据库:

@Bean
public DataSource database() {
    return DataSourceBuilder.create()
        .url("jdbc:mysql://127.0.0.1:3306/activiti-spring-boot?characterEncoding=UTF-8")
        .username("alfresco")
        .password("alfresco")
        .driverClassName("com.mysql.jdbc.Driver")
        .build();
}

从Maven依赖中移除H2,并为classpath添加MySQL驱动与Tomcat连接池:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.34</version>
</dependency>
<dependency>
	<groupId>org.apache.tomcat</groupId>
	<artifactId>tomcat-jdbc</artifactId>
	<version>8.0.15</version>
</dependency>

应用这次启动后,可以看到使用了MySQL作为数据库(也使用了Tomcat连接池框架):

org.activiti.engine.impl.db.DbSqlSession   : performing create on engine with resource org/activiti/db/create/activiti.mysql.create.engine.sql
org.activiti.engine.impl.db.DbSqlSession   : performing create on history with resource org/activiti/db/create/activiti.mysql.create.history.sql
org.activiti.engine.impl.db.DbSqlSession   : performing create on identity with resource org/activiti/db/create/activiti.mysql.create.identity.sql

多次重启应用,会发现任务的数量增加了(H2内存数据库在关闭后会丢失,而MySQL不会)。

5.7.4. REST支持 (REST support)

通常在嵌入的Activiti引擎之上,需要提供REST API(用于与公司的不同服务交互)。Spring Boot让这变得很容易。在classpath中添加下列依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>${spring.boot.version}</version>
</dependency>

创建一个新的Spring服务类,并创建两个方法:一个用于启动流程,另一个用于获得给定任务办理人的任务列表。在这里我们简单地包装了Activiti调用,但很明显在实际使用场景中会比这复杂得多。

@Service
public class MyService {

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

	@Transactional
    public void startProcess() {
        runtimeService.startProcessInstanceByKey("oneTaskProcess");
    }

	@Transactional
    public List<Task> getTasks(String assignee) {
        return taskService.createTaskQuery().taskAssignee(assignee).list();
    }

}

现在可以用@RestController来注解类,以创建REST端点(endpoint)。在这里我们简单地委派给了上面定义的服务。

@RestController
public class MyRestController {

    @Autowired
    private MyService myService;

    @RequestMapping(value="/process", method= RequestMethod.POST)
    public void startProcessInstance() {
        myService.startProcess();
    }

    @RequestMapping(value="/tasks", method= RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
    public List<TaskRepresentation> getTasks(@RequestParam String assignee) {
        List<Task> tasks = myService.getTasks(assignee);
        List<TaskRepresentation> dtos = new ArrayList<TaskRepresentation>();
        for (Task task : tasks) {
            dtos.add(new TaskRepresentation(task.getId(), task.getName()));
        }
        return dtos;
    }

    static class TaskRepresentation {

        private String id;
        private String name;

        public TaskRepresentation(String id, String name) {
            this.id = id;
            this.name = name;
        }

         public String getId() {
            return id;
        }
        public void setId(String id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }

    }

}

自动组件扫描(@ComponentScan)会找到我们添加在应用类上的@Service@RestController。再次运行应用类,现在可以与REST API交互了。例如使用cURL:

curl http://localhost:8080/tasks?assignee=kermit
[]

curl -X POST  http://localhost:8080/process
curl http://localhost:8080/tasks?assignee=kermit
[{"id":"10004","name":"my task"}]

5.7.5. JPA支持 (JPA support)

要为Spring Boot中的Activiti添加JPA支持,增加下列依赖:

<dependency>
	<groupId>org.activiti</groupId>
	<artifactId>activiti-spring-boot-starter-jpa</artifactId>
	<version>${activiti.version}</version>
</dependency>

这会加入Spring的配置,以及JPA用的bean。默认使用Hibernate作为JPA提供者。

创建一个简单的实体类:

@Entity
class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String firstName;

    private String lastName;

    private Date birthDate;

    public Person() {
    }

    public Person(String username, String firstName, String lastName, Date birthDate) {
        this.username = username;
        this.firstName = firstName;
        this.lastName = lastName;
        this.birthDate = birthDate;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }
}

默认情况下,如果没有使用内存数据库,不会自动创建数据库表。在classpath中创建__application.properties_文件并加入下列参数:

spring.jpa.hibernate.ddl-auto=update

添加下列类:

public interface PersonRepository extends JpaRepository<Person, Long> {

    Person findByUsername(String username);

}

这是一个Spring存储(repository),提供了直接可用的增删改查。我们添加了通过username查找Person的方法。Spring会基于约定自动实现它(也就是使用names属性)。

现在进一步增强我们的服务:

  • 在类上添加@Transactional。请注意,通过上面添加的JPA依赖,之前使用的DataSourceTransactionManager会自动替换为JpaTransactionManager。

  • startProcess增加了任务办理人参数,用于查找Person,并将Person JPA对象作为流程变量存入流程实例。

  • 添加了创建示例用户的方法。CommandLineRunner使用它打桩数据库。

@Service
@Transactional
public class MyService {

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    @Autowired
    private PersonRepository personRepository;

    public void startProcess(String assignee) {

        Person person = personRepository.findByUsername(assignee);

        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("person", person);
        runtimeService.startProcessInstanceByKey("oneTaskProcess", variables);
    }

    public List<Task> getTasks(String assignee) {
        return taskService.createTaskQuery().taskAssignee(assignee).list();
    }

    public void createDemoUsers() {
		 if (personRepository.findAll().size() == 0) {
            personRepository.save(new Person("jbarrez", "Joram", "Barrez", new Date()));
            personRepository.save(new Person("trademakers", "Tijs", "Rademakers", new Date()));
        }
    }

}

CommandLineRunner现在为:

@Bean
public CommandLineRunner init(final MyService myService) {

	return new CommandLineRunner() {
    	public void run(String... strings) throws Exception {
        	myService.createDemoUsers();
        }
    };

}

RestController也有小改动(只展示新方法),以配合上面的改动。HTTP POST现在有了body,存有办理人用户名:

@RestController
public class MyRestController {

    @Autowired
    private MyService myService;

    @RequestMapping(value="/process", method= RequestMethod.POST)
    public void startProcessInstance(@RequestBody StartProcessRepresentation startProcessRepresentation) {
        myService.startProcess(startProcessRepresentation.getAssignee());
    }

   ...

    static class StartProcessRepresentation {

        private String assignee;

        public String getAssignee() {
            return assignee;
        }

        public void setAssignee(String assignee) {
            this.assignee = assignee;
        }
    }

最后,为了试用Spring-JPA-Activiti集成,我们在流程定义中,将Person JPA对象的id指派为任务办理人:

<userTask id="theTask" name="my task" activiti:assignee="${person.id}"/>

现在可以通过在POST body中提供用户名,启动一个新的流程实例:

curl -H "Content-Type: application/json" -d '{"assignee" : "jbarrez"}' http://localhost:8080/process

也可以使用Person id获取任务列表:

curl http://localhost:8080/tasks?assignee=1

[{"id":"12505","name":"my task"}]

5.7.6. 扩展阅读 Further Reading

很明显还有很多Spring Boot相关的内容还没有提及,例如简单的JTA集成,构建能在主流应用服务器上运行的war文件。还有很多关于Spring Boot集成的内容:

  • Actuator支持

  • Spring Integration支持

  • Rest API集成:启动Spring应用中嵌入的Activiti Rest API

  • Spring Security支持

目前这些领域都是初版,未来会不断演进。

6. 部署 Deployment

6.1. 业务存档 Business archives

要部署流程,需要将它们包装在业务存档里。业务存档是Activiti引擎的部署单元,也就是一个zip文件。可以包含BPMN 2.0流程,任务表单,规则,与其他类型的文件。总的来说,业务存档包含一组已命名的资源。

当部署业务存档时,会扫描具有.bpmn20.xml.bpmn扩展名的BPMN文件。每一个这种文件都会被解析,并可以包含多个流程定义。

业务存档中的Java类不会添加至classpath。业务存档中,所有流程定义使用的自定义类(例如Java服务任务service tasks或者事件监听器实现event listener implementations),都应该放在用于运行流程的activiti引擎的classpath下。

6.1.1. 编程方式部署 Deploying programmatically

从zip文件部署业务存档,可以这样做:

String barFileName = "path/to/process-one.bar";
ZipInputStream inputStream = new ZipInputStream(new FileInputStream(barFileName));

repositoryService.createDeployment()
    .name("process-one.bar")
    .addZipInputStream(inputStream)
    .deploy();

也可以从不同资源构建部署。查看javadoc获取更多信息。

6.1.2. 使用Activiti Exploreer部署 (Deploying with Activiti Explorer)

Activiti Explorer web应用,可以通过web应用用户界面,上传bar文件(或者单独的bpmn20.xml文件)。选择Management页签并点击Deployment:

deployment.explorer

会弹出窗口,让你选择电脑中的文件,或者(如果你的浏览器支持)可以直接拖放文件到指定区域。

deployment.explorer.2

6.2. 外部资源 External resources

流程定义保存在Activiti数据库中。这些流程定义,在使用服务任务、执行监听器或执行Activiti配置文件中定义的Spring bean时,可以引用委托类。这些类与Spring配置文件,需要对所有可能运行这个流程定义的流程引擎都可用。

6.2.1. Java类 Java classes

所有流程中用到的自定义类(例如服务任务、事件监听器、任务监听器等中,用到的JavaDelegate),在流程启动时,都需要存在于引擎的classpath中。

然而在业务存档部署时,classpath中不是必须要有这些类。这意味着使用Ant部署新业务存档时,你的代理类不必须放在classpath中。

当使用演示配置,且希望添加自定义类时,需要在activiti-explorer或activiti-rest web应用库中,添加包含有你的自定义类的jar。别忘了也要添加你的自定义类的依赖(若有)。或者,也可以将你的依赖添加到Tomcat的库文件夹,${tomcat.home}/lib中。

6.2.2. 在流程中使用Spring bean (Using Spring beans from a process)

当在表达式或脚本中使用Spring bean时,执行该流程定义的引擎需要可以使用这些bean。如果你自行构建web应用,并按照spinrg集成章节 the spring integration section的介绍,在上下文中配置流程引擎,就可以直接使用。但也请牢记在心,如果使用Activiti rest web应用,就需要更新它的上下文配置。用包含你的Spring上下文配置的activiti-context.xml文件,替换activiti-rest/lib/activiti-cfg.jar jar文件中的activiti.cfg.xml

6.2.3. 创建单独应用 Creating a single app

如果不想费心保证所有流程引擎都在classpath中含有所有需要的代理类,以及保证它们都使用了正确的Spring配置,也可以考虑将Activiti rest web应用嵌入你自己的web应用,也就是说只有一个单独的ProcessEngine

6.3. 流程定义的版本 Versioning of process definitions

BPMN并没有版本的概念。这其实很好,因为可执行的BPMN流程文件很可能已经作为你的开发项目的一部分,保存在版本管理系统仓库中了(例如Subversion,Git,或者Mercurial)。流程定义的版本在部署时创建。在部署时,Activiti会在保存至Activiti数据库前,为ProcessDefinition指定版本。

对于业务存档中的每个流程定义,下列步骤都会执行,以初始化keyversionnameid参数:

  • XML文件中的流程定义id属性作为流程定义的key参数。

  • XML文件中的流程定义name属性作为流程定义的name参数。如果未给定name属性,会使用id作为name。

  • 当每个key的流程第一次部署时,指定版本为1。对其后所有使用相同key的流程定义,部署时版本会在该key当前已部署的最高版本号基础上加1。key参数用于区分流程定义。

  • id参数设置为{processDefinitionKey}:{processDefinitionVersion}:{generated-id},其中generated-id是一个唯一数字,用以保证在集群环境下,流程定义缓存中,流程id的唯一性。

以下面的流程为例

<definitions id="myDefinitions" >
  <process id="myProcess" name="My important process" >
    ...

当部署这个流程定义时,数据库中的流程定义会是这个样子:

id key name version

myProcess:1:676

myProcess

My important process

1

如果我们现在部署同一个流程的更新版本(例如改变部分用户任务),且保持流程定义的id不变,那么流程定义表中会包含下面的记录:

id key name version

myProcess:1:676

myProcess

My important process

1

myProcess:2:870

myProcess

My important process

2

当调用runtimeService.startProcessInstanceByKey("myProcess")时,会使用版本2的流程定义,因为这是这个流程定义的最新版本。

如果再创建第二个流程,如下定义并部署至Activiti,表中会增加第三行。

<definitions id="myNewDefinitions" >
  <process id="myNewProcess" name="My important process" >
    ...

表将显示类似:

id key name version

myProcess:1:676

myProcess

My important process

1

myProcess:2:870

myProcess

My important process

2

myNewProcess:1:1033

myNewProcess

My important process

1

请注意新流程的key与第一个流程的不同。即使name是相同的(我们可能本应该也改变它),Activiti也只用id属性来区分流程。因此新的流程部署时版本为1.

6.4. 提供流程图 Providing a process diagram

部署可以添加流程图图片。这个图片将存储在Activiti数据库中,并可以使用API访问。这个图片可以用在Activiti Explorer中,使流程形象化。

如果在classpath中,有一个org/activiti/expenseProcess.bpmn20.xml流程,key为’expense'。则流程图图片会使用下列命名约定(按此顺序):

  • 如果部署中有图片资源,并且它的名字为BPMN 2.0 XML文件名,加上流程key以及图片后缀,则使用这个图片。在我们的例子中,就是org/activiti/expenseProcess.expense.png(或者.jpg/gif)。如果一个BPMN 2.0 XML文件中有多个流程定义,这个方式就很合理,因为每一个流程图的文件名中都有流程key。

  • 如果没有这种图片,就会寻找部署中匹配BPMN 2.0 XML文件名的图片资源。在我们的例子中,就是org/activiti/expenseProcess.png。请注意这就意味着同一个BPMN 2.0文件中的每一个流程定义,都会使用同一个流程图图片。很显然,如果每个BPMN 2.0 XML文件中只有一个流程定义,就没有问题。

用编程方式部署的例子:

repositoryService.createDeployment()
  .name("expense-process.bar")
  .addClasspathResource("org/activiti/expenseProcess.bpmn20.xml")
  .addClasspathResource("org/activiti/expenseProcess.png")
  .deploy();

图片资源可用下面的API获取:

ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
  .processDefinitionKey("expense")
  .singleResult();

String diagramResourceName = processDefinition.getDiagramResourceName();
InputStream imageStream = repositoryService.getResourceAsStream(
    processDefinition.getDeploymentId(), diagramResourceName);

6.5. 生成流程图 Generating a process diagram

如果部署时没有按上小节介绍的提供图片,且流程定义中包含必要的“图形交换(diagram interchange)”信息,Activiti引擎会生成流程图。

可以用与部署时提供图片完全相同的方法获取图片资源。

deployment.image.generation

如果由于某种原因,不需要或不希望在部署时生成流程图,可以在流程引擎配置中设置isCreateDiagramOnDeploy参数:

<property name="createDiagramOnDeploy" value="false" />

这样就不会生成流程图了。

6.6. 类别 Category

部署与流程定义都可以定义类别。流程定义的类别使用BPMN文件中的<definitions …​ targetNamespace="yourCategory" …​/>设置。

部署的类别可用API如此设定:

repositoryService
    .createDeployment()
    .category("yourCategory")
    ...
    .deploy();

7. BPMN 2.0介绍 BPMN 2.0 Introduction

7.1. BPMN是什么? What is BPMN?

7.2. 定义流程 Defining a process

这个介绍的写作,基于使用Eclipse IDE创建与编辑文件。但其实只有很少的部分使用了Eclipse的特性。可以使用你喜欢的任何其他工具创建包含BPMN 2.0的XML文件。

创建一个新的XML文件(在任意项目上右击,选择New→Other→XML-XML File)并命名。确保该文件名以.bpmn20.xml或.bpmn结尾,因为只有这样,引擎才会在部署时选择这个文件。

new.bpmn.procdef

BPMN 2.0概要(schema)的根元素(root element)是definitions元素。在这个元素中,可以定义多个流程定义(然而我们建议在每个文件中,只有一个流程定义。这样可以简化已部署流程的管理)。下面显示的是一个空流程定义。请注意definitions元素最少需要包含xmlnstargetNamespace声明。targetNamespace可以为空,用于对流程定义进行分类。

<definitions
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="Examples">

  <process id="myProcess" name="My First Process">
    ..
  </process>

</definitions>

BPMN 2.0 XML概要,除了使用Eclipse中的XML分类配置,也可以使用在线概要。

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL
                    http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd

process元素有两个属性:

  • id: 必填属性,映射为Activiti ProcessDefinition对象的key参数。可以使用RuntimeService中的startProcessInstanceByKey方法,使用id来启动这个流程定义的新流程实例。这个方法总会使用流程定义的最后部署版本

ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess");
  • 请注意这与调用startProcessInstanceById方法不同。startProcessInstanceById方法的参数为Activiti引擎在部署时生成的字符串id,可以通过调用processDefinition.getId()方法获取。生成id的格式为key:version,长度限制为64字符。如果有ActivitiException显示生成id过长,请限制流程key参数(即这个id字段)的文字长度。

  • name: 可选属性,映射为ProcessDefinitionname参数。引擎自己不会使用这个参数,可以用于例如,在用户界面上显示更用户友好的名字。

[[10minutetutorial]]

7.3. 准备:十分钟教程 Getting started: 10 minute tutorial

这个章节包含了一个(很简单的)业务流程,用于介绍一些基本的Activiti概念,以及Activiti API。

7.3.1. 先决条件 Prerequisites

这个教程假设你已经运行了Activiti演示配置,并使用独立的H2服务器。编辑db.properties并设置jdbc.url=jdbc:h2:tcp://localhost/activiti,然后按照H2文档的介绍运行独立服务器。

7.3.2. 目标 Goal

这个教程的目标是学习Activiti以及BPMN 2.0的一些基础概念。最后成果是一个简单的Java SE程序,部署了一个流程定义,并通过Activiti引擎API与流程进行交互。当然,在这个教程里学到的东西,也可以基于你的业务流程,用于构建你自己的web应用程序。

7.3.3. 用例 Use case

用例很直接:有一个公司,叫做BPMCorp。在BPMCorp中,每月需要为投资人撰写一份金融报告,由会计部门负责。在报告完成后,需要上层经理中的一位进行审核,然后才能发给所有投资人。

7.3.4. 流程图 Process diagram

上面描述的业务流程,可以使用Activiti Designer可视地画出。但是在这个教程里,我们自己写XML,这样可以学习更多。这个流程的图形化BPMN 2.0注记像是这样:

financial.report.example.diagram

我们看到的是一个空启动事件 none Start Event(左边的圆圈),接下来是两个用户任务 User Tasks'Write monthly financial report(撰写月度金融报告)''Verify monthly financial report(审核月度金融报告)'。最后是空结束事件 none end event(右边的粗线条圆圈)。

7.3.5. XML表现 XML representation

这个业务流程的XML版本(FinancialReportProcess.bpmn20.xml)像下面显示的一样。很容易认出流程的主要元素(点击链接可以跳转到BPMN 2.0结构的详细章节):

<definitions id="definitions"
  targetNamespace="http://activiti.org/bpmn20"
  xmlns:activiti="http://activiti.org/bpmn"
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">

	<process id="financialReport" name="Monthly financial report reminder process">

	  <startEvent id="theStart" />

	  <sequenceFlow id='flow1' sourceRef='theStart' targetRef='writeReportTask' />

	  <userTask id="writeReportTask" name="Write monthly financial report" >
	    <documentation>
	      Write monthly financial report for publication to shareholders.
	    </documentation>
	    <potentialOwner>
	      <resourceAssignmentExpression>
	        <formalExpression>accountancy</formalExpression>
	      </resourceAssignmentExpression>
	    </potentialOwner>
	  </userTask>

	  <sequenceFlow id='flow2' sourceRef='writeReportTask' targetRef='verifyReportTask' />

	  <userTask id="verifyReportTask" name="Verify monthly financial report" >
	    <documentation>
	      Verify monthly financial report composed by the accountancy department.
	      This financial report is going to be sent to all the company shareholders.
	    </documentation>
	    <potentialOwner>
	      <resourceAssignmentExpression>
	        <formalExpression>management</formalExpression>
	      </resourceAssignmentExpression>
	    </potentialOwner>
	  </userTask>

	  <sequenceFlow id='flow3' sourceRef='verifyReportTask' targetRef='theEnd' />

	  <endEvent id="theEnd" />

	</process>

</definitions>

7.3.6. 启动流程实例 Starting a process instance

现在我们已经创建了业务流程的流程定义。使用这样的流程定义,可以创建流程实例。在这个例子中,一个流程实例将对应一个特定月份的一次财经报告创建与审核工作。所有流程实例共享相同的流程定义。

要用给定的流程定义创建流程实例,需要首先部署(deploy)流程定义。部署流程定义意味着两件事:

  • 流程定义将会存储在Activiti引擎配置的持久化数据库中。因此通过部署业务流程,保证了引擎在重启后也能找到流程定义。

  • BPMN 2.0流程文件会解析为内存中的对象模型。这个模型可以通过Activiti API操纵。

更多关于部署的信息可以在部署专门章节中找到。

与该章节的描述一样,部署有很多种方式。一种是通过下面展示的API。请注意所有与Activiti引擎的交互都要通过它的服务(services)

Deployment deployment = repositoryService.createDeployment()
  .addClasspathResource("FinancialReportProcess.bpmn20.xml")
  .deploy();

现在可以使用在流程定义中定义的id(参见XML文件中的流程元素)启动新流程实例。请注意这个id在Activiti术语中被称作key

ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("financialReport");

这会创建流程实例,并首先通过开始事件。在开始事件后,会沿着所有出口顺序流(在这个例子中只有一个)继续,并到达第一个任务(撰写月度金融报告 write monthly financial report)。这时,Activiti引擎会在持久化数据库中存储一个任务。同时,会解析这个任务附加的分配用户或组,也保存在数据库中。请注意,Activiti引擎会持续执行流程步骤,直到到达等待状态 wait state,例如用户任务。在这种等待状态时,流程实例的当前状态会存储在数据库中,并保持这个状态,直到用户决定完成任务。这时,引擎会继续执行,直到遇到新的等待状态,或者流程结束。如果在这期间引擎重启或崩溃,流程的状态也仍在数据库中安全并妥善的保存。

在任务创建后,startProcessInstanceByKey方法会返回,因为用户任务活动是一个等待状态。在这个例子里,这个任务分配给一个组。这意味着这个组的每一个成员都是处理这个任务的候选人 candidate

现在可以将这些整合起来,创建一个简单的Java程序。创建一个新的Eclipse项目,在它的classpath中添加Activiti jar与依赖(可以在Activiti发行版的libs目录下找到)。在能够调用Activiti服务前,需要首先构建ProcessEngine (流程引擎),用于访问服务。这里我们使用'standalone 独立'配置,这个配置会构建ProcessEngine,并使用与演示配置中相同的数据库。

可以从这里下载流程定义XML。这个文件包含了上面展示的XML,同时包含了必要的BPMN图形交互信息 diagram interchange information,用于在Activiti的工具中可视化展示流程。

public static void main(String[] args) {

  // 创建Activiti流程引擎 Create Activiti process engine
  ProcessEngine processEngine = ProcessEngineConfiguration
    .createStandaloneProcessEngineConfiguration()
    .buildProcessEngine();

  // 获取Activiti服务 Get Activiti services
  RepositoryService repositoryService = processEngine.getRepositoryService();
  RuntimeService runtimeService = processEngine.getRuntimeService();

  // 部署流程定义 Deploy the process definition
  repositoryService.createDeployment()
    .addClasspathResource("FinancialReportProcess.bpmn20.xml")
    .deploy();

  // 启动流程实例 Start a process instance
  runtimeService.startProcessInstanceByKey("financialReport");
}

7.3.7. 任务列表 Task lists

现在可以通过添加下列逻辑,获取这个任务:

List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit").list();

请注意传递给这个操作的用户需要是accountancy组的成员,因为在流程定义中是这么声明的:

<potentialOwner>
  <resourceAssignmentExpression>
    <formalExpression>accountancy</formalExpression>
  </resourceAssignmentExpression>
</potentialOwner>

也可以使用任务查询API,用组名查得相同结果。可以在代码中添加下列逻辑:

TaskService taskService = processEngine.getTaskService();
List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();

因为我们将ProcessEngine配置为使用与演示配置中相同的数据库,因此可以登录Activiti Explorer。默认情况下,accountancy组中没有用户。使用kermit/kermit登录,点击Groups,然后"Create group (创建组)"。然后点击Users,并向组中添加fozzie。现在使用fozzie/fozzie登录,就会发现在选择了Processes页面,点击'Monthly financial report (月度金融报告)''Actions'栏的'Start Process (开始流程)'链接后,可以启动我们的业务流程。

bpmn.financial.report.example.start.process

前面已经解释过,流程会执行到第一个用户任务。因为使用Fozzie登录,就可以看到在启动流程实例后,他有一个新的候选任务(candidate task)。选择Tasks页面来查看这个新任务。请注意即使流程是由其他人启动的,accountancy组中的每一个人仍然都能看到这个候选任务。

bpmn.financial.report.example.task.assigned

7.3.8. 申领任务 Claiming the task

会计师(accountancy组的成员)现在需要申领任务。申领任务后,这个用户会成为任务的执行人 (assignee),这个任务也会从accountancy组的其他成员的任务列表中消失。申领任务通过编程方式如下实现:

taskService.claim(task.getId(), "fozzie");

这个任务现在在申领任务者的个人任务列表中

List<Task> tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();

在Activiti Explorer UI中,点击claim按钮会执行相同操作。这个任务会转移到登录用户的个人任务列表中。也可以看到任务执行人变更为当前登录用户。

bpmn.financial.report.example.claim.task

7.3.9. 完成任务 Completing the task

会计师(accountancy组的成员)现在需要开始撰写金融报告了。一旦报告完成,他就可以完成任务。这意味着这个任务的所有工作都已经完成。

taskService.complete(task.getId());

对于Activiti引擎来说,这是个外部信号,指示流程实例需要继续执行。任务本身会从运行时数据中移除,并继续这个任务唯一的出口转移(outgoing transition),将执行移至第二个任务('verification of the report 审核月度报告')。与上面介绍的第一个任务使用的相同的机制,会用于为第二个任务分配执行人。有一点小区别,这个任务会分配给management组。

在演示设置中,完成任务可以通过点击任务列表中的complete按钮。因为Fozzie不是经理,我们需要登出Activiti Explorer,并用kermit(他是经理)登录。第二个任务现在可以在未分配任务列表中看到。

7.3.10. 结束流程 Ending the process

与之前完全相同的方式,可以获取并申领审核任务。完成这个第二个任务,会将流程执行移至结束事件,并结束流程实例。这个流程实例与所有相关的运行时执行数据都会从数据库中移除。

登录至Activiti Explorer可以验证这一点,流程执行的存储表中找不到记录。

bpmn.financial.report.example.process.ended

也可以通过编程方式,使用historyService验证流程已经结束

HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance =
historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());

7.3.11. 代码总结 Code overview

将之前章节的所有代码片段整合起来,会得到类似这样的代码(这段代码考虑到了你可能已经使用Activiti Explorer UI启动了一些流程实例。代码中总是获取任务列表而不是一个任务,因此总能执行):

public class TenMinuteTutorial {

  public static void main(String[] args) {

    // 创建Activiti流程引擎 Create Activiti process engine
    ProcessEngine processEngine = ProcessEngineConfiguration
      .createStandaloneProcessEngineConfiguration()
      .buildProcessEngine();

    // 获取Activiti服务 Get Activiti services
    RepositoryService repositoryService = processEngine.getRepositoryService();
    RuntimeService runtimeService = processEngine.getRuntimeService();

    // 部署流程定义 Deploy the process definition
    repositoryService.createDeployment()
      .addClasspathResource("FinancialReportProcess.bpmn20.xml")
      .deploy();

    // 启动流程实例 Start a process instance
    String procId = runtimeService.startProcessInstanceByKey("financialReport").getId();

    // 获取第一个任务 Get the first task
    TaskService taskService = processEngine.getTaskService();
    List<Task> tasks = taskService.createTaskQuery().taskCandidateGroup("accountancy").list();
    for (Task task : tasks) {
      System.out.println("Following task is available for accountancy group: " + task.getName());

      // 申领 claim it
      taskService.claim(task.getId(), "fozzie");
    }

    // 验证Fozzie获取了任务 Verify Fozzie can now retrieve the task
    tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
    for (Task task : tasks) {
      System.out.println("Task for fozzie: " + task.getName());

      // 完成任务 Complete the task
      taskService.complete(task.getId());
    }

    System.out.println("Number of tasks for fozzie: "
            + taskService.createTaskQuery().taskAssignee("fozzie").count());

    // 获取并申领第二个任务 Retrieve and claim the second task
    tasks = taskService.createTaskQuery().taskCandidateGroup("management").list();
    for (Task task : tasks) {
      System.out.println("Following task is available for management group: " + task.getName());
      taskService.claim(task.getId(), "kermit");
    }

    // 完成第二个任务并结束流程 Completing the second task ends the process
    for (Task task : tasks) {
      taskService.complete(task.getId());
    }

    // 验证流程已经结束 verify that the process is actually finished
    HistoryService historyService = processEngine.getHistoryService();
    HistoricProcessInstance historicProcessInstance =
      historyService.createHistoricProcessInstanceQuery().processInstanceId(procId).singleResult();
    System.out.println("Process instance end time: " + historicProcessInstance.getEndTime());
  }

}

7.3.12. 继续提高 Future enhancements

可以看出这个业务流程太简单了,不能实际使用。然而,随着继续浏览Activiti中可用的BPMN 2.0结构,可以增强业务流程通过:

  • 定义网关 gateway执行选择。这样,经理可以驳回金融报告,并重新为会计师创建任务。

  • 定义并使用变量 variables。这样可以存储或引用报告,并可以在表单中显示它。

  • 在流程结束处定义服务任务 service task,将报告发送给每一个投资人。

  • 等等。

8. BPMN 2.0 结构 BPMN 2.0 Constructs

本章节包含了Activiti支持的BPMN 2.0结构,以及对BPMN标准的自定义扩展。

8.1. 自定义扩展 Custom extensions

BPMN 2.0标准对流程的所有的参与者都是个好东西。最终用户不需要因为依赖专利解决方案,而被供应商“绑架”。Activiti之类的开源框架,也可以提供与大型供应商的解决方案相同(经常是更好;-)的实现。有了BPMN 2.0标准,从大型供应商解决方案向Activiti的转变,就变得简单平滑。

然而标准的缺点,是它通常是不同公司(不同观点)大量讨论与妥协的结果。作为阅读BPMN 2.0 XML流程定义的开发者,有时会觉得某些结构或做事方法太笨重了。Activiti将开发者的感受放在最高优先,因此引入了一些Activiti BPMN extensions(扩展)。这些“扩展”并不在BPMN 2.0规格中,有些是新结构,有些是对特定结构的简化。

尽管BPMN 2.0规格明确指出可以支持自定义扩展,我们仍做了如下保证:

  • 自定义扩展的前提是,做事情的标准方式可以进行更简化的改造。因此当你决定使用自定义扩展时,不用担心无路可退(仍然可以用标准方式)。

  • 使用自定义扩展时,总是通过为新的XML元素、属性等提供activiti:命名空间前缀,明确标识出来。

因此是否使用自定义扩展,完全取决于你自己。有些其他因素会影响选择(图形化编辑器的使用,公司策略,等等)。我们提供扩展,只是因为相信,标准中的某些地方可以用更简单或效率更高的方式处理。请不要吝啬给我们反馈对扩展的评价(正面的和/或负面的),也可以给我们提供关于自定义扩展的新想法。说不定某一天,你的想法会成为规范的一部分!

8.2. 事件 Events

事件通常用于为流程生命周期中发生的事情建模。事件总是图形化为圆圈。在BPMN 2.0中,有两种主要的事件分类:捕获(catching)抛出(throwing)事件。

  • 捕获: 当流程执行到达这个事件时,会等待直到触发器动作。触发器的类型,由其中的图标,或者说XML中的类型声明而定义。捕获事件与抛出事件显示上的区别,是其内部的图标没有填充(也就是说,是白色的)。

  • 抛出: 当流程执行到达这个事件时,会触发一个触发器。触发器的类型,由其中的图标,或者说XML中的类型声明而定义。抛出事件与捕获事件显示上的区别,是其内部的图标填充为黑色。

8.2.1. 事件定义 Event Definitions

事件定义,定义了事件的语义。没有事件定义的话,事件就“不做什么特别的事情”。例如一个没有事件定义的开始事件,并不限定具体是什么启动了流程。如果为这个开始事件添加事件定义(例如定时器事件定义),就声明了启动流程的“类型”(例如对于定时器事件定义,就是到达了特定的时间点)。

8.2.2. 定时器事件定义 Timer Event Definitions

定时器事件,是由定义的定时器触发的事件。可以用于开始事件 start event中间事件 intermediate event,或边界事件 boundary event。定时器事件的行为,取决于所使用的业务日历(business calendar)。定时器事件有默认的业务日历,但也可以为每个定时器事件定义,定义业务日历。

<timerEventDefinition activiti:businessCalendarName="custom">
    ...
</timerEventDefinition>

其中businessCalendarName指向流程引擎配置中的业务日历。如果省略业务日历定义,就使用默认业务日历。

定时器定义必须且只能使用下列的一种元素:

  • timeDate。这个方式指定了ISO 8601格式的固定时间。 在这个时间点,会触发触发器。例如:

<timerEventDefinition>
    <timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
  • timeDuration。要定义在触发前,定时器需要等待多长时间,可以用timeDuration作为timerEventDefinition的子元素来指定。使用ISO 8601格式(BPMN 2.0规范要求)。例如(等待10天):

<timerEventDefinition>
    <timeDuration>P10D</timeDuration>
</timerEventDefinition>
  • timeCycle。指定重复周期,可用于周期性启动流程,或者为超期用户任务多次发送提醒。这个元素可以使用两种格式。第一种是按照ISO 8601标准定义的循环时间周期。例如(三次重复间隔,每次间隔为10小时):

<timerEventDefinition>
    <timeCycle activiti:endDate="2015-02-25T16:42:11+00:00">R3/PT10H</timeCycle>
</timerEventDefinition>

也可以指定endDate,作为timeCycle的可选属性,或者像这样直接写在时间表达式的结尾:R3/PT10H/${EndDate}。 当到达endDate时,应用会停止,并为该任务创建其他作业。 可以使用ISO 8601标准的静态值,比如"2015-02-25T16:42:11+00:00"。也可以使用变量${EndDate}

<timerEventDefinition>
    <timeCycle>R3/PT10H/${EndDate}</timeCycle>
</timerEventDefinition>

如果同时使用了两种指定方式,则系统会使用属性方式定义的endDate。

目前只有BoundaryTimerEventsCatchTimerEvent支持EndDate功能。

另外,也可以使用cron表达式指定定时周期。下面的例子展示了一个整点启动,每5分钟触发的触发器:

0 0/5 * * * ?

请参考这个教程了解如何使用cron表达式。

请注意: 与普通的Unix cron不同,第一个符号代表的是秒。

重复时间周期更适用于使用相对时间,也就是从某个特定时间点开始计算(比如用户任务开始的时间)。而cron表达式可以使用绝对时间,因此绝对适合用于定时启动事件 timer start events

可以在定时事件定义中使用表达式,也就是使用流程变量调整定时器定义。这个流程变量必须是包含合适时间格式的字符串,ISO 8601(或者对于循环类型,cron)。

<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
  <timerEventDefinition>
    <timeDuration>${duration}</timeDuration>
  </timerEventDefinition>
</boundaryEvent>

请注意:定时器只有在作业或者异步执行器启用时才能触发(也就是说,需要在activiti.cfg.xml中,将jobExecutorActivate或者asyncExecutorActivate设置为true。因为默认情况下,作业与异步执行器都是禁用的)。

8.2.3. 错误事件定义 Error Event Definitions

重要提示: BPMN错误与Java异常是一回事。事实上,这两者毫无共同点。BPMN错误事件是建模业务异常(business exceptions)的方式。而Java异常使用它们自己的方式处理。

<endEvent id="myErrorEndEvent">
  <errorEventDefinition errorRef="myError" />
</endEvent>

8.2.4. 信号事件定义 Signal Event Definitions

信号事件,是引用具名信号的事件。信号是全局范围(广播)的事件,并会被传递给所有激活的处理器(等待中的流程实例/捕获信号事件 catching signal events)。

信号事件定义使用signalEventDefinition元素声明。其signalRef属性引用一个signal元素,该signal元素需要声明为definitions根元素的子元素。下面摘录一个流程,使用中间事件(intermediate event)抛出与捕获信号事件。

<definitions... >
	<!-- 声明信号 -->
	<signal id="alertSignal" name="alert" />

	<process id="catchSignal">
		<intermediateThrowEvent id="throwSignalEvent" name="Alert">
			<!-- 信号事件定义 -->
			<signalEventDefinition signalRef="alertSignal" />
		</intermediateThrowEvent>
		...
		<intermediateCatchEvent id="catchSignalEvent" name="On Alert">
			<!-- 信号事件定义 -->
			<signalEventDefinition signalRef="alertSignal" />
		</intermediateCatchEvent>
		...
	</process>
</definitions>

两个signalEventDefinition引用同一个signal元素。

抛出信号事件 Throwing a Signal Event

信号可以由流程实例使用BPMN结构抛出,也可以通过编程方式使用Java API抛出。下面org.activiti.engine.RuntimeService中的方法可以用于编程方式抛出信号:

RuntimeService.signalEventReceived(String signalName);
RuntimeService.signalEventReceived(String signalName, String executionId);

signalEventReceived(String signalName);signalEventReceived(String signalName, String executionId);的区别,是前者在全局范围,为所有已订阅处理器抛出信号(广播),而后者只为指定的执行传递信号。

捕获信号事件 Catching a Signal Event

信号事件可用信号捕获中间事件(intermediate catch signal event)或者信号边界事件(signal boundary event)捕获。

查询信号事件订阅 Querying for Signal Event subscriptions

可以查询订阅了某一信号事件的所有执行:

 List<Execution> executions = runtimeService.createExecutionQuery()
      .signalEventSubscriptionName("alert")
      .list();

可以使用signalEventReceived(String signalName, String executionId)方法为这些执行传递这个信号。

信号事件范围 Signal event scope

默认情况下,信号事件在流程引擎全局广播。这意味着你可以在一个流程实例中抛出一个信号事件,而不同流程定义的不同流程实例都会响应这个事件。

然而,有时也会希望只在同一个流程实例中响应信号事件。例如在流程实例中使用异步机制,而两个或多个活动彼此互斥的时候。

要限制信号事件的范围(scope),在信号事件定义中添加(非BPMN 2.0标准!)scope属性

<signal id="alertSignal" name="alert" activiti:scope="processInstance"/>

这个属性的默认值为"global(全局)"

信号事件示例 Signal Event example(s)

下面是一个关于两个不同的流程通过信号通信的例子。第一个流程在保险政策更新或变更时启动。在变更由人工审核之后,会抛出信号事件,指出政策已经发生了变更:

bpmn.signal.event.throw

这个事件可以被所有感兴趣的流程实例捕获。下面是一个订阅这个事件的流程的例子。

bpmn.signal.event.catch

请注意:要理解信号事件会广播给所有激活的处理器,这很重要。这意味着在上面的例子中,所有捕获这个信号的流程实例,都会接收这个信号。在这个例子中这就是我们期望的。然而,有的情况下,不希望使用广播方式。考虑下面的流程:

bpmn.signal.event.warning.1

Activiti不支持上面流程中描述的模式。理想情况是,在执行"do something"任务时抛出的错误,由错误边界事件捕获,并通过信号抛出事件传播至执行的并行分支,最终中断"do something in parallel"任务。到目前为止Activiti会按照预期效果执行。然而,由于信号的广播效应,它也会被传播至所有其他订阅了这个信号事件的流程实例。这可能并我们希望的效果。

请注意:信号事件与特定的流程实例无关,而是会广播给所有流程实例。如果你需要只为某一特定的流程实例传递信号,则需要使用signalEventReceived(String signalName, String executionId)手动建立关联,并使用适当的的查询机制 query mechanisms

8.2.5. 消息事件定义 Message Event Definitions

消息事件,是指引用具名消息的事件。消息具有名字与载荷。与信号不同,消息事件只有一个接收者。

消息事件定义使用messageEventDefinition元素声明。其messageRef属性引用一个message元素,该message元素需要声明为definitions根元素的子元素。下面摘录一个流程,声明了两个消息事件,并由开始事件与消息捕获中间事件(intermediate catching message event)引用。

<definitions id="definitions"
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="Examples"
  xmlns:tns="Examples">

  <message id="newInvoice" name="newInvoiceMessage" />
  <message id="payment" name="paymentMessage" />

  <process id="invoiceProcess">

    <startEvent id="messageStart" >
    	<messageEventDefinition messageRef="newInvoice" />
    </startEvent>
    ...
    <intermediateCatchEvent id="paymentEvt" >
    	<messageEventDefinition messageRef="payment" />
    </intermediateCatchEvent>
    ...
  </process>

</definitions>
抛出消息事件 Throwing a Message Event

作为可嵌入的流程引擎,Activiti不关心实际接收消息。因为这可能与环境相关,并需要进行平台定义的操作,例如连接至JMS(Java Messaging Service,Java消息服务)队列(Queue)/主题(Topic),或者处理Webservice或者REST请求。因此接收消息需要作为应用的一部分,或者是流程引擎所嵌入的基础框架中的一部分,由你自行实现。

在应用中接收到消息后,需要决定如何处理它。如果这个消息需要启动新的流程实例,可以选择下面由runtime服务提供的方法中的一种:

ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object> processVariables);

这些方法可以使用引用的消息启动流程实例。

如果需要由已有的流程实例接收消息,需要首先将消息与特定的流程实例关联(查看后续章节),然后触发等待中的执行,让其继续。runtime服务提供了下列方法,根据消息事件的订阅,触发执行:

void messageEventReceived(String messageName, String executionId);
void messageEventReceived(String messageName, String executionId, HashMap<String, Object> processVariables);
查询消息事件订阅 Querying for Message Event subscriptions
  • 对于消息启动事件,消息事件的订阅与特定的流程定义相关。这种类型的消息订阅,可以使用ProcessDefinitionQuery查询:

ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
      .messageEventSubscription("newCallCenterBooking")
      .singleResult();

因为对于一个消息,只能有一个流程定义订阅,因此这个查询总是返回0或1个结果。如果流程定义更新了,只有该流程定义的最新版本会订阅这个消息事件。

  • 对于消息捕获中间事件(intermediate catch message event),消息事件的订阅与特定的执行相关。这种类型的消息订阅,可以使用ExecutionQuery查询:

Execution execution = runtimeService.createExecutionQuery()
      .messageEventSubscriptionName("paymentReceived")
      .variableValueEquals("orderId", message.getOrderId())
      .singleResult();

这种查询通常有关联查询,并且通常需要了解流程(在这个例子里,对于给定的orderId,至多只有一个流程实例)。

消息事件示例 Message Event example(s)

下面是一个流程的例子,可以使用两种不同的消息启动:

bpmn.start.message.event.example.1

在流程需要通过不同的方式响应不同的启动事件,但是后续使用统一的方式处理时,这就很有用。

8.2.6. 启动事件 Start Events

启动事件指明了流程的起点。启动事件的类型(流程在消息到达时启动,在指定的时间间隔后启动,等等),定义了流程如何启动,并显示为启动事件中的小图标。在XML中,类型由子元素声明来定义。

启动事件“随时捕获”:概念上,事件(随时)等候,直到特定的触发器被触发。

在启动事件中,可以使用下列Activiti专用参数:

  • initiator: 指明保存认证用户(authenticated user)id用的变量名。在流程启动时,该id会使用这个变量名被保存。例如:

<startEvent id="request" activiti:initiator="initiator" />

认证用户必须通过IdentityService.setAuthenticatedUserId(String)方法,在try-finally块中设置,像这样:

try {
  identityService.setAuthenticatedUserId("bono");
  runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
  identityService.setAuthenticatedUserId(null);
}

这段代码在集成在Activiti Explorer应用中。因此可以与表单一起使用。

8.2.7. 空启动事件 None Start Event

描述 Description

“空”启动事件,技术上指的是没有特别指定启动流程实例的触发器。这意味着引擎无法预知何时启动流程实例。空启动事件用于流程实例通过调用下列startProcessInstanceByXXX API方法启动的情况。

ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();

请注意:子流程(subprocess)总是有空启动事件。

图示 Graphical notation

空启动事件用空心圆圈表示,中间没有图标(也就是说没有触发器)。

bpmn.none.start.event
XML表示 XML representation

空启动事件的XML表示格式,就是普通的启动事件声明,而没有任何子元素(其他种类的启动事件都有子元素,用于声明其类型)。

<startEvent id="start" name="my start event" />
空启动事件的自定义扩展 Custom extensions for the none start event

formKey: 引用表单模板,用户需要在启动新流程实例时填写该表单。可以在表单章节找到更多信息。例如:

<startEvent id="request" activiti:formKey="org/activiti/examples/taskforms/request.form" />

8.2.8. 定时器启动事件 Timer Start Event

描述 Description

定时器启动事件,用于在指定时间创建流程实例。在流程只需要启动一次,或者流程需要在特定的时间间隔重复启动时,都可以使用。

请注意:子流程不能有定时器启动事件。

请注意:定时器启动事件,在流程部署的同时就开始计时。不需要调用startProcessInstanceByXXX,尽管也不禁止使用启动流程的方法。调用startProcessInstanceByXXX时也会启动流程。

请注意:当部署带有定时器启动事件的流程的新版本时,上一版本的定时器作业会被移除。这是因为通常并不希望旧版本的流程仍然自动启动新的流程实例。

图示 Graphical notation

定时器启动事件,用其中有一个钟表图标的圆圈来表示。

bpmn.clock.start.event
XML表示 XML representation

定时器启动事件的XML表示格式,是普通的启动事件声明,加上定时器定义子元素。请参考定时器定义了解详细配置方法。

示例:流程会启动4次,间隔5分钟,从2011年3月11日,12:13开始

<startEvent id="theStart">
  <timerEventDefinition>
    <timeCycle>R4/2011-03-11T12:13/PT5M</timeCycle>
</timerEventDefinition>
</startEvent>

示例:流程会在选定的时间启动一次

<startEvent id="theStart">
  <timerEventDefinition>
    <timeDate>2011-03-11T12:13:14</timeDate>
  </timerEventDefinition>
</startEvent>

8.2.9. 消息启动事件 Message Start Event

描述 Description

消息启动事件,使用具名消息启动流程实例。它让我们可以使用消息名,有效地在一组可选的启动事件中选择正确的启动事件。

部署具有一个或多个消息启动事件的流程定义时,会考虑下列因素:

  • 消息启动事件的名字,在给定流程定义中,必须是唯一的。一个流程定义不得包含多个同名的消息启动事件。如果流程定义中有两个或多个消息启动事件引用同一个消息,也即两个或多个消息启动事件引用了具有相同消息名字的消息,则Activiti在部署这个流程定义时,会抛出异常。

  • 消息启动事件的名字,在所有已部署的流程定义中,必须是唯一的。如果流程定义中,一个或多个消息启动事件,引用了已经部署的另一流程定义中消息启动事件的消息名,则Activiti在部署这个流程定义时,会抛出异常。

  • 流程版本:在部署流程定义的新版本时,会取消上一版本的消息订阅。即使新版本中并没有这个消息事件,仍然如此(取消上版本的消息订阅)。

启动流程实例时,可以使用下列RuntimeService中的方法,触发消息启动事件:

ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object< processVariables);

messageName是由message元素的name属性决定的名字。message元素被messageEventDefinitionmessageRef属性引用。当启动流程实例时,请考虑下列因素:

  • 只有顶层流程(top-level process)才支持消息启动事件。嵌入式子流程不支持消息启动事件。

  • 如果一个流程定义中有多个消息启动事件,runtimeService.startProcessInstanceByMessage(…​)允许选择合适的启动事件。

  • 如果一个流程定义中有多个消息启动事件,与一个空启动事件,则runtimeService.startProcessInstanceByKey(…​)runtimeService.startProcessInstanceById(…​)会使用空启动事件启动流程实例。

  • 如果一个流程定义中有多个消息启动事件,而没有空启动事件,则runtimeService.startProcessInstanceByKey(…​)runtimeService.startProcessInstanceById(…​)会抛出异常。

  • 如果一个流程定义中只有一个消息启动事件,则runtimeService.startProcessInstanceByKey(…​)runtimeService.startProcessInstanceById(…​)会使用这个消息启动事件启动新流程实例。

  • 如果流程由调用活动(call activity)启动,则消息启动事件只有在下列情况下才被支持

    • 除了消息启动事件,流程还有唯一的空启动事件

    • 或者流程只有唯一的消息启动事件,而没有其他启动事件。

图示 Graphical notation

消息启动事件,用其中有一个消息事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。

bpmn.start.message.event
XML表示 XML representation

消息启动事件的XML表示格式,为普通启动事件声明,加上messageEventDefinition子元素:

<definitions id="definitions"
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="Examples"
  xmlns:tns="Examples">

  <message id="newInvoice" name="newInvoiceMessage" />

  <process id="invoiceProcess">

    <startEvent id="messageStart" >
    	<messageEventDefinition messageRef="tns:newInvoice" />
    </startEvent>
    ...
  </process>

</definitions>

8.2.10. 信号启动事件 Signal Start Event

描述 Description

信号启动事件,使用具名信号启动流程实例。这个信号可以由流程实例中的信号抛出中间事件(intermediary signal throw event),或者API(runtimeService.signalEventReceivedXXX方法)触发。这些情况下,所有拥有相同名字信号启动事件的流程定义都会被启动。

请注意这些情况下,都可以选择异步还是同步启动流程实例。

需要为API传递的signalName,是由signal元素的name属性决定的名字。signal元素被signalEventDefinitionsignalRef属性所引用。

图示 Graphical notation

信号启动事件,用其中有一个信号事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。

bpmn.start.signal.event
XML表示 XML representation

信号启动事件的XML表示格式,为普通启动事件声明,加上signalEventDefinition子元素:

<signal id="theSignal" name="The Signal" />

<process id="processWithSignalStart1">
  <startEvent id="theStart">
    <signalEventDefinition id="theSignalEventDefinition" signalRef="theSignal"  />
  </startEvent>
  <sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
  <userTask id="theTask" name="Task in process A" />
  <sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
	  <endEvent id="theEnd" />
</process>

8.2.11. 错误启动事件 Error Start Event

描述 Description

错误启动事件,可用于触发事件子流程(Event Sub-Process)。错误启动事件不能用于启动流程实例

错误启动事件总是中断。

图示 Graphical notation

错误启动事件,用其中有一个错误事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。

bpmn.start.error.event
XML表示 XML representation

错误启动事件的XML表示格式,为普通启动事件声明,加上errorEventDefinition子元素:

<startEvent id="messageStart" >
	<errorEventDefinition errorRef="someError" />
</startEvent>

8.2.12. 结束事件 End Events

结束事件标志着(子)流程的(分支的)结束。结束事件总是抛出(型)事件。这意味着当流程执行到达结束事件时,会抛出一个结果。结果的类型由事件内部的黑色图标描绘。在XML表示中,类型由子元素声明给出。

8.2.13. 空结束事件 None End Event

描述 Description

“空”结束事件,意味着当到达这个事件时,抛出的结果没有特别指定。因此,引擎除了结束当前执行分支之外,不会多做任何事情。

图示 Graphical notation

空结束事件,用其中没有图标(没有结果类型)的粗圆圈表示。

bpmn.none.end.event
XML表示 XML representation

空事件的XML表示格式,为普通结束事件声明,没有任何子元素(其它种类的结束事件都有子元素,用于声明其类型)。

<endEvent id="end" name="my end event" />

8.2.14. 错误结束事件 Error End Event

描述 Description

当流程执行到达错误结束事件时,结束执行的当前分支,并抛出错误。这个错误可以使用匹配的错误边界中间事件 intermediate boundary error event 捕获。如果找不到匹配的错误边界事件,将会抛出异常。

图示 Graphical notation

错误结束事件事件,用内部有一个错误图标的标准结束事件(粗圆圈)表示。错误图标是全黑的,代表抛出的含义。

bpmn.error.end.event
XML表示 XML representation

错误结束事件,表示为结束事件,加上errorEventDefinition子元素:

<endEvent id="myErrorEndEvent">
  <errorEventDefinition errorRef="myError" />
</endEvent>

errorRef属性可以引用在流程外定义的error元素:

<error id="myError" errorCode="123" />
...
<process id="myProcess">
...

errorerrorCode用于查找匹配的错误捕获边界事件。如果errorRef不匹配任何已定义的error,则该errorRef会用做errorCode的快捷方式。这个快捷方式是Activiti特有的。下面的代码片段在功能上是相同的。

<error id="myError" errorCode="error123" />
...
<process id="myProcess">
...
  <endEvent id="myErrorEndEvent">
    <errorEventDefinition errorRef="myError" />
  </endEvent>
...

与下面的功能相同

<endEvent id="myErrorEndEvent">
  <errorEventDefinition errorRef="error123" />
</endEvent>

请注意errorRef必须遵从BPMN 2.0概要(schema),且必须是合法的QName。

8.2.15. 终止结束事件 Terminate End Event

描述 Description

当到达终止结束事件时,当前的流程实例或子流程会被终止。概念上说,当执行到达终止结束事件时,会判断第一个范围 scope(流程或子流程)并终止它。请注意在BPMN 2.0中,子流程可以是嵌入式子流程,调用活动,事件子流程,或事务子流程。有一条通用规则:当存在多实例的调用过程或嵌入式子流程时,只会终止一个实例,其他的实例与流程实例不会受影响。

可以添加一个可选属性terminateAll。当其为true时,无论该终止结束事件在流程定义中的位置,也无论它是否在子流程(甚至是嵌套子流程)中,都会终止(根)流程实例。

图示 Graphical notation

终止结束事件,用内部有一个全黑圆的标准结束事件(粗圆圈)表示。

bpmn.terminate.end.event
XML表示 XML representation

终止结束事件,表示为结束事件,加上terminateEventDefinition子元素。

请注意terminateAll属性是可选的(默认为false)。

<endEvent id="myEndEvent >
  <terminateEventDefinition activiti:terminateAll="true"></terminateEventDefinition>
</endEvent>

8.2.16. 取消结束事件 Cancel End Event

描述 Description

取消结束事件,只能与bpmn事务子流程(bpmn transaction subprocess)一起使用。当到达取消结束事件时,会抛出取消事件,且必须由取消边界事件(cancel boundary event)捕获。之后这个取消边界事件将取消事务,并触发补偿(compensation)。

图示 Graphical notation

取消结束事件,用内部有一个取消图标的标准结束事件(粗圆圈)表示。取消图标是全黑的,代表抛出的含义。

bpmn.cancel.end.event
XML表示 XML representation

取消结束事件,表示为结束事件,加上cancelEventDefinition子元素。

<endEvent id="myCancelEndEvent">
  <cancelEventDefinition />
</endEvent>

8.2.17. 边界事件 Boundary Events

边界事件是捕获(型)事件,依附在活动(activity)上(边界事件永远不会抛出)。这意味着当活动运行时,事件在监听特定类型的触发器。当事件捕获时,活动会被终止,并沿该事件的出口顺序流继续。

所有的边界事件都用相同的方式定义:

<boundaryEvent id="myBoundaryEvent" attachedToRef="theActivity">
      <XXXEventDefinition/>
</boundaryEvent>

边界事件由下列(元素)定义:

  • 唯一标识符(流程范围)

  • 通过attachedToRef属性定义的,对该事件所依附的活动的引用。请注意边界事件,与其所依附的活动,定义在相同级别(也就是说,边界事件并不包含在活动内部)。

  • 定义了边界事件的类型的,XXXEventDefinition形式的XML子元素(例如TimerEventDefinitionErrorEventDefinition,等等)。查阅特定边界事件类型,以了解更多细节。

8.2.18. 定时器边界事件 Timer Boundary Event

描述 Description

定时器边界事件的行为像是跑表与闹钟。当执行到达边界事件所依附的活动时,启动定时器。当定时器触发时(例如在特定事件间隔后),活动会被中断,沿着边界事件继续执行。

图示 Graphical Notation

定时器边界事件,用内部有一个定时器图标的标准边界事件(圆圈)表示。

bpmn.boundary.timer.event
XML表示 XML Representation

定时器边界事件与一般边界事件一样定义。其中类型子元素为timerEventDefinition元素。

<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
  <timerEventDefinition>
    <timeDuration>PT4H</timeDuration>
  </timerEventDefinition>
</boundaryEvent>

请参考定时器事件定义了解定时器配置的细节。

上面的例子在图示中,圆圈画为虚线:

bpmn.non.interrupting.boundary.timer.event

其典型使用场景,是发送额外的升级邮件,但不中断正常的流程流向。

在BPMN 2.0中,中断与非中断定时器事件是不同的。默认为中断。非中断意味着最初的活动不会被中断,而会保留。并会创建额外的执行,用于处理事件的出口转移(outgoing transition)。在XML表示中,cancelActivity属性设置为false。

<boundaryEvent id="escalationTimer" cancelActivity="false" attachedToRef="firstLineSupport"/>

请注意:定时器边界事件只有在作业或异步执行器启用时才能触发(也就是说,需要在activiti.cfg.xml中,将jobExecutorActivate或者asyncExecutorActivate设置为true。因为默认情况下,作业与异步执行器都是禁用的。)

边界事件的已知问题 Known issue with boundary events

所有类型的边界事件,都有一个关于并发的已知问题。不能在边界事件上附加多个出口顺序流(查看问题ACT-47)。这个问题的解决方案,是使用一条出口顺序流,指向并行网关。

bpmn.known.issue.boundary.event

8.2.19. 错误边界事件 Error Boundary Event

描述 Description

在活动边界上的错误捕获中间(事件),或简称错误边界事件,捕获其依附的活动范围内抛出的错误。

嵌入式子流程或者调用活动上定义错误边界事件最有意义,因为子流程会为其中的所有活动创建范围。错误由错误结束事件抛出。这样的错误会逐层向其上级父范围传播,直到找到一个错误边界事件的范围,该范围定义了匹配的错误事件定义。

当错误事件被捕获时,边界事件定义所在的活动会被销毁,同时销毁其中所有的当前执行(例如,并行活动,嵌套子流程,等等)。流程执行沿着边界事件的出口顺序流继续。

图示 Graphical notation

错误边界事件,用内部有一个错误图标的标准中间事件(两层圆圈)表示。错误图标是白色的,代表捕获的含义。

bpmn.boundary.error.event
XML表示 Xml representation

错误边界事件与标准边界事件一样定义:

<boundaryEvent id="catchError" attachedToRef="mySubProcess">
  <errorEventDefinition errorRef="myError"/>
</boundaryEvent>

边界事件中,errorRef引用一个流程元素外定义的错误:

<error id="myError" errorCode="123" />
...
<process id="myProcess">
...

errorCode用于匹配捕获的错误:

  • 如果省略了errorRef,错误边界事件会捕获所有错误事件,无论error的errorCode是什么。

  • 如果提供了errorRef,并且其引用了存在的error,则边界事件只会捕获相同错误代码的错误

  • 如果提供了errorRef,但BPMN 2.0文件中没有定义error,则errorRef会用作errorCode(与错误结束事件类似)。

示例 Example

下面的示例流程展示了如何使用错误结束事件。当'Review profitability (审核盈利能力)'用户任务完成,并指出提供的信息不足时,会抛出错误。当这个错误被子流程边界捕获时,'Review sales lead (审核销售线索)'子流程中的所有运行中活动都会被销毁(即使'Review customer rating 审核客户等级'还没有完成),并创建'Provide additional details (提供更多信息)'用户任务。

bpmn.boundary.error.example

这个流程作为演示配置的示例提供。可以在org.activiti.examples.bpmn.event.error包中找到流程XML与单元测试。

8.2.20. 信号边界事件 Signal Boundary Event

描述 Description

依附在活动边界上的信号捕获中间(事件),或简称信号边界事件,捕获与其信号定义具有相同信号名的信号。

请注意:与其他事件例如错误边界事件不同的是,信号边界事件不只是捕获其所依附范围抛出的信号。信号边界事件为全局范围(广播)的,意味着信号可以从任何地方抛出,甚至是不同的流程实例。

请注意:与其他事件如错误事件不同,信号在被捕获后不会被消耗。如果有两个激活的信号边界事件,捕获相同的信号事件,则两个边界事件都会被触发,哪怕它们不在同一个流程实例里。

图示 Graphical notation

信号边界事件,用内部有一个信号图标的标准中间事件(两层圆圈)表示。信号图标是白色的,代表捕获的含义。

bpmn.boundary.signal.event
XML表示 XML representation

信号边界事件与标准边界事件一样定义:

<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
          <signalEventDefinition signalRef="alertSignal"/>
</boundaryEvent>
示例 Example

查看信号事件定义章节内容。

8.2.21. 消息边界事件 Message Boundary Event

描述 Description

在活动边界上的消息捕获中间(事件),或简称消息边界事件,捕获与其消息定义具有相同消息名的消息。

图示 Graphical notation

消息边界事件,用内部有一个消息图标的标准中间事件(两层圆圈)表示。信号图标是白色的,代表捕获的含义。

bpmn.boundary.message.event

请注意消息边界事件既可以是中断型的(右手边),也可以是非中断型的(左手边)。

XML表示 XML representation

消息边界事件与标准边界事件一样定义:

<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
          <messageEventDefinition messageRef="newCustomerMessage"/>
</boundaryEvent>
示例 Example

查看消息事件定义章节内容。

8.2.22. 取消边界事件 Cancel Boundary Event

描述 Description

依附在事务子流程边界上的取消捕获中间(事件),或简称取消边界事件,在事务取消时触发。当取消边界事件触发时,首先会中断当前范围的所有活动执行。接下来,启动事务范围内所有有效的的补偿边界事件(compensation boundary event)。补偿会同步执行,也就是说在离开事务前,边界事件会等待补偿完成。当补偿完成时,使用取消边界事件的出口顺序流,离开事务子流程。

请注意:一个事务子流程只允许一个取消边界事件。

请注意:如果事务子流程中有嵌套的子流程,只有成功完成的子流程才会触发补偿。

请注意:如果取消边界事件放置在具有多实例特性的事务子流程上,如果一个实例触发了取消,则边界事件将取消所有实例。

图示 Graphical notation

取消边界事件,用内部有一个取消图标的标准中间事件(两层圆圈)表示。取消图标是白色的(未填充),代表捕获的含义。

bpmn.boundary.cancel.event
XML表示 XML representation

取消边界事件与标准边界事件一样定义:

<boundaryEvent id="boundary" attachedToRef="transaction" >
          <cancelEventDefinition />
</boundaryEvent>

因为取消边界事件总是中断型的,因此不需要cancelActivity属性。

8.2.23. 补偿边界事件 Compensation Boundary Event

描述 Description

依附在活动边界上的补偿捕获中间(事件),或简称补偿边界事件,可以为活动附加补偿处理器。

补偿边界事件必须通过直接关联的方式,引用单个的补偿处理器。

补偿边界事件与其它边界事件的活动策略不同。其它边界事件,例如信号边界事件,当其依附的活动启动时激活;当离开该活动时,会被解除,并取消相应的事件订阅。而补偿边界事件不是这样。补偿边界事件在其依附的活动成功完成时激活,同时创建补偿事件的相应订阅。当补偿事件被触发,或者相应的流程实例结束时,才会移除订阅。请考虑下列因素:

  • 当补偿被触发时,补偿边界事件关联的补偿处理器会被调用,次数与其依附的活动成功完成的次数相同。

  • 如果补偿边界事件依附在具有多实例特性的活动上,则会为每一个实例创建补偿事件订阅。

  • 如果补偿边界事件依附在位于循环内部的活动上,则每次该活动执行时,都会创建一个补偿事件订阅。

  • 如果流程实例结束,则取消补偿事件的订阅。

请注意:嵌入式子流程不支持补偿边界事件。

图示 Graphical notation

补偿边界事件,用内部有一个补偿图标的标准中间事件(两层圆圈)表示。补偿图标是白色的(未填充),代表捕获的含义。另外,补偿边界事件使用单向连接关联补偿处理器,如下图所示:

bpmn.boundary.compensation.event
XML表示 XML representation

补偿边界事件与标准边界事件一样定义:

<boundaryEvent id="compensateBookHotelEvt" attachedToRef="bookHotel" >
          <compensateEventDefinition />
</boundaryEvent>

<association associationDirection="One" id="a1"  sourceRef="compensateBookHotelEvt" targetRef="undoBookHotel" />

<serviceTask id="undoBookHotel" isForCompensation="true" activiti:class="..." />

补偿边界事件在活动完成后才激活,因此不支持cancelActivity属性。

8.2.24. 捕获中间事件 Intermediate Catching Events

所有的捕获中间事件都使用相同方式定义:

<intermediateCatchEvent id="myIntermediateCatchEvent" >
      <XXXEventDefinition/>
</intermediateCatchEvent>

捕获中间事件由下列(元素)定义

  • 唯一标识符(流程范围)

  • 定义了捕获中间事件类型的,XXXEventDefinition形式的XML子元素(例如TimerEventDefinition等)。查阅特定中间捕获事件类型,以了解更多细节。

8.2.25. 定时器捕获中间事件 Timer Intermediate Catching Event

描述 Description

定时器捕获中间事件的行为像是跑表。当执行到达捕获事件活动(catching event activity)时,启动定时器;当定时器触发时(例如在一段时间间隔后),沿定时器中间事件的出口顺序流继续执行。

图示 Graphical Notation

定时器中间事件,用内部有定时器图标的中间捕获事件表示。

bpmn.intermediate.timer.event
XML表示 XML Representation

定时器中间事件与捕获中间事件一样定义。指定类型的子元素为timerEventDefinition元素。

<intermediateCatchEvent id="timer">
  <timerEventDefinition>
    <timeDuration>PT5M</timeDuration>
  </timerEventDefinition>
</intermediateCatchEvent>

查看定时器事件定义了解详细配置。

8.2.26. 信号捕获中间事件 Signal Intermediate Catching Event

描述 Description

信号捕获中间事件,捕获与其引用的信号定义具有相同信号名称的信号。

请注意:与其他事件如错误事件不同,信号在被捕获后不会被消耗。如果有两个激活的信号中间事件,捕获相同的信号事件,则两个中间事件都会被触发,哪怕它们不在同一个流程实例里。

图示 Graphical notation

信号捕获中间事件,用内部有信号图标的标准中间事件(两层圆圈)表示。信号图标是白色的(未填充),代表捕获的含义。

bpmn.intermediate.signal.catch.event
XML表示 XML representation

信号中间事件与捕获中间事件一样定义。指定类型的子元素为signalEventDefinition元素。

<intermediateCatchEvent id="signal">
  <signalEventDefinition signalRef="newCustomerSignal" />
</intermediateCatchEvent>
示例 Example

查看信号事件定义章节。

8.2.27. 消息捕获中间事件 Message Intermediate Catching Event

描述 Description

消息捕获中间事件,捕获特定名字的消息。

图示 Graphical notation

消息捕获中间事件,用内部有消息图标的标准中间事件(两层圆圈)表示。消息图标是白色的(未填充),代表捕获的含义。

bpmn.intermediate.message.catch.event
XML表示 XML representation

消息中间事件与捕获中间事件一样定义。指定类型的子元素为messageEventDefinition元素。

<intermediateCatchEvent id="message">
  <messageEventDefinition signalRef="newCustomerMessage" />
</intermediateCatchEvent>
示例 Example

查看消息事件定义章节。

8.2.28. 抛出中间事件 Intermediate Throwing Event

所有的抛出中间事件都使用相同方式定义:

<intermediateThrowEvent id="myIntermediateThrowEvent" >
      <XXXEventDefinition/>
</intermediateThrowEvent>

抛出中间事件由下列(元素)定义

  • 唯一标识符(流程范围)

  • 定义了抛出中间事件类型的,XXXEventDefinition形式的XML子元素(例如signalEventDefinition等)。查阅特定中间抛出事件类型,以了解更多细节。

8.2.29. 空抛出中间事件 Intermediate Throwing None Event

下面的流程图展示了空中间事件的简单例子,其用于指示流程已经到达了某种状态。

bpmn.intermediate.none.event

基本上添加一个执行监听器 execution listener后,空中间事件就可以成为很好的监视某些KPI(Key Performance Indicators 关键绩效指标)的钩子。

<intermediateThrowEvent id="noneEvent">
  <extensionElements>
    <activiti:executionListener class="org.activiti.engine.test.bpmn.event.IntermediateNoneEventTest$MyExecutionListener" event="start" />
  </extensionElements>
</intermediateThrowEvent>

你也可以添加一些自己的代码,将部分事件发送给你的BAM(Business Activity Monitoring 业务活动监控)工具,或者DWH(Data Warehouse 数据仓库)。引擎本身不会在事件中做任何事情,只是从中穿过。

8.2.30. 信号抛出中间事件 Signal Intermediate Throwing Event

描述 Description

信号抛出中间事件,抛出已定义信号的信号事件。

在Activiti中,信号会广播至所有的激活的处理器(也就是说,所有的捕获信号事件)。信号可以同步或异步地发布。

  • 在默认配置中,信号同步地传递。这意味着抛出(信号的)流程实例会等待,直到信号传递至所有的捕获(信号的)流程实例。所有的捕获流程实例也会在与抛出流程实例相同的事务中,也就是说如果收到通知的流程实例中,有一个实例产生了技术错误(抛出异常),则所有相关的实例都会失败。

  • 信号也可以异步地传递。这是由到达抛出信号事件时,激活的是哪一个(发送)处理器来决定的。对于每个激活的处理器,JobExecutor会为其存储并传递一个异步通知消息,asynchronous notification message(作业 Job)。

图示 Graphical notation

消息抛出中间事件,用内部有信号图标的标准中间事件(两层圆圈)表示。信号图标是黑色的(已填充),代表抛出的含义。

bpmn.intermediate.signal.throw.event
XML表示 XML representation

信号中间事件与抛出中间事件一样定义。指定类型的子元素为signalEventDefinition元素。

<intermediateThrowEvent id="signal">
  <signalEventDefinition signalRef="newCustomerSignal" />
</intermediateThrowEvent>

异步信号事件像这样定义:

<intermediateThrowEvent id="signal">
  <signalEventDefinition signalRef="newCustomerSignal" activiti:async="true" />
</intermediateThrowEvent>
示例 Example

查看信号事件定义章节。

8.2.31. 补偿抛出中间事件 Compensation Intermediate Throwing Event

描述 Description

补偿抛出中间事件,可用于触发补偿。

触发补偿:补偿既可以为设计的活动触发,也可以为补偿事件所在的范围触发。补偿由活动所关联的补偿处理器执行。

  • 抛出补偿时,活动关联的补偿处理器执行的次数,与活动成功完成的次数相同。

  • 如果为当前范围抛出了补偿,则当前范围中所有的活动都会被补偿,包括并行分支上的活动。

  • 补偿分层触发:如果将要被补偿的活动是一个子流程,则该子流程中所有的活动都会触发补偿。如果该子流程有嵌套的活动,则会递归地抛出补偿。然而,补偿不会传播至流程的上层:如果子流程中触发了补偿,该补偿不会传播至子流程范围外的活动。BPMN规范指出,补偿为“与子流程在相同级别”的活动触发。

  • 在Activiti中,补偿按照执行的相反顺序运行。这意味着最后完成的活动会第一个补偿,等等。

  • 补偿抛出中间事件,可用于补偿已经成功完成的事务子流程。

请注意:如果抛出补偿的范围中有一个子流程,而该子流程包含有关联了补偿处理器的活动,则当抛出补偿时,只有当该子流程成功完成的情况,补偿才会传播至该子流程。如果子流程内嵌套的部分活动已经完成,并附加了补偿处理器,则如果包含这些活动的子流程还没有完成,这些补偿处理器不会执行。参考下面的例子:

bpmn.throw.compensation.example1

在这个流程中,有两个并行的执行。一个执行嵌入子流程,另一个执行“charge credit card(信用卡付款)”活动。假定两个执行都已开始,且第一个并行执行正等待用户完成“review bookings(检查预定)”任务。第二个执行进行了“charge credit card(信用卡付款)”活动的操作,抛出了一个错误,导致“cancel reservations(取消预订)”事件触发补偿。这时并行子流程还未完成,意味着补偿不会传播至该子流程,因此不会执行“cancel hotel reservation(取消酒店预订)”补偿处理器。而如果“cancel reservations(取消预订)”运行前,这个用户任务(因此该嵌入式子流程也)已经完成,则补偿会传播至该嵌入式子流程。

流程变量:当补偿嵌入式子流程时,用于执行补偿处理器的执行,可以以变量在子流程完成时所处的状态,访问子流程的局部流程变量。围了实现这一点,会为范围执行(为执行子流程所创建的执行)所关联的流程变量,进行快照。意味着:

  • 子流程范围内创建的并行执行所添加的变量,补偿执行器无法访问。

  • 上层的执行关联的流程变量(例如流程实例的执行关联的流程变量),不在该快照中:补偿处理器(本就)可以以其在抛出补偿时所处的状态,访问这些流程变量。

  • 只会为嵌入式子流程,而不会为其他活动,进行变量快照。

目前的限制:

  • 目前不支持waitForCompletion="false"。当补偿抛出中间事件触发补偿时,只有在补偿成功完成时,才会离开该事件。

  • 补偿现在由并行执行来运行。该并行执行按照补偿活动完成的逆序启动。Activiti的后续版本可能会添加选项,使补偿可以按(活动完成的)顺序运行。

  • 补偿不会传播至调用活动(call activity)生成的子流程。

图示 Graphical notation

补偿抛出中间事件,用内部有补偿图标的标准中间事件(两层圆圈)表示。补偿图标是黑色的(已填充),代表抛出的含义。

bpmn.intermediate.compensation.throw.event
Xml representation

补偿中间事件与抛出中间事件一样定义。指定类型的子元素为compensateEventDefinition元素。

<intermediateThrowEvent id="throwCompensation">
	<compensateEventDefinition />
</intermediateThrowEvent>

另外,activityRef可选项可用于为指定的范围/活动触发补偿:

<intermediateThrowEvent id="throwCompensation">
	<compensateEventDefinition activityRef="bookHotel" />
</intermediateThrowEvent>

8.3. 顺序流 Sequence Flow

8.3.1. 描述 Description

顺序流是流程中两个元素间的连接器。当流程执行中,一个元素被访问后,会沿着所有的出口顺序流继续。这意味着BPMN 2.0的默认性质是并行的:两个出口顺序流,会创建两个独立的,并行的执行路径。

8.3.2. 图示 Graphical notation

顺序流,用从源元素指向目标元素的箭头表示。箭头总是指向目标元素。

bpmn.sequence.flow

8.3.3. XML表示 XML representation

顺序流需要有流程唯一的id,以及对存在的目标元素的引用。

<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />

8.3.4. 条件顺序流 Conditional sequence flow

描述 Description

在顺序流上可以定义条件。当离开BPMN 2.0活动时,默认行为是计算其出口顺序流上的条件。当条件计算为true时,选择该出口顺序流。如果该方法选择了多条顺序流,则会生成多个执行,流程会以并行方式继续。

请注意:上面的介绍对BPMN 2.0活动(与事件)有效,但不适用于网关(gateway)。不同类型的网关,会用不同的方式处理带有条件的顺序流。

图示 Graphical notation

条件顺序流,用起点带有小菱形的一般顺序流表示。条件表达式挨着顺序流显示。

bpmn.conditional.sequence.flow
XML表示 XML representation

条件顺序流的XML表示格式,为含有conditionExpression(条件表达式)子元素的普通顺序流。请注意目前只支持tFormalExpressions。省略xsi:type=""定义会默认为唯一支持的表达式类型。

<sequenceFlow id="flow" sourceRef="theStart" targetRef="theTask">
  <conditionExpression xsi:type="tFormalExpression">
    <![CDATA[${order.price > 100 && order.price < 250}]]>
  </conditionExpression>
</sequenceFlow>

目前conditionalExpressions只能使用UEL,详细信息可以在表达式章节找到。使用的表达式需要能解析为boolean值,否则当计算条件时会抛出异常。

  • 下面的例子,通过典型的JavaBean的方式,使用getter引用流程变量的数据。

<conditionExpression xsi:type="tFormalExpression">
  <![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
  • 这个例子调用了一个解析为boolean值的方法。

<conditionExpression xsi:type="tFormalExpression">
  <![CDATA[${order.isStandardOrder()}]]>
</conditionExpression>

Activiti发行版中包含了下列示例流程,展示值表达式与方法表达式的使用(参见org.activiti.examples.bpmn.expression)。

bpmn.uel expression.on.seq.flow

8.3.5. 默认顺序流 Default sequence flow

描述 Description

所有的BPMN 2.0任务与网关,都可以使用默认顺序流。这种顺序流只有当没有其他顺序流可以选择时,才会被选择为活动的出口顺序流。默认顺序流上的条件会被忽略。

图示 Graphical notation

默认顺序流,用起点带有“斜线”标记的一般顺序流表示。

bpmn.default.sequence.flow
XML表示 XML representation

活动的默认顺序流,由该活动的default属性定义。下面的XML片段展示了一个排他网关(exclusive gateway),带有默认顺序流flow 2。只有当conditionAconditionB都计算为false时,默认顺序流才会被选择为网关的出口顺序流。

<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" default="flow2" />
<sequenceFlow id="flow1" sourceRef="exclusiveGw" targetRef="task1">
  <conditionExpression xsi:type="tFormalExpression">${conditionA}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="task2"/>
<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="task3">
  <conditionExpression xsi:type="tFormalExpression">${conditionB}</conditionExpression>
</sequenceFlow>

对应下面的图示:

(原图缺失)

8.4. 网关 Gateways

网关用于控制执行的流向(或者按BPMN 2.0描述的,执行的token 标志)。网关可以消耗生成标志。

网关用其中带有图标的菱形表示。该图标显示了网关的类型。

bpmn.gateway

8.4.1. 排他网关 Exclusive Gateway

描述 Description

排他网关(也叫异或网关 XOR gateway,或者更专业的,基于数据的排他网关 exclusive data-based gateway),用于为流程中的决策建模。当执行到达这个网关时,所有出口顺序流会按照它们定义的顺序进行计算。条件计算为true的顺序流(当没有设置条件时,认为顺序流定义为true)会被选择用于继续流程。

请注意这里出口顺序流的含义与BPMN 2.0中的一般情况不一样。一般情况下,所有条件计算为true的顺序流,都会被选择继续,并行执行。而使用排他网关时,只会选择一条顺序流。当多条顺序流的条件都计算为true时,其中在XML中定义的第一条(也只有这条)会被选择,用于继续流程。如果没有可选的顺序流,会抛出异常。

图示 Graphical notation

排他网关,用内部带有’X’图标的标准网关(菱形)表示,'X’图标代表异或(XOR)的含义。请注意内部没有图标的网关默认为排他网关。BPMN 2.0规范不允许在同一个流程中,混合使用带有及没有X的菱形标志。

bpmn.exclusive.gateway.notation
XML表示 XML representation

排他网关的XML表示格式很直接:一行定义网关的XML,而条件表达式定义在出口顺序流上。查看条件顺序流章节了解这种表达式的可用选项。

以下面的模型为例:

bpmn.exclusive.gateway

用XML表示如下:

<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" />

<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="theTask1">
  <conditionExpression xsi:type="tFormalExpression">${input == 1}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="theTask2">
  <conditionExpression xsi:type="tFormalExpression">${input == 2}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow4" sourceRef="exclusiveGw" targetRef="theTask3">
  <conditionExpression xsi:type="tFormalExpression">${input == 3}</conditionExpression>
</sequenceFlow>

8.4.2. 并行网关 Parallel Gateway

描述 Description

网关也可以用于对流程中并行的建模。在流程模型中引入并行的最简单的网关,就是并行网关。它可以将执行分支(fork)为多条路径,也可以合并(join)执行的多条入口路径。

并行网关的功能,基于其入口与出口顺序流:

  • 分支:所有的出口顺序流都并行执行,为每一条顺序流创建一个并行执行。

  • 合并:所有到达并行网关的并行执行,都在网关处等待,直到每一条入口顺序流都有一个执行到达。然后流程经过该合并网关继续。

请注意,如果并行网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有入口顺序流,然后分裂为多条并行执行路径。

与其他网关类型的重要区别,是并行网关不计算条件。如果连接到并行网关的顺序流上定义了条件,条件会被简单地忽略。

图示 Graphical Notation

并行网关,用内部带有’加号’图标的网关(菱形)表示,代表与(AND)的含义。

bpmn.parallel.gateway
XML表示 XML representation

定义并行网关需要一行XML:

<parallelGateway id="myParallelGateway" />

实际行为(分支,合并或两者皆有),由连接到该并行网关的顺序流定义。

例如,上面的模型表现为下面的XML:

<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />

<parallelGateway id="fork" />
<sequenceFlow sourceRef="fork" targetRef="receivePayment" />
<sequenceFlow sourceRef="fork" targetRef="shipOrder" />

<userTask id="receivePayment" name="Receive Payment" />
<sequenceFlow sourceRef="receivePayment" targetRef="join" />

<userTask id="shipOrder" name="Ship Order" />
<sequenceFlow sourceRef="shipOrder" targetRef="join" />

<parallelGateway id="join" />
<sequenceFlow sourceRef="join" targetRef="archiveOrder" />

<userTask id="archiveOrder" name="Archive Order" />
<sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />

<endEvent id="theEnd" />

在上面的例子中,当流程启动后,会创建两个任务:

ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
                         .processInstanceId(pi.getId())
                         .orderByTaskName()
                         .asc();

List<Task> tasks = query.list();
assertEquals(2, tasks.size());

Task task1 = tasks.get(0);
assertEquals("Receive Payment", task1.getName());
Task task2 = tasks.get(1);
assertEquals("Ship Order", task2.getName());

当这两个任务完成后,第二个并行网关会合并这两个执行,并且由于只有一条出口顺序流,不会再创建并行执行路径,只会激活Archive Order(存档订单)任务。

请注意并行网关不需要“平衡”(也就是说,对应的并行网关,其入口/出口顺序流的数量不需要匹配)。并行网关会简单地等待所有入口顺序流,并为每一条出口顺序流创建并行执行,不受流程模型中的其他结构影响。因此,下面的流程在BPMN 2.0中是合法的:

bpmn.unbalanced.parallel.gateway

8.4.3. 包容网关 Inclusive Gateway

描述 Description

包容网关可被视作排他网关与并行网关的组合。与排他网关一样,可以在出口顺序流上定义条件,包容网关会计算它们。然而主要的区别是,包容网关与并行网关一样,可以选择多于一条(出口)顺序流。

包容网关的功能,基于其入口与出口顺序流:

  • 分支:所有出口顺序流的条件都会被计算,对于条件计算为true的顺序流,流程会并行地沿其继续,为每一条顺序流创建一个并行执行。

  • 合并:所有到达包容网关的并行执行,都会在网关处等待,直到每一条具有流程标志的入口顺序流,都有一个执行到达。这是与并行网关的重要区别。换句话说,包容网关只会等待将会被执行的入口顺序流。在合并后,流程穿过合并并行网关继续。

请注意,如果包容网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有具有流程标志的入口顺序流,然后为条件计算为true的出口顺序流,分裂为多条并行执行路径。

图示 Graphical Notation

包容网关,用内部带有’圆圈’图标的网关(菱形)表示。

bpmn.inclusive.gateway
XML表示 XML representation

定义包容网关需要一行XML:

<inclusiveGateway id="myInclusiveGateway" />

实际行为(分支,合并或两者皆有),由连接到该包容网关的顺序流定义。

例如,上面的模型表现为下面的XML:

<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />

<inclusiveGateway id="fork" />
<sequenceFlow sourceRef="fork" targetRef="receivePayment" >
  <conditionExpression xsi:type="tFormalExpression">${paymentReceived == false}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="fork" targetRef="shipOrder" >
  <conditionExpression xsi:type="tFormalExpression">${shipOrder == true}</conditionExpression>
</sequenceFlow>

<userTask id="receivePayment" name="Receive Payment" />
<sequenceFlow sourceRef="receivePayment" targetRef="join" />

<userTask id="shipOrder" name="Ship Order" />
<sequenceFlow sourceRef="shipOrder" targetRef="join" />

<inclusiveGateway id="join" />
<sequenceFlow sourceRef="join" targetRef="archiveOrder" />

<userTask id="archiveOrder" name="Archive Order" />
<sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />

<endEvent id="theEnd" />

在上面的例子中,当流程启动后,如果流程变量paymentReceived == false且shipOrder == true,将会创建两个任务。如果只有一个流程变量等于true,则只会创建一个任务。如果没有条件计算为true,会抛出异常,并可通过指定默出口顺序流避免。在下面的例子中,只有ship order(传递订单)一个任务会被创建:

HashMap<String, Object> variableMap = new HashMap<String, Object>();
          variableMap.put("receivedPayment", true);
          variableMap.put("shipOrder", true);
          ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
                         .processInstanceId(pi.getId())
                         .orderByTaskName()
                         .asc();

List<Task> tasks = query.list();
assertEquals(1, tasks.size());

Task task = tasks.get(0);
assertEquals("Ship Order", task.getName());

当这个任务完成后,第二个包容网关会合并这两个执行,并且由于只有一条出口顺序流,不会再创建并行执行路径,只会激活Archive Order(存档订单)任务。

请注意包容网关不需要“平衡”(也就是说,对应的包容网关,其入口/出口顺序流的数量不需要匹配)。包容网关会简单地等待所有入口顺序流,并为每一条出口顺序流创建并行执行,不受流程模型中的其他结构影响。

8.4.4. 基于事件的网关 Event-based Gateway

描述 Description

基于事件的网关,允许基于事件做选择。网关的每一条出口顺序流,都需要连接至一个捕获中间事件。当流程执行到达基于事件的网关时,网关类似等待状态地动作:执行被暂停。并且,为每一条出口顺序流,创建一个事件订阅。

请注意基于事件的网关,其出口顺序流与一般的顺序流不同。这些顺序流从不实际被执行。相反,它们允许流程引擎决定,当执行到达一个基于事件的网关时,需要订阅什么事件。基于下列约束:

  • 一个基于事件的网关,必须有两条或更多的出口顺序流。

  • 基于事件的网关,只能连接至intermediateCatchEvent(捕获中间事件)类型的元素(Activiti不支持基于事件的网关后,连接接收任务,Receive Task)。

  • 连接至基于事件的网关的intermediateCatchEvent,必须只有一个入口顺序流。

图示 Graphical notation

基于事件的网关,用内部带有特殊图标的网关(菱形)表示。

bpmn.event.based.gateway.notation
XML表示 XML representation

用于定义基于事件的网关的XML元素为eventBasedGateway

示例 Example(s)

下面的流程,是带有基于事件的网关的流程的例子。当执行到达基于事件的网关时,流程执行被暂停。并且,流程实例订阅alert信号事件,并创建一个10分钟后触发的定时器。这使得流程引擎等待10分钟,并等待信号事件。如果信号在10分钟内触发,则定时器会被取消,执行沿着信号继续。如果信号未被触发,执行会在定时器到时后继续,并取消信号订阅。

bpmn.event.based.gateway.example
<definitions id="definitions"
	xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
	xmlns:activiti="http://activiti.org/bpmn"
	targetNamespace="Examples">

	<signal id="alertSignal" name="alert" />

	<process id="catchSignal">

		<startEvent id="start" />

		<sequenceFlow sourceRef="start" targetRef="gw1" />

		<eventBasedGateway id="gw1" />

		<sequenceFlow sourceRef="gw1" targetRef="signalEvent" />
		<sequenceFlow sourceRef="gw1" targetRef="timerEvent" />

		<intermediateCatchEvent id="signalEvent" name="Alert">
			<signalEventDefinition signalRef="alertSignal" />
		</intermediateCatchEvent>

		<intermediateCatchEvent id="timerEvent" name="Alert">
			<timerEventDefinition>
				<timeDuration>PT10M</timeDuration>
			</timerEventDefinition>
		</intermediateCatchEvent>

		<sequenceFlow sourceRef="timerEvent" targetRef="exGw1" />
		<sequenceFlow sourceRef="signalEvent" targetRef="task" />

		<userTask id="task" name="Handle alert"/>

		<exclusiveGateway id="exGw1" />

		<sequenceFlow sourceRef="task" targetRef="exGw1" />
		<sequenceFlow sourceRef="exGw1" targetRef="end" />

		<endEvent id="end" />
</process>
</definitions>

8.5. 任务 Tasks

8.5.1. 用户任务 User Task

描述 Description

“用户任务”用于建模需要人工执行的任务。当流程执行到达用户任务时,会为指派至该任务的用户或组的任务列表创建一个新任务。

图示 Graphical notation

用户任务,用左上角有一个小用户图标的标准任务(圆角矩形)表示。

bpmn.user.task
XML表示 XML representation

用户任务在XML中如下定义。id是必须属性,name是可选属性。

<userTask id="theTask" name="Important task" />

一个用户任务也可以有一个描述(description)。事实上任何BPMN 2.0元素都可以有一个描述。描述使用附加的documentation元素定义。

<userTask id="theTask" name="Schedule meeting" >
  <documentation>
	  Schedule an engineering meeting for next week with the new hire.
  </documentation>

描述文本可以从任务中,使用标准Java方式获取:

task.getDescription()
到期日期 Due Date

每个任务都有一个字段,标志该任务的到期日期。可以使用查询API,查询在给定日期前或后到期的任务。

有一个Activiti的扩展,可以在任务定义中指定表达式,以在任务创建时,设定初始到期日期。该表达式必须解析为java.util.Datejava.util.String (ISO8601格式),ISO8601时间长度(例如PT50M),或者null。例如,可以使用在流程里前一个表单中输入的日期,或者由前一个服务任务计算出的日期。如果使用的是时间长度,则到期日期基于当前时间加上给定长度计算。例如当dueDate使用“PT30M”时,任务在从现在起30分钟后到期。

<userTask id="theTask" name="Important task" activiti:dueDate="${dateVariable}"/>

任务的到期日期,也可以使用TaskService,或者在TaskListener中使用传递的DelegateTask修改。

用户指派 User assignment

一个用户任务可以直接指派给一个用户。可以通过定义humanPerformer子元素实现。这个humanPerformer定义需要resourceAssignmentExpression来实际定义用户。目前,只支持formalExpressions

<process >

  ...

  <userTask id='theTask' name='important task' >
    <humanPerformer>
      <resourceAssignmentExpression>
        <formalExpression>kermit</formalExpression>
      </resourceAssignmentExpression>
    </humanPerformer>
  </userTask>

只有一个用户可被指定为任务的humanPerformer。在Activiti术语中,这个用户被称作办理人(assignee)。拥有办理人的任务,在其他人的任务列表中不可见,而可以在该办理人的个人任务列表中看到。

特定用户办理的任务,可以通过TaskService如下获取:

List<Task> tasks = taskService.createTaskQuery().taskAssignee("kermit").list();

任务也可以放在用户的候选任务列表中。在这个情况下,需要使用potentialOwner(潜在用户)结构。用法与humanPerformer结构类似。请注意需要为表达式中的每一个元素指定其为用户还是组(引擎无法自行判断)。

<process >

  ...

  <userTask id='theTask' name='important task' >
    <potentialOwner>
      <resourceAssignmentExpression>
        <formalExpression>user(kermit), group(management)</formalExpression>
      </resourceAssignmentExpression>
    </potentialOwner>
  </userTask>

定义了potential owner结构的任务,可用如下方法获取(或类似于指派用户任务,使用TaskQuery查询):

 List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit");

将获取所有kermit作为候选用户的任务,也就是说,表达式含有user(kermit)的任务,也将获取所有指派给kermit为其成员的组的任务(例如group(management),如果kermit是这个组的成员,并且使用Activiti身份组件)。组会在运行时解析,并可通过IdentityService(身份服务)管理。

如果并未指定给定字符串是用户还是组,引擎默认其为组。因此下列代码与声明了group(accountancy)一样。

<formalExpression>accountancy</formalExpression>
用于任务指派的Activiti扩展 Activiti extensions for task assignment

很明显,当指派关系不复杂时,这种用户与组的指派方式十分笨重。为避免这种复杂性,可以在用户任务上使用自定义扩展

  • assignee(办理人)属性:这个自定义扩展用于直接将一个用户任务指派至一个给定用户。

<userTask id="theTask" name="my task" activiti:assignee="kermit" />

与使用上面定义的humanPerformer结构完全相同。

  • candidateUsers(候选用户)属性:这个自定义扩展用于为一个任务指定候选用户。

<userTask id="theTask" name="my task" activiti:candidateUsers="kermit, gonzo" />

与使用上面定义的potentialOwner结构完全相同。请注意不需要像在potential owner中一样,使用user(kermit)的声明,因为这个属性只能用于用户。

  • candidateGroups(候选组)attribute:这个自定义扩展用于为一个任务指定候选组。

<userTask id="theTask" name="my task" activiti:candidateGroups="management, accountancy" />

与使用上面定义的potentialOwner结构完全相同。请注意不需要像在potential owner中一样,使用group(management)的声明,因为这个属性只能用于组。

  • candidateUserscandidateGroups可以定义在同一个用户任务上。

请注意:尽管Activiti提供了身份管理组件,通过IdentityService暴露,但并不会检查给定的用户是否在身份组件中存在。这样Activiti在嵌入应用时,可以与已有的身份管理解决方案集成。

自定义身份联系类型(试验特性) Custom identity link types (Experimental)

用户指派中定义过,BPMN标准支持单个指派用户即hunamPerformer,或者一组用户构成potentialOwners潜在用户池。另外,Activiti为用户任务定义了扩展属性元素,代表任务的办理人或者候选用户

Activiti支持的身份联系类型有:

public class IdentityLinkType {
  /* Activiti原生角色 Activiti native roles */
  public static final String ASSIGNEE = "assignee";
  public static final String CANDIDATE = "candidate";
  public static final String OWNER = "owner";
  public static final String STARTER = "starter";
  public static final String PARTICIPANT = "participant";
}

BPMN标准与Activiti示例身份认证是用户。在前一章节提到过,Activiti的身份管理实现并不适用于生产环境,而需要在支持的认证概要下扩展。

如果需要添加额外的联系类型,可按照下列语法,使用自定义资源作为扩展元素:

<userTask id="theTask" name="make profit">
  <extensionElements>
    <activiti:customResource activiti:name="businessAdministrator">
      <resourceAssignmentExpression>
        <formalExpression>user(kermit), group(management)</formalExpression>
      </resourceAssignmentExpression>
    </activiti:customResource>
  </extensionElements>
</userTask>

自定义联系表达式添加至TaskDefinition类:

protected Map<String, Set<Expression>> customUserIdentityLinkExpressions =
      new HashMap<String, Set<Expression>>();
protected Map<String, Set<Expression>> customGroupIdentityLinkExpressions =
      new HashMap<String, Set<Expression>>();

public Map<String,
         Set<Expression>> getCustomUserIdentityLinkExpressions() {
  return customUserIdentityLinkExpressions;
}

public void addCustomUserIdentityLinkExpression(String identityLinkType,
      Set<Expression> idList)
  customUserIdentityLinkExpressions.put(identityLinkType, idList);
}

public Map<String,
       Set<Expression>> getCustomGroupIdentityLinkExpressions() {
  return customGroupIdentityLinkExpressions;
}

public void addCustomGroupIdentityLinkExpression(String identityLinkType,
       Set<Expression> idList) {
  customGroupIdentityLinkExpressions.put(identityLinkType, idList);
}

并将会在运行时,由UserTaskActivityBehavior handleAssignments方法填写。

最后,需要扩展IdentityLinkType类,以支持自定义身份联系类型:

package com.yourco.engine.task;

public class IdentityLinkType
    extends org.activiti.engine.task.IdentityLinkType
{
    public static final String ADMINISTRATOR = "administrator";

    public static final String EXCLUDED_OWNER = "excludedOwner";
}
通过任务监听器自定义指派 Custom Assignment via task listeners

如果上面的方式仍不能满足要求,可以在创建事件(create event)上使用任务监听器,代理自定义指派逻辑:

<userTask id="task1" name="My task" >
  <extensionElements>
    <activiti:taskListener event="create" class="org.activiti.MyAssignmentHandler" />
  </extensionElements>
</userTask>

传递至TaskListener实现的DelegateTask,可用于设置办理人与候选用户/组:

public class MyAssignmentHandler implements TaskListener {

  public void notify(DelegateTask delegateTask) {
    // Execute custom identity lookups here

    // and then for example call following methods:
    delegateTask.setAssignee("kermit");
    delegateTask.addCandidateUser("fozzie");
    delegateTask.addCandidateGroup("management");
    ...
  }

}

当使用Spring时,可以按上面章节的介绍使用自定义指派属性,并代理至使用任务监听器、带有表达式的Spring bean,监听任务创建事件。在下面的例子中,通过调用ldapService Spring bean的findManagerOfEmployee方法,设置办理人。传递的emp参数是一个流程变量。

<userTask id="task" name="My Task" activiti:assignee="${ldapService.findManagerForEmployee(emp)}"/>

也可以用于候选用户与组:

<userTask id="task" name="My Task" activiti:candidateUsers="${ldapService.findAllSales()}"/>

请注意调用方法的返回类型必须是StringCollection<String>(候选用户与组):

public class FakeLdapService {

  public String findManagerForEmployee(String employee) {
    return "Kermit The Frog";
  }

  public List<String> findAllSales() {
    return Arrays.asList("kermit", "gonzo", "fozzie");
  }

}

8.5.2. 脚本任务 Script Task

描述 Description

脚本任务是自动化的活动。当流程执行到达脚本任务时,会执行相应的脚本。

图示 Graphical Notation

脚本任务,用左上角有一个小“脚本”图标的标准BPMN 2.0任务(圆角矩形)表示。

bpmn.scripttask
XML表示 XML representation

脚本任务通过指定scriptscriptFormat定义。

<scriptTask id="theScriptTask" name="Execute script" scriptFormat="groovy">
  <script>
    sum = 0
    for ( i in inputArray ) {
      sum += i
    }
  </script>
</scriptTask>

scriptFormat属性的值,必须是兼容JSR-223(Java平台脚本)的名字。默认情况下,JavaScript包含在每一个JDK中,因此不需要添加任何jar。如果想使用其它(兼容JSR-223的)脚本引擎,需要在classpath中添加相应的jar,并使用适当的名字。例如,Activiti单元测试经常使用Groovy,因为其语法与Java十分相似。

请注意Groovy脚本引擎与groovy-all jar捆绑在一起。在2.0版本以前,脚本引擎是Groovy jar的一部分。因此,现在必须添加如下依赖:

<dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>2.x.x<version>
</dependency>
脚本中的变量 Variables in scripts

到达脚本引擎的执行可以访问的所有流程变量,都可以在脚本中使用。在这个例子里,脚本变量'inputArray'实际上就是一个流程变量(integer数组)。

<script>
    sum = 0
    for ( i in inputArray ) {
      sum += i
    }
</script>

也可以简单地调用execution.setVariable("variableName", variableValue),在脚本中设置流程变量。默认情况下,变量不会自动储存(请注意,在Activiti 5.12以前是这样的!)。可以将scriptTaskautoStoreVariables参数设置为true,以自动保存任何在脚本中定义的变量(例如上例中的sum)。然而,最佳实践不是这么做,而是直接调用execution.setVariable(),因为在JDK近期的一些版本中,某些脚本语言不能自动保存变量。查看这个链接了解更多信息。

<scriptTask id="script" scriptFormat="JavaScript" activiti:autoStoreVariables="false">

这个参数的默认值为false,意味着这个参数将在脚本任务定义中忽略,所有声明的变量将只在脚本执行期间有效。

在脚本中设置变量的例子:

<script>
    def scriptVar = "test123"
    execution.setVariable("myVar", scriptVar)
</script>

请注意:下列名字被保留,不能用于变量名:out,out:print,lang:import,context,elcontext

脚本结果 Script results

脚本任务的返回值,可以通过为脚本任务定义的'activiti:resultVariable'属性设置流程变量名,指定为已经存在的,或者新的流程变量。指定的已有值的流程变量,会被脚本执行的结果值覆盖。当不指定结果变量名时,脚本结果值将被忽略。

<scriptTask id="theScriptTask" name="Execute script" scriptFormat="juel" activiti:resultVariable="myVar">
  <script>#{echo}</script>
</scriptTask>

在上面的例子中,脚本执行的结果(解析表达式'#{echo}'的值),将在脚本完成后,设置为名为'myVar'的流程变量。

安全性 Security

当使用javascript作为脚本语言时,可以使用“安全脚本(secure scripting)”。参见安全脚本章节

8.5.3. Java服务任务 Java Service Task

描述 Description

Java服务任务用于执行外部的Java类。

图示 Graphical Notation

服务任务,用左上角有一个小齿轮图标的圆角矩形表示。

bpmn.java.service.task
XML表示 XML representation

有四种方法声明如何调用Java逻辑:

  • 指定实现了JavaDelegate或ActivityBehavior的类

  • 对解析为代理对象的表达式求值

  • 调用方法表达式

  • 对值表达式求值

要指定流程执行时调用的类,需要使用activiti:class属性提供全限定类名(fully qualified classname)。

<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:class="org.activiti.MyJavaDelegate" />

查看实现章节,了解关于如何使用这种类的更多信息。

也可以使用解析为对象的表达式。该对象必须遵循的规则,与使用activiti:class创建的对象规则相同(查看更多)。

<serviceTask id="serviceTask" activiti:delegateExpression="${delegateExpressionBean}" />

这里,delegateExpressionBean是一个实现了JavaDelegate接口的bean,在Spring容器中定义。

要指定需要计算的UEL方法表达式,使用activiti:expression属性。

<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:expression="#{printer.printMessage()}" />

将在名为printer的对象上调用printMessage方法(不带参数)。

也可以为表达式中使用的方法传递变量。

<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:expression="#{printer.printMessage(execution, myVar)}" />

将在名为printer的对象上调用printMessage方法。传递的第一个参数为DelegateExecution,名为execution,在表达式上下文中默认可用。传递的第二个参数,是当前执行中,名为myVar变量的值。

可以使用activiti:expression属性指定需要计算的UEL值表达式。

<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:expression="#{split.ready}" />

会调用名为split的bean的ready参数的getter方法,getReady(不带参数)。该对象会被解析为执行的流程变量或(如果可用的话)Spring上下文中的bean。

实现 Implementation

要实现可以在流程执行中调用的类,需要实现org.activiti.engine.delegate.JavaDelegate接口,并在execute方法中提供所需逻辑。当流程执行到达该活动时,会执行方法中定义的逻辑,并按照BPMN 2.0的默认方法离开活动。

让我们创建一个Java类的示例,可用于将流程变量String改为大写。这个类需要实现org.activiti.engine.delegate.JavaDelegate接口,因此需要实现execute(DelegateExecution)方法。这个方法就是引擎将调用的方法,需要实现业务逻辑。可以通过DelegateExecution接口(点击链接获取该接口操作的详细Javadoc)访问流程实例信息,如流程变量等。

public class ToUppercase implements JavaDelegate {

  public void execute(DelegateExecution execution) throws Exception {
    String var = (String) execution.getVariable("input");
    var = var.toUpperCase();
    execution.setVariable("input", var);
  }

}

请注意:只会为serviceTask上定义的Java类创建一个实例。所有流程实例共享同一个类实例,用于调用execute(DelegateExecution)。这意味着该类不能有任何成员变量,并需要是线程安全的,因为它可能会在不同线程中同时执行。这也影响了字段注入的使用方法。(译者注:原文可能较老,不正确。5.21中,activiti:class指定的类,会在流程实例启动时,为每个活动,分别进行实例化。不过,当该活动在流程中重复执行,或者为多实例时,使用的都会是同一个类实例。)

在流程定义中(如通过activiti:class)引用的类,不会在部署时实例化。只有当流程执行第一次到达该类使用的地方时,才会创建该类的实例。如果找不到这个类,会抛出ActivitiException。这是因为部署时的环境(更准确的说classpath),与实际运行的环境经常不一样。例如当使用ant或者Activiti Explorer中业务存档上传的方式部署的流程,其classpath中并没有流程引用的类。

[内部:非公有实现类]也可以使用实现了org.activiti.engine.impl.pvm.delegate.ActivityBehavior接口的类。该实现可以访问更强大的ActivityExecution,可以例如影响流程的控制流程。请注意这并不是很好的实践,需要避免这么使用。因此,建议只有在高级使用场景下,并且你确知在做什么的时候,才使用ActivityBehavior接口。

字段注入 Field Injection

可以为代理类的字段注入值。支持下列注入方式:

  • 字符串常量

  • 表达式

如果可以的话,会按照Java Bean命名约定(例如,firstName成员使用setter setFirstName(…​)),通过代理类的公有setter方法,注入变量。如果该字段没有可用的setter,会直接设置该代理类的私有成员的值。有的环境中,SecurityManagers不允许修改私有字段,因此为想要注入的字段,暴露一个公有setter方法,是更安全的做法。

不论在流程定义中声明的是什么类型的值,注入对象的setter/私有字段的类型,总是org.activiti.engine.delegate.Expression。解析表达式后,可以被转型为合适的类型。

当使用'actviiti:class'属性时,支持字段注入。也可以在使用activiti:delegateExpression属性时,进行字段注入,然而因为线程安全的考虑,需要有特殊的规则(参见下一章节)。

下面的代码片段展示了如何为类中声明的字段注入常量值。请注意按照BPMN 2.0 XML概要的要求,在实际字段注入声明前,需要先声明’extensionElements’XML元素

<serviceTask id="javaService"
    name="Java service invocation"
    activiti:class="org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
    <extensionElements>
      <activiti:field name="text" stringValue="Hello World" />
  </extensionElements>
</serviceTask>

ToUpperCaseFieldInjected类有一个字段text,为org.activiti.engine.delegate.Expression类型。当调用text.getValue(execution)时,会返回配置的字符串Hello World

public class ToUpperCaseFieldInjected implements JavaDelegate {

  private Expression text;

  public void execute(DelegateExecution execution) {
    execution.setVariable("var", ((String)text.getValue(execution)).toUpperCase());
  }

}

另外,对于较长文本(例如邮件内容),可以使用'activiti:string'子元素:

<serviceTask id="javaService"
    name="Java service invocation"
    activiti:class="org.activiti.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
  <extensionElements>
    <activiti:field name="text">
        <activiti:string>
          This is a long string with a lot of words and potentially way longer even!
      </activiti:string>
    </activiti:field>
  </extensionElements>
</serviceTask>

要在运行时动态解析注入的值,可以使用表达式。这种表达式可以使用流程变量,或者Spring定义的bean(如果使用Spring)。像服务任务实现中提到的,当服务任务中使用activiti:class属性时,该Java类的实例在所有流程实例中共享。要动态地为字段注入值,可以在org.activiti.engine.delegate.Expression中注入值或方法表达式,它们会通过execute方法传递的DelegateExecution计算/调用。

下面的示例类,使用了注入的表达式,并使用当前的DelegateExecution解析它们。调用generBean方法时传递的是gender变量。完整的代码与测试可以在org.activiti.examples.bpmn.servicetask.JavaServiceTaskTest.testExpressionFieldInjection中找到

<serviceTask id="javaService" name="Java service invocation"
  activiti:class="org.activiti.examples.bpmn.servicetask.ReverseStringsFieldInjected">

  <extensionElements>
    <activiti:field name="text1">
      <activiti:expression>${genderBean.getGenderString(gender)}</activiti:expression>
    </activiti:field>
    <activiti:field name="text2">
       <activiti:expression>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</activiti:expression>
    </activiti:field>
  </ extensionElements>
</ serviceTask>
public class ReverseStringsFieldInjected implements JavaDelegate {

  private Expression text1;
  private Expression text2;

  public void execute(DelegateExecution execution) {
    String value1 = (String) text1.getValue(execution);
    execution.setVariable("var1", new StringBuffer(value1).reverse().toString());

    String value2 = (String) text2.getValue(execution);
    execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
  }
}

另外,为避免XML太过冗长,可以将表达式设置为属性,而不是子元素。

<activiti:field name="text1" expression="${genderBean.getGenderString(gender)}" />
<activiti:field name="text1" expression="Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}" />
字段注入与线程安全 Field injection and thread safety

总的来说,在服务任务中使用Java代理与字段注入是线程安全的。然而,有些情况下不能保证线程安全,取决于设置,或Activiti运行所在的环境。

当使用activiti:class属性时,使用字段注入总是线程安全的(译者注:仍不完全安全,如对于多实例服务任务,使用的是同一个类实例)。对于引用了某个类的每一个服务任务,都会实例化新的实例,并且在创建实例时注入一次字段。在不同的任务或流程定义中多次使用同一个类没有问题。

当使用activiti:expression属性时,不能使用字段注入。只能通过方法调用传递变量,并且这总是线程安全的。

当使用activiti:delegateExpression属性时,代理实例的线程安全性,取决于表达式解析的方式。如果该代理表达式在多个任务与/或流程定义中重复使用,并且表达式总是返回相同的示例,则字段注入不是线程安全的。让我们看几个例子。

假设表达式为${factory.createDelegate(someVariable)},其中factory为引擎可用的Java bean(例如使用Spring集成时的Spring bean),并在每次表达式解析时,创建新的实例。这种情况下,使用字段注入时,没有线程安全性问题:每次表达式解析时,新实例的字段都会注入。

然而,如果表达式为${someJavaDelegateBean},解析为JavaDelegate的实现,并且在创建单例的环境(如Spring)中运行。当在不同的任务和/或流程定义中使用这个表达式时,表达式总会解析为相同的实例。这种情况下,使用字段注入不是线程安全的。例如:

<serviceTask id="serviceTask1" activiti:delegateExpression="${someJavaDelegateBean}">
    <extensionElements>
        <activiti:field name="someField" expression="${input * 2}"/>
    </extensionElements>
</serviceTask>

<!-- other process definition elements -->

<serviceTask id="serviceTask2" activiti:delegateExpression="${someJavaDelegateBean}">
    <extensionElements>
        <activiti:field name="someField" expression="${input * 2000}"/>
    </extensionElements>
</serviceTask>

这段示例代码有两个服务任务,使用同一个代理表达式,但是expression字段填写不同的值。如果该表达式解析为相同的实例,就会在并发场景下,注入someField字段时出现竞争条件

最简单的解决方案,为

  • 重写Java代理,以使用表达式,并将所需数据通过方法参数传递给代理。

  • 或者,在每次代理表达式解析时,返回代理类的新实例。这意味着这个bean的scope(范围)必须是prototype(原型)(例如在代理类上加上@Scope(SCOPE_PROTOTYPE)注解)。

在Activiti 5.21版本中,可以通过配置流程引擎配置,禁用在代理表达式上使用字段注入。需要设置delegateExpressionFieldInjectionMode参数(取org.activiti.engine.imp.cfg.DelegateExpressionFieldInjectionMode枚举中的值)。

可使用下列选项:

  • DISABLED(禁用):当使用代理表达式时,完全禁用字段注入。不会再尝试进行字段注入。这是最安全的方式,保证线程安全。

  • COMPATIBILITY(兼容):在这个模式下,行为与5.21版本之前完全一样:可以在代理表达式中使用字段注入,如果代理类中没有定义该字段,会抛出异常。这是最不线程安全的模式,但可以保证历史版本兼容性,也可以在代理表达式只在一个任务中使用的时候(因此不会产生并发竞争条件),安全使用。

  • MIXED(混合):可以在使用代理表达式时注入,但当代理中没有定义字段时,不会抛出异常。这样可以在部分代理中使用注入(例如不是单例时),而在部分代理中不使用注入。

  • Activiti 5.x版本的默认模式为COMPATIBILITY(兼容)

  • Activiti 6.x版本的默认模式为MIXED(混合)

例如,假设使用MIXED模式,并使用Spring集成,在Spring配置中定义了如下bean:

<bean id="singletonDelegateExpressionBean"
  class="org.activiti.spring.test.fieldinjection.SingletonDelegateExpressionBean" />

<bean id="prototypeDelegateExpressionBean"
  class="org.activiti.spring.test.fieldinjection.PrototypeDelegateExpressionBean"
  scope="prototype" />

第一个bean是一般的Spring bean,因此是单例的。第二个的scope为prototype,因此每次请求这个bean时,Spring容器都会返回一个新实例。

在以下流程定义中:

<serviceTask id="serviceTask1" activiti:delegateExpression="${prototypeDelegateExpressionBean}">
  <extensionElements>
    <activiti:field name="fieldA" expression="${input * 2}"/>
    <activiti:field name="fieldB" expression="${1 + 1}"/>
    <activiti:field name="resultVariableName" stringValue="resultServiceTask1"/>
  </extensionElements>
</serviceTask>

<serviceTask id="serviceTask2" activiti:delegateExpression="${prototypeDelegateExpressionBean}">
  <extensionElements>
    <activiti:field name="fieldA" expression="${123}"/>
    <activiti:field name="fieldB" expression="${456}"/>
    <activiti:field name="resultVariableName" stringValue="resultServiceTask2"/>
  </extensionElements>
</serviceTask>

<serviceTask id="serviceTask3" activiti:delegateExpression="${singletonDelegateExpressionBean}">
  <extensionElements>
    <activiti:field name="fieldA" expression="${input * 2}"/>
    <activiti:field name="fieldB" expression="${1 + 1}"/>
    <activiti:field name="resultVariableName" stringValue="resultServiceTask1"/>
  </extensionElements>
</serviceTask>

<serviceTask id="serviceTask4" activiti:delegateExpression="${singletonDelegateExpressionBean}">
  <extensionElements>
    <activiti:field name="fieldA" expression="${123}"/>
    <activiti:field name="fieldB" expression="${456}"/>
    <activiti:field name="resultVariableName" stringValue="resultServiceTask2"/>
  </extensionElements>
</serviceTask>

有四个服务任务,第一、二个使用${prototypeDelegateExpressionBean}代理表达式,第三、四个使用${singletonDelegateExpressionBean}代理表达式。

先看原型bean:

public class PrototypeDelegateExpressionBean implements JavaDelegate {

  public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);

  private Expression fieldA;
  private Expression fieldB;
  private Expression resultVariableName;

  public PrototypeDelegateExpressionBean() {
    INSTANCE_COUNT.incrementAndGet();
  }

  @Override
  public void execute(DelegateExecution execution) throws Exception {

    Number fieldAValue = (Number) fieldA.getValue(execution);
    Number fieldValueB = (Number) fieldB.getValue(execution);

    int result = fieldAValue.intValue() + fieldValueB.intValue();
    execution.setVariable(resultVariableName.getValue(execution).toString(), result);
  }

}

在运行上面流程定义的一个流程实例后,检查INSTANCE_COUNT,会得到2。这是因为每次${prototypeDelegateExpressionBean}解析时,都会创建新实例。可以看到三个Expression成员字段的注入没有任何问题。

而在原型bean中,有一点区别:

public class SingletonDelegateExpressionBean implements JavaDelegate {

  public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);

  public SingletonDelegateExpressionBean() {
    INSTANCE_COUNT.incrementAndGet();
  }

  @Override
  public void execute(DelegateExecution execution) throws Exception {

    Expression fieldAExpression = DelegateHelper.getFieldExpression(execution, "fieldA");
    Number fieldA = (Number) fieldAExpression.getValue(execution);

    Expression fieldBExpression = DelegateHelper.getFieldExpression(execution, "fieldB");
    Number fieldB = (Number) fieldBExpression.getValue(execution);

    int result = fieldA.intValue() + fieldB.intValue();

    String resultVariableName = DelegateHelper.getFieldExpression(execution, "resultVariableName").getValue(execution).toString();
    execution.setVariable(resultVariableName, result);
  }

}

INSTANCE_COUNT总是1,因为是单例模式。在这个代理中,没有Expression成员字段。因为我们使用的是MIXED模式,可以这样用。而在COMPATIBILITY模式下,就会抛出异常,因为需要有成员字段。这个bean也可以使用DISABLED模式,但会禁用上面进行了字段注入的原型bean。

在代理的代码里,使用了org.activiti.engine.delegate.DelegateHelper。它提供了一些有用的工具方法,用于执行相同的逻辑,并且在单例中是线程安全的。与注入Expression不同,它通过getFieldExpression读取。这意味着在服务任务的XML里,字段定义与单例bean完全相同。查看上面的XML代码,可以看到定义是相同的,只是实现逻辑不同。

(技术提示:getFieldExpression直接读取BpmnModel,并在方法执行时创建表达式,因此是线程安全的)。

  • 在Activiti 5.x版本中,(由于架构缺陷)不能在ExecutionListenerTaskListener中使用DelegateHelper。要保证监听器的线程安全,仍需使用表达式,或确保每次解析代理表达式时,都创建新实例。

  • 在Activiti 6.x版本中,在ExecutionListenerTaskListener中可以使用DelegateHelper。例如在6.x版本中,下列代码可以使用DelegateHelper

<extensionElements>
  <activiti:executionListener
      delegateExpression="${testExecutionListener}" event="start">
    <activiti:field name="input" expression="${startValue}" />
    <activiti:field name="resultVar" stringValue="processStartValue" />
  </activiti:executionListener>
</extensionElements>

其中testExecutionListener解析为ExecutionListener接口的一个实现的实例:

@Component("testExecutionListener")
public class TestExecutionListener implements ExecutionListener {

  @Override
  public void notify(DelegateExecution execution) {
    Expression inputExpression = DelegateHelper.getFieldExpression(execution, "input");
    Number input = (Number) inputExpression.getValue(execution);

    int result = input.intValue() * 100;

    Expression resultVarExpression = DelegateHelper.getFieldExpression(execution, "resultVar");
    execution.setVariable(resultVarExpression.getValue(execution).toString(), result);
  }

}
服务任务的结果 Service task results

服务执行的返回值(仅对使用表达式的服务任务),可以通过为脚本任务定义的'activiti:resultVariable'属性设置流程变量名,指定为已经存在的,或者新的流程变量。指定的已有值的流程变量,会被服务执行的结果值覆盖。当不指定结果变量名时,服务执行的结果值将被忽略。

<serviceTask id="aMethodExpressionServiceTask"
    activiti:expression="#{myService.doSomething()}"
    activiti:resultVariable="myVar" />

在上例中,服务执行的结果(流程变量或Spring bean中,使用'myService'名字获取的对象,调用'doSomething()'方法的返回值),在服务执行完成后,会设置为名为'myVar'的流程变量。

处理异常 Handling exceptions

当执行自定义逻辑时,通常需要捕获特定的业务异常,并在流程中处理。Activiti提供了不同的方法。

抛出BPMN错误 Throwing BPMN Errors

可以在服务任务或脚本任务的用户代码中抛出BPMN错误。要这么做,可以在Java代理、脚本、表达式与代理表达式中,抛出特殊的ActivitiException,叫做BpmnError。引擎会捕获这个异常,并将其转发至合适的错误处理器,例如异常边界事件,或者错误事件子程序。

public class ThrowBpmnErrorDelegate implements JavaDelegate {

  public void execute(DelegateExecution execution) throws Exception {
    try {
      executeBusinessLogic();
    } catch (BusinessException e) {
      throw new BpmnError("BusinessExceptionOccurred");
    }
  }

}

构造函数的参数是错误代码,将被用于决定处理这个错误的错误处理器。参见错误边界事件了解如何捕获BPMN错误。

这个机制只应该用于业务错误,需要通过流程中定义的错误边界事件或错误事件子流程处理。技术错误应该通过其他异常类型表现,并且通常不在流程内部处理。

异常映射 Exception mapping

也可以使用mapException扩展,直接将Java异常映射至业务异常(错误)。单一映射是最简单的格式:

<serviceTask id="servicetask1" name="Service Task" activiti:class="...">
  <extensionElements>
    <activiti:mapException
          errorCode="myErrorCode1">org.activiti.SomeException</activiti:mapException>
  </extensionElements>
</serviceTask>

在上面的代码中,如果服务任务抛出了org.activiti.SomeException的实例,则会被捕获,并被转换为带有给定errorCode的BPMN异常(错误)。从这里开始,可以与普通BPMN异常(错误)完全一样地处理。

其他异常会依照没有映射被处理,将传播至API调用者。

也可以在一行中,使用includeChildExceptions属性,映射特定异常的所有子异常。

<serviceTask id="servicetask1" name="Service Task" activiti:class="...">
  <extensionElements>
    <activiti:mapException errorCode="myErrorCode1"
           includeChildExceptions="true">org.activiti.SomeException</activiti:mapException>
  </extensionElements>
</serviceTask>

上面的代码中,Activiti会将任何直接或间接的SomeException的子类,转换为带有给定错误代码的BPMN错误。当未指定includeChildExceptions时,视为“false”。

最普通的是默认映射。默认映射是一个没有类的映射,可以匹配任何Java异常:

<serviceTask id="servicetask1" name="Service Task" activiti:class="...">
  <extensionElements>
    <activiti:mapException errorCode="myErrorCode1"/>
  </extensionElements>
</serviceTask>

映射会按照顺序检查,从上至下,使用第一个匹配的映射,除了默认映射。默认映射将只在所有映射都不能成功匹配时使用。只有第一个没有类的映射会当做默认映射处理。默认映射忽略includeChildExceptions

异常顺序流 Exception Sequence Flow

也可以选择在发生异常时,将流程执行路由至另一条路径。下面的例子展示了如何做。

<serviceTask id="javaService"
  name="Java service invocation"
  activiti:class="org.activiti.ThrowsExceptionBehavior">
</serviceTask>

<sequenceFlow id="no-exception" sourceRef="javaService" targetRef="theEnd" />
<sequenceFlow id="exception" sourceRef="javaService" targetRef="fixException" />

在这里,这个服务任务具有两条出口顺序流,分别称为exceptionno-exception。这些顺序流id会在发生异常时,用于控制流程流向:

public class ThrowsExceptionBehavior implements ActivityBehavior {

  public void execute(ActivityExecution execution) throws Exception {
    String var = (String) execution.getVariable("var");

    PvmTransition transition = null;
    try {
      executeLogic(var);
      transition = execution.getActivity().findOutgoingTransition("no-exception");
    } catch (Exception e) {
      transition = execution.getActivity().findOutgoingTransition("exception");
    }
    execution.take(transition);
  }

}
在JavaDelegate中使用Activiti服务 Using an Activiti service from within a JavaDelegate

有的时候,需要在Java服务任务中使用Activiti服务(例如当调用活动不符合需求时,通过RuntimeService启动流程实例)。org.activiti.engine.delegate.DelegateExecution可以方便地通过org.activiti.engine.EngineServices接口使用这些服务:

public class StartProcessInstanceTestDelegate implements JavaDelegate {

  public void execute(DelegateExecution execution) throws Exception {
    RuntimeService runtimeService = execution.getEngineServices().getRuntimeService();
    runtimeService.startProcessInstanceByKey("myProcess");
  }

}

通过这个接口可以访问所有Activiti服务API。

使用这些API调用造成的所有数据变更,都处在当前事务中。在具有依赖注入的环境,如Spring或CDI中,使用或不使用激活JTA的数据源,也都可以使用。例如,下面的代码片段与上面的代码具有相同功能,但RuntimeService是通过注入而不是通过org.activiti.engine.EngineServices接口获得的。

@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {

    @Autowired
    private RuntimeService runtimeService;

    public void startProcess() {
      runtimeService.startProcessInstanceByKey("oneTaskProcess");
    }

}

重要技术提示:在当前事务中进行的服务调用,产生或修改的数据是在服务任务执行完成的,因此更改还未刷入数据库。所有API调用都通过处理数据库数据而生效,这意味着这些未提交的修改在服务任务的API调用中“不可见”。

8.5.4. Web服务任务 Web Service Task

描述 Description

Web服务任务用于同步调用外部的Web服务。

图示 Graphical Notation

Web服务任务,与Java服务任务显示地一样。

bpmn.web.service.task
XML表示 XML representation

要使用Web服务,需要导入其操作,以及复杂的类型。通过使用指向Web服务的WSDL的导入标签(import tag),可以自动完成这些:

<import importType="http://schemas.xmlsoap.org/wsdl/"
	location="http://localhost:63081/counter?wsdl"
	namespace="http://webservice.activiti.org/" />

上面的声明告知Activiti导入定义,但并不创建条目定义(item definition)与消息。假设我们需要调用一个名为’prettyPrint’的方法,我们需要为请求与回复消息,创建相应的消息与条目定义:

<message id="prettyPrintCountRequestMessage" itemRef="tns:prettyPrintCountRequestItem" />
<message id="prettyPrintCountResponseMessage" itemRef="tns:prettyPrintCountResponseItem" />

<itemDefinition id="prettyPrintCountRequestItem" structureRef="counter:prettyPrintCount" />
<itemDefinition id="prettyPrintCountResponseItem" structureRef="counter:prettyPrintCountResponse" />

在声明服务任务前,需要定义实际引用Web服务的BPMN接口与操作。基本上,是定义“接口”与所需的“操作”。我们对每一个操作都重复使用之前定义的传入与传出消息。例如,下面的声明定义了“counter”接口,与“prettyPrintCountOperation”操作:

<interface name="Counter Interface" implementationRef="counter:Counter">
	<operation id="prettyPrintCountOperation" name="prettyPrintCount Operation"
			implementationRef="counter:prettyPrintCount">
		<inMessageRef>tns:prettyPrintCountRequestMessage</inMessageRef>
		<outMessageRef>tns:prettyPrintCountResponseMessage</outMessageRef>
	</operation>
</interface>

现在可以通过使用##WebService实现,声明Web服务任务,并引用Web服务操作。

<serviceTask id="webService"
	name="Web service invocation"
	implementation="##WebService"
	operationRef="tns:prettyPrintCountOperation">
Web服务任务IO规范 Web Service Task IO Specification

除非使用简化方法处理输入与输出数据关联(见下),否则需要为每个Web服务任务声明IO规范,指出任务的输入与输出是什么。这个方法很简单,也兼容BPMN 2.0。在prettyPrint例子中,根据之前声明的条目定义,定义输入与输出:

<ioSpecification>
	<dataInput itemSubjectRef="tns:prettyPrintCountRequestItem" id="dataInputOfServiceTask" />
	<dataOutput itemSubjectRef="tns:prettyPrintCountResponseItem" id="dataOutputOfServiceTask" />
	<inputSet>
		<dataInputRefs>dataInputOfServiceTask</dataInputRefs>
	</inputSet>
	<outputSet>
		<dataOutputRefs>dataOutputOfServiceTask</dataOutputRefs>
	</outputSet>
</ioSpecification>
Web服务任务数据输入关联 Web Service Task data input associations

有两种指定数据输入关联的方式:

  • 使用表达式

  • 使用简化方法

要使用表达式指定数据输入关联,需要定义条目的源与目标,并指定每个条目字段的关联。下面的例子中我们指定了条目的prefix与suffix字段:

<dataInputAssociation>
	<sourceRef>dataInputOfProcess</sourceRef>
	<targetRef>dataInputOfServiceTask</targetRef>
	<assignment>
		<from>${dataInputOfProcess.prefix}</from>
		<to>${dataInputOfServiceTask.prefix}</to>
	</assignment>
	<assignment>
		<from>${dataInputOfProcess.suffix}</from>
		<to>${dataInputOfServiceTask.suffix}</to>
	</assignment>
</dataInputAssociation>

另一方面,也可以使用简化方法。'sourceRef’元素是一个Activiti变量名,而’targetRef’是条目定义的参数。在下面的例子里,将’PrefixVariable’变量的值关联至’prefix’字段,并将’SuffixVariable’变量的值关联至’suffix’字段。

<dataInputAssociation>
	<sourceRef>PrefixVariable</sourceRef>
	<targetRef>prefix</targetRef>
</dataInputAssociation>
<dataInputAssociation>
	<sourceRef>SuffixVariable</sourceRef>
	<targetRef>suffix</targetRef>
</dataInputAssociation>
Web服务任务数据输出关联 Web Service Task data output associations

有两种指定数据输出关联的方式:

  • 使用表达式

  • 使用简化方法

要使用表达式指定数据输出关联,需要定义目标变量与源表达式。这种方法很直接,与数据输入关联类似:

<dataOutputAssociation>
	<targetRef>dataOutputOfProcess</targetRef>
	<transformation>${dataOutputOfServiceTask.prettyPrint}</transformation>
</dataOutputAssociation>

另一方面,也可以使用简化方法。'sourceRef’是条目定义的参数,而’targetRef’元素是一个Activiti变量名。这种方法很直接,与数据输入关联类似:

<dataOutputAssociation>
	<sourceRef>prettyPrint</sourceRef>
	<targetRef>OutputVariable</targetRef>
</dataOutputAssociation>

8.5.5. 业务规则任务 Business Rule Task

描述 Description

业务规则任务用于同步执行一条或多条规则。Activiti使用名为Drools Expert的Drools规则引擎执行业务规则。目前,业务规则中包含的.drl文件,必须与定义了业务规则服务并执行规则的流程定义,一起部署。这意味着流程中使用的所有.drl文件都需要打包在流程BAR文件中,与任务表单类似。要了解为Drools Expert创建业务规则的更多信息,请访问位于JBoss Drools的Drools文档。

如果想要插入自己的规则任务实现,例如,希望通过不同方法使用Drools,或者想使用完全不同的规则引擎,则可以使用BusinessRuleTask的class或expression属性。这样它会与服务任务的行为完全相同。

图示 Graphical Notation

业务规则任务,显示为带有表格图标的圆角矩形。

bpmn.business.rule.task
XML表示 XML representation

要执行一条或多条,与流程定义在同一个BAR文件中部署的业务规则,需要定义输入与结果变量。输入变量可以用流程变量的列表定义,使用逗号分隔。输出变量只能有一个变量名,将执行业务规则数处对象存储至流程变量。请注意结果变量会包含对象的list。如果没有指定结果变量名,会使用默认的org.activiti.engine.rules.OUTPUT。

下面的业务规则任务,执行与流程定义一起部署的所有业务规则:

<process id="simpleBusinessRuleProcess">

  <startEvent id="theStart" />
  <sequenceFlow sourceRef="theStart" targetRef="businessRuleTask" />

  <businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
      activiti:resultVariable="rulesOutput" />

  <sequenceFlow sourceRef="businessRuleTask" targetRef="theEnd" />

  <endEvent id="theEnd" />

</process>

也可以将业务规则任务配置为只执行部署的.drl文件中的一组规则。要做到这一点,需要指定规则名字的列表,用逗号分隔。

<businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
      activiti:rules="rule1, rule2" />

这个例子中只会执行rule1与rule2。

也可以定义需要从执行中排除的规则列表。 execution.

<businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
      activiti:rules="rule1, rule2" exclude="true" />

这个例子中与流程定义一起部署在同一个BAR文件中的所有规则都会被执行,除了rule1与rule2.

前面提到过,还可以自行处理BusinessRuleTask的实现:

<businessRuleTask id="businessRuleTask" activiti:class="${MyRuleServiceDelegate}" />

这样业务规则任务与服务任务的行为完全一样,但仍保持业务规则任务的图标,显示在这里处理业务规则。

8.5.6. 邮件任务 Email Task

Activiti可以通过自动邮件服务任务,增强业务流程。可以向一个或多个收信人发送邮件,支持cc,bcc,HTML内容,等等。请注意邮件任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。因此,在Activiti中,邮件任务实现为一种特殊的服务任务。

邮件服务器配置 Mail server configuration

Activiti引擎通过支持SMTP的外部邮件服务器发送邮件。要发送邮件,引擎需要了解如何连接邮件服务器。可以在activiti.cfg.xml配置文件中设置下面的参数:

参数 必填? 描述

mailServerHost

邮件服务器的主机名(如mail.mycorp.com)。默认为localhost

mailServerPort

是,如果不使用默认端口

邮件服务器的SMTP端口。默认值为25

mailServerDefaultFrom

若用户没有提供地址,默认使用的邮件发件人地址。默认为activiti@activiti.org

mailServerUsername

若服务器需要

部分邮件服务器发信时需要进行认证。默认为空。

mailServerPassword

若服务器需要

部分邮件服务器发信时需要进行认证。默认为空。

mailServerUseSSL

若服务器需要

部分邮件服务器要求ssl通信。默认设置为false。

mailServerUseTLS

若服务器需要

部分邮件服务器要求TLS通信(例如gmail)。默认设置为false。

定义邮件任务 Defining an Email Task

邮件任务实现为特殊的服务任务,通过将服务任务的type定义为'mail'设置。

<serviceTask id="sendMail" activiti:type="mail">

邮件任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:

参数 必填? 描述

to

邮件的收信人。可以使用逗号分隔的列表定义多个接收人

from

邮件的发信人地址。如果不设置,会使用默认配置的地址

cc

邮件的抄送人。可以使用逗号分隔的列表定义多个接收人

bcc

邮件的密送人。可以使用逗号分隔的列表定义多个接收人

charset

可以修改邮件的字符集,对许多非英语语言很必要。

html

邮件的HTML内容

text

邮件的内容,普通非富文本的邮件。对于不支持富文本内容的客户端,可以与html一起使用。客户端会退回为纯文本格式。

htmlVar

存储邮件HTML内容的流程变量名。与html参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。

textVar

存储邮件纯文本内容的流程变量名。与text参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。

ignoreException

处理邮件时的失败,是否抛出ActivitiException。默认设置为false。

exceptionVariableName

当处理邮件时的失败,由于ignoreException = true设置而不会抛出异常,则使用给定名字的变量保存失败信息

使用示例 Example usage

下面的XML代码片段展示了使用邮件任务的示例。

<serviceTask id="sendMail" activiti:type="mail">
  <extensionElements>
    <activiti:field name="from" stringValue="order-shipping@thecompany.com" />
    <activiti:field name="to" expression="${recipient}" />
    <activiti:field name="subject" expression="Your order ${orderId} has been shipped" />
    <activiti:field name="html">
      <activiti:expression>
        <![CDATA[
          <html>
            <body>
              Hello ${male ? 'Mr.' : 'Mrs.' } ${recipientName},<br/><br/>

              As of ${now}, your order has been <b>processed and shipped</b>.<br/><br/>

              Kind regards,<br/>

              TheCompany.
            </body>
          </html>
        ]]>
      </activiti:expression>
    </activiti:field>
  </extensionElements>
</serviceTask>

产生如下结果:

email.task.result

8.5.7. Mule任务 Mule Task

Mule任务可以向Mule发送消息,增强Activiti的集成特性。请注意Mule任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。因此,在Activiti中,Mule任务实现为一种特殊的服务任务。

定义Mule任务 Defining an Mule Task

Mule任务实现为特殊的服务任务,通过将服务任务的type定义为'mule'设置。

<serviceTask id="sendMule" activiti:type="mule">

Mule任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:

参数 必填? 描述

endpointUrl

希望调用的Mule终端(endpoint)。

language

计算payloadExpression字段所用的语言。

payloadExpression

消息的载荷表达式

resultVariable

存储调用结果的变量名。

使用示例 Example usage

下面的XML代码片段展示了使用Mule任务的示例。

<extensionElements>
  <activiti:field name="endpointUrl">
    <activiti:string>vm://in</activiti:string>
  </activiti:field>
  <activiti:field name="language">
    <activiti:string>juel</activiti:string>
  </activiti:field>
  <activiti:field name="payloadExpression">
    <activiti:string>"hi"</activiti:string>
  </activiti:field>
  <activiti:field name="resultVariable">
    <activiti:string>theVariable</activiti:string>
  </activiti:field>
</extensionElements>

8.5.8. Camel任务 Camel Task

Camel任务可以向Mule发送与接收消息,增强Activiti的集成特性。请注意Camel任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。因此,在Activiti中,Camel任务实现为一种特殊的服务任务。还请注意要使用Camel任务功能,需要在项目中包含Activiti Camel模块。

定义Camel任务 Defining a Camel Task

Camel任务实现为特殊的服务任务,通过将服务任务的type定义为'camel'设置。

<serviceTask id="sendCamel" activiti:type="camel">

流程定义本身只需要在服务任务上定义Camel类型。集成逻辑都通过Camel容器代理。默认情况下Activiti引擎在Spring容器中查找camelContext bean。camelContext bean定义了由Camel容器装载的Camel路由。在下面的例子中,路由通过给定的Java包装载,但也可以自行在Spring配置中直接定义路由。

<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
  <packageScan>
    <package>org.activiti.camel.route</package>
  </packageScan>
</camelContext>

可以在Camel网站找到关于Camel路由的更多文档。这篇文档中只通过几个小例子展示基本概念。在第一个例子中,在Activiti工作流中进行最简单的Camel调用。叫做SimpleCamelCall。

如果想要定义多个Camel上下文bean,并且/或想使用不同的bean名字,可以在Camel任务定义中像这样覆盖:

<serviceTask id="serviceTask1" activiti:type="camel">
  <extensionElements>
    <activiti:field name="camelContext" stringValue="customCamelContext" />
  </extensionElements>
</serviceTask>
简单Camel调用示例 Simple Camel Call example

这个例子相关的所有文件,都可以在activiti-camel模块的org.activiti.camel.examples.simpleCamelCall包中找到。目的是简单启动一个camel路由。首先需要一个配置了上面提到的路由的Spring上下文。下面的代码用做这个目的:

<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
  <packageScan>
    <package>org.activiti.camel.examples.simpleCamelCall</package>
  </packageScan>
</camelContext>
public class SimpleCamelCallRoute extends RouteBuilder {

  @Override
  public void configure() throws Exception {
    from("activiti:SimpleCamelCallProcess:simpleCall").to("log:org.activiti.camel.examples.SimpleCamelCall");
  }
}

路由只是记录消息体,不做更多事情。请注意from终端的格式,包含三个部分:

终端Url部分 描述

activiti

引用Activiti终端

SimpleCamelCallProcess

流程名

simpleCall

流程中Camel服务的名字

现在路由已经正确配置,可以访问Camel。下面需要像这样定义工作流:

<process id="SimpleCamelCallProcess">
  <startEvent id="start"/>
  <sequenceFlow id="flow1" sourceRef="start" targetRef="simpleCall"/>

  <serviceTask id="simpleCall" activiti:type="camel"/>

  <sequenceFlow id="flow2" sourceRef="simpleCall" targetRef="end"/>
  <endEvent id="end"/>
</process>
连通性测试 Ping Pong example

示例已经可以工作,但实际上Camel与Activiti之间并没有通信,因此没有太多价值。在这个例子里,将试着从Camel接收与发送消息。将发送一个字符串,Camel在其上连接一些东西,并返回作为结果。发送部分比较普通,以变量的格式将信息发送给Camel服务。这是我们的调用代码:

@Deployment
public void testPingPong() {
  Map<String, Object> variables = new HashMap<String, Object>();

  variables.put("input", "Hello");
  Map<String, String> outputMap = new HashMap<String, String>();
  variables.put("outputMap", outputMap);

  runtimeService.startProcessInstanceByKey("PingPongProcess", variables);
  assertEquals(1, outputMap.size());
  assertNotNull(outputMap.get("outputValue"));
  assertEquals("Hello World", outputMap.get("outputValue"));
}

“input”变量是实际上是Camel路由的输入,而outputMap用于捕获Camel传回的结果。流程像是这样:

<process id="PingPongProcess">
  <startEvent id="start"/>
  <sequenceFlow id="flow1" sourceRef="start" targetRef="ping"/>
  <serviceTask id="ping" activiti:type="camel"/>
  <sequenceFlow id="flow2" sourceRef="ping" targetRef="saveOutput"/>
  <serviceTask id="saveOutput"  activiti:class="org.activiti.camel.examples.pingPong.SaveOutput" />
  <sequenceFlow id="flow3" sourceRef="saveOutput" targetRef="end"/>
  <endEvent id="end"/>
</process>

请注意SaveOutput服务任务,将“Output”变量从上下文中取出,存储至上面提到的OutputMap。现在需要了解变量如何发送至Camel,以及如何返回。这就需要了解Camel行为(Behavior)的概念。变量与Camel通信的方式可以通过CamelBehavior配置。在这个例子里使用默认配置,其它配置在后面会进行简短介绍。下面的代码配置了期望的Camel行为:

<serviceTask id="serviceTask1" activiti:type="camel">
  <extensionElements>
    <activiti:field name="camelBehaviorClass" stringValue="org.activiti.camel.impl.CamelBehaviorCamelBodyImpl" />
  </extensionElements>
</serviceTask>

如果不指定行为,则会设置为org.activiti.camel.impl.CamelBehaviorDefaultImpl。这个行为将以相同名字,将变量复制到Camel参数。对于返回值,无论选择什么行为,如果Camel消息体是一个map,则其中的每个元素都将复制为变量,否则整个对象将复制为名为"camelBody"的特定变量。了解这些后,Camel路由总结为第二个例子:

@Override
public void configure() throws Exception {
  from("activiti:PingPongProcess:ping").transform().simple("${property.input} World");
}

在这个路由中,字符串"world"会在结尾连接上名为“input”的参数,结果作为消息体。可以通过Java服务任务检查"camelBody"变量,并复制到“outputMap”,并可通过测试用例检查。既然这个例子使用默认行为,就让我们看看还有什么其他选择。在每个Camel路由的开始处,流程实例id会复制为名为"PROCESS_ID_PROPERTY"的Camel参数。之后会用于将流程实例与Camel路由相关联,也可以在Camel路由中使用。

Activiti中有已经可以使用三种不同的行为。可以通过修改路由URL中特定的部分,覆写行为。这里有个在URL中重载已有行为的例子:

from("activiti:asyncCamelProcess:serviceTaskAsync2?copyVariablesToProperties=true").

下表展示了三种可用的Camel行为:

行为 Url中 描述

CamelBehaviorDefaultImpl

copyVariablesToProperties

将Activiti变量复制为Camel参数

CamelBehaviorCamelBodyImpl

copyCamelBodyToBody

只将名为"camelBody"的Activiti变量复制为Camel消息体

CamelBehaviorBodyAsMapImpl

copyVariablesToBodyAsMap

将一个map中的所有Activiti变量复制为Camel消息体

上表解释了Activiti变量如何传递给Camel。下表解释了Camel变量如何返回至Activiti。只能在路由URL中配置。

Url 描述

Default

如果Camel消息体是一个map,则将其中每一对象复制为Activiti变量;否则将整个Camel消息体复制为"camelBody" Activiti变量

copyVariablesFromProperties

将Camel参数以同名复制为Activiti变量

copyCamelBodyToBodyAsString

与default相同,但如果Camel消息体不是map,则首先将其转换为字符串,然后再复制为"camelBody"

copyVariablesFromHeader

额外将Camel头复制为Activiti的同名变量

返回变量 Returning back the variables

上面提到的传递变量,不论是从Camel到Activiti还是反过来,都只用于变量传递的开始侧。要特别注意,由于Activiti的非阻塞行为,Activiti不会自动向Camel返回变量。因此,提供了特殊的语法。可以在Camel路由URL中,以var.return.someVariableName的格式,使用一个或多个参数。与这些参数同名,但没有var.return部分的变量,会被认为是输出变量,因此将会以相同的名字复制回Camel参数。例如在如下路由中:

from("direct:start").to("activiti:process?var.return.exampleVar").to("mock:result");

名为exampleVar的Activiti变量,将被认为是输出变量,因此会以同名复制回Camel参数。

异步连通性测试 Asynchronous Ping Pong example

上面的例子都是同步的。工作流停止,直到Camel路由结束并返回。有时,需要Activiti工作流继续运行。为了这个目的,Camel服务任务的异步功能就很有用。可以通过将Camel服务任务的异步参数设置为true,启用这个功能。

<serviceTask id="serviceAsyncPing" activiti:type="camel" activiti:async="true"/>

设置这个特性后,Camel路由会由Activiti作业执行器异步启动。如果定义了Camel路由队列,Activiti流程会继续执行Camel服务任务之后的活动。Camel路由会与流程执行完全异步地执行。如果需要在流程定义的某处等待Camel服务任务的响应,可以使用接收任务(receive task)。

<receiveTask id="receiveAsyncPing" name="Wait State" />

流程实例会等待,直到接收到信号,例如来自Camel。在Camel中,可以通过向合适的Activiti终端发送消息,来为流程实例发送信号。

 from("activiti:asyncPingProcess:serviceAsyncPing").to("activiti:asyncPingProcess:receiveAsyncPing");

(译者注:原文如此。可能为缺失了的 to 终端的定义:)

  • “activiti”字符串常量

  • 流程名

  • 接收任务名

从Camel路由实例化工作流 Instantiate workflow from Camel route

上面的所有例子,都是先启动Activiti工作流,然后在工作流中启动Camel路由。也可以反过来。可以在已经启动的Camel路由中实例化工作流。与为接收任务发送消息很类似,除了最后一部分。这是一个简单的路由:

from("direct:start").to("activiti:camelProcess");

可以看到url有两部分,第一部分是“activiti”字符串常量,第二个名字是流程的名字。很明显流程需要已经部署,并且可以通过引擎配置启动。

也可以在Camel头中,将流程起动人设置为某个已认证用户id。要这么做,首先需要在流程定义中指定启动人变量:

<startEvent id="start" activiti:initiator="initiator" />

然后在Camel头中的CamelProcessInitiatorHeader指定用户id。Camel路由会如下定义:

from("direct:startWithInitiatorHeader")
    .setHeader("CamelProcessInitiatorHeader", constant("kermit"))
    .to("activiti:InitiatorCamelCallProcess?processInitiatorHeaderName=CamelProcessInitiatorHeader");

8.5.9. 手动任务 Manual Task

描述 Description

手动任务定义了在BPMN引擎外部的任务。用于建模引擎不需要了解的某项工作,或者其他系统或用户界面。对于引擎来说,手动任务将按直接穿过活动处理,在流程执行到达时,自动继续流程。

图示 Graphical Notation

手动任务,表现为左上角带有“手型”图标的圆角矩形。

bpmn.manual.task
XML表示 XML representation
<manualTask id="myManualTask" name="Call client for more information" />

8.5.10. Java接收任务 Java Receive Task

描述 Description

接收任务,是等待特定消息到达的简单任务。目前,我们只为这个任务实现了Java语义。当流程执行到达接收任务时,流程状态将提交至持久化存储。这意味着流程将保持等待状态,直到引擎接收到特定的消息,并将触发流程通过接收任务。

图示 Graphical notation

接收任务,表现为右上角带有消息图标的任务(圆角矩形)。消息图标是白色的(黑色消息图标代表发送的含义)。

bpmn.receive.task
XML表示 XML representation
<receiveTask id="waitState" name="wait" />

要使流程实例从当前的等待状态,如接收任务中继续,需要使用到达接收任务的执行id,调用runtimeService.signal(executionId)。下面的代码片段展示了如何操作:

ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
Execution execution = runtimeService.createExecutionQuery()
  .processInstanceId(pi.getId())
  .activityId("waitState")
  .singleResult();
assertNotNull(execution);

runtimeService.signal(execution.getId());

8.5.11. Shell任务 Shell Task

描述 Description

Shell任务可以运行Shell脚本与命令。请注意Shell任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。

定义Shell任务 Defining a shell task

Shell任务实现为特殊的服务任务,通过将服务任务的type定义为'shell'设置。

<serviceTask id="shellEcho" activiti:type="shell">

Shell任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:

参数 必填? 类型 描述 默认值

command

String

要执行的Shell命令。

arg0-5

String

参数0至参数5

wait

true/false

如果可能,是否等待Shell进程终止。

true

redirectError

true/false

将标准错误(standard error)并入标准输出(standard output)。

false

cleanEnv

true/false

Shell进程不继承当前环境。

false

outputVariable

String

保存输出的变量名

不会记录输出。

errorCodeVariable

String

保存结果错误代码的变量名

不会注册错误级别。

directory

String

Shell进程的默认目录

当前目录

使用示例 Example usage

下面的XML代码片段展示了使用Shell任务的例子。会运行"cmd /c echo EchoTest" Shell脚本,等待其结束,并将结果放入resultVar。

<serviceTask id="shellEcho" activiti:type="shell" >
  <extensionElements>
    <activiti:field name="command" stringValue="cmd" />
    <activiti:field name="arg1" stringValue="/c" />
    <activiti:field name="arg2" stringValue="echo" />
    <activiti:field name="arg3" stringValue="EchoTest" />
    <activiti:field name="wait" stringValue="true" />
    <activiti:field name="outputVariable" stringValue="resultVar" />
  </extensionElements>
</serviceTask>

8.5.12. 执行监听器 Execution listener

兼容性提示:在5.3版本后,我们发现执行监听器、任务监听器(task listeners)与表达式仍然在非公开API中。这些类在org.activiti.engine.impl…​子包中。org.activiti.engine.impl.pvm.delegate.ExecutionListenerorg.activiti.engine.impl.pvm.delegate.TaskListenerorg.activiti.engine.impl.pvm.el.Expression已被废弃。从现在起,应该使用org.activiti.engine.delegate.ExecutionListenerorg.activiti.engine.delegate.TaskListenerorg.activiti.engine.delegate.Expression。在新的公开可用的API中,对ExecutionListenerExecution.getEventSource()的访问已被移除。除了编译器的废弃警告,现有代码可以正常运行。但是请考虑切换至新的公开API接口(包名中不带有.impl.)。

执行监听器可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式。可以被捕获的事件有:

  • 流程实例的start(启动)和end(结束)。

  • take(进行)转移(transition)。

  • 活动的start和end。

  • 网关的start和end。

  • 中间事件的start和end。

  • 启动事件的end,和结束事件的start。

下面的流程定义包含了三个执行监听器:

<process id="executionListenersProcess">

  <extensionElements>
    <activiti:executionListener class="org.activiti.examples.bpmn.executionlistener.ExampleExecutionListenerOne" event="start" />
  </extensionElements>

  <startEvent id="theStart" />
  <sequenceFlow sourceRef="theStart" targetRef="firstTask" />

  <userTask id="firstTask" />
  <sequenceFlow sourceRef="firstTask" targetRef="secondTask">
    <extensionElements>
      <activiti:executionListener class="org.activiti.examples.bpmn.executionListener.ExampleExecutionListenerTwo" />
    </extensionElements>
  </sequenceFlow>

  <userTask id="secondTask" >
    <extensionElements>
      <activiti:executionListener expression="${myPojo.myMethod(execution.event)}" event="end" />
    </extensionElements>
  </userTask>
  <sequenceFlow sourceRef="secondTask" targetRef="thirdTask" />

  <userTask id="thirdTask" />
  <sequenceFlow sourceRef="thirdTask" targetRef="theEnd" />

  <endEvent id="theEnd" />

</process>

第一个执行监听器将在流程启动时得到通知。这个监听器是一个外部Java类(例如ExampleExecutionListenerOne),并且需要实现org.activiti.engine.delegate.ExecutionListener接口。当该事件发生时(这里是start事件),会调用notify(ExecutionListenerExecution execution)方法。

public class ExampleExecutionListenerOne implements ExecutionListener {

  public void notify(ExecutionListenerExecution execution) throws Exception {
    execution.setVariable("variableSetInExecutionListener", "firstValue");
    execution.setVariable("eventReceived", execution.getEventName());
  }
}

也可以使用实现了org.activiti.engine.delegate.JavaDelegate接口的代理类。这些代理类也可以用于其他的结构,例如服务任务的代理。

第二个执行监听器在take(进行)转移时被调用。请注意listener元素并未定义event,因为在转移上只会触发take事件。当监听器定义在转移上时,event属性的值将被忽略。

最后一个执行监听器在secondTask活动结束时被调用。监听器声明中没有使用class,而是定义了expression,并将在事件触发时计算/调用。

<activiti:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />

与其他表达式一样,可以使用与解析execution变量。因为execution实现对象有一个暴露事件名的参数,因此可以使用execution.eventName向你的方法传递事件名。

执行监听器也支持使用delegateExpression与服务任务类似

<activiti:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />

在Activiti 5.12中,我们也引入了新的执行监听器类型,org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener。这个脚本执行监听器,可以为一个执行监听器事件执行一段脚本逻辑。

<activiti:executionListener event="start" class="org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener" >
  <activiti:field name="script">
    <activiti:string>
      def bar = "BAR";  // local variable
      foo = "FOO"; // pushes variable to execution context
      execution.setVariable("var1", "test"); // test access to execution instance
      bar // implicit return value
    </activiti:string>
  </activiti:field>
  <activiti:field name="language" stringValue="groovy" />
  <activiti:field name="resultVariable" stringValue="myVar" />
</activiti:executionListener>
执行监听器上的字段注入 Field injection on execution listeners

当使用通过class属性配置的执行监听器时,可以使用字段注入。与服务任务字段注入使用完全相同的机制,可以在那里看到字段注入提供的各种可能用法。

下面的代码片段展示了简单的示例流程,有一个使用了字段注入的执行监听器。

<process id="executionListenersProcess">
  <extensionElements>
    <activiti:executionListener class="org.activiti.examples.bpmn.executionListener.ExampleFieldInjectedExecutionListener" event="start">
      <activiti:field name="fixedValue" stringValue="Yes, I am " />
      <activiti:field name="dynamicValue" expression="${myVar}" />
    </activiti:executionListener>
  </extensionElements>

  <startEvent id="theStart" />
  <sequenceFlow sourceRef="theStart" targetRef="firstTask" />

  <userTask id="firstTask" />
  <sequenceFlow sourceRef="firstTask" targetRef="theEnd" />

  <endEvent id="theEnd" />
</process>
public class ExampleFieldInjectedExecutionListener implements ExecutionListener {

  private Expression fixedValue;

  private Expression dynamicValue;

  public void notify(ExecutionListenerExecution execution) throws Exception {
    execution.setVariable("var", fixedValue.getValue(execution).toString() + dynamicValue.getValue(execution).toString());
  }
}

ExampleFieldInjectedExecutionListener类连接两个字段(一个是固定值,另一个是动态值),并将其存储在'var'流程变量中。

@Deployment(resources = {"org/activiti/examples/bpmn/executionListener/ExecutionListenersFieldInjectionProcess.bpmn20.xml"})
public void testExecutionListenerFieldInjection() {
  Map<String, Object> variables = new HashMap<String, Object>();
  variables.put("myVar", "listening!");

  ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("executionListenersProcess", variables);

  Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
  assertNotNull(varSetByListener);
  assertTrue(varSetByListener instanceof String);

  // Result is a concatenation of fixed injected field and injected expression
  assertEquals("Yes, I am listening!", varSetByListener);
}

请注意,关于线程安全的规则与服务任务相同。请阅读相应章节了解更多信息。

8.5.13. 任务监听器 Task listener

任务监听器用于在特定的任务相关事件发生时,执行自定义的Java逻辑或表达式。

任务监听器只能在流程定义中作为用户任务的子元素。请注意,任务监听器是一个Activiti自定义结构,因此也需要作为BPMN 2.0 extensionElements,放在activiti命名空间下。

<userTask id="myTask" name="My Task" >
  <extensionElements>
    <activiti:taskListener event="create" class="org.activiti.MyTaskCreateListener" />
  </extensionElements>
</userTask>

任务监听器支持下列属性:

  • event(事件)(必填):任务监听器将被调用的任务事件类型。可用的事件有:

    • create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。

    • assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,create事件触发,首先触发assignment事件。这看起来不是自然顺序,但是有实际原因的:当收到create事件时,我们通常希望查看任务的所有参数,包括办理人。

    • complete(完成):当任务已经完成,从运行时数据中删除前触发。

    • delete(删除):在任务即将被删除前触发。请注意当任务通过completeTask正常完成时也会触发。

  • class:需要调用的代理类。这个类必须实现org.activiti.engine.delegate.TaskListener接口。

public class MyTaskCreateListener implements TaskListener {

  public void notify(DelegateTask delegateTask) {
    // Custom logic goes here
  }

}

也可以使用字段注入,为代理类传递流程变量或执行。请注意代理类的实例在流程部署时创建(与Activiti中其它的代理类一样),这意味着该实例会在所有流程实例执行中共享。

  • expression:(不能与class属性一起使用):指定在事件发生时要执行的表达式。可以为被调用的对象传递DelegateTask对象与事件名(使用task.eventName)作为参数。

<activiti:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
  • delegateExpression:可以指定一个能够解析为TaskListener接口实现类对象的表达式。与服务任务类似

<activiti:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
  • 在Activiti 5.12中,我们也引入了新的执行监听器类型,org.activiti.engine.impl.bpmn.listener.ScriptTaskListener。这个脚本任务监听器,可以为一个任务监听器事件执行一段脚本逻辑。

<activiti:taskListener event="complete" class="org.activiti.engine.impl.bpmn.listener.ScriptTaskListener" >
  <activiti:field name="script">
    <activiti:string>
      def bar = "BAR";  // local variable
      foo = "FOO"; // pushes variable to execution context
      task.setOwner("kermit"); // test access to task instance
      bar // implicit return value
    </activiti:string>
  </activiti:field>
  <activiti:field name="language" stringValue="groovy" />
  <activiti:field name="resultVariable" stringValue="myVar" />
</activiti:taskListener>

8.5.14. 多实例 Multi-instance (for each)

描述 Description

多实例活动是在业务流程中,为特定步骤定义重复的方式。在编程概念中,多实例匹配for each结构:可以为给定集合中的每一条目,顺序或并行地,执行特定步骤,甚至是整个子流程。

多实例是一个普通活动,加上定义(被称作“多实例特性”)的额外参数,会使得活动在运行时被多次执行。下列活动可以成为多实例活动:

网关事件不能成为多实例。

按照规范的要求,所有用于为每个实例创建执行的父执行,都有下列变量:

  • nrOfInstances:实例总数

  • nrOfActiveInstances:当前活动的,也就是说未完成的,实例数量。对于顺序多实例,这个值总为1.

  • nrOfCompletedInstances:已经完成的实例数量

可以通过调用execution.getVariable(x)方法,获取这些值。

另外,每个创建的执行,都有执行本地变量(也就是说,对其他执行不可见,也不存储在流程实例级别):

  • loopCounter:代表给定实例在foreach循环中的index。可以通过Activiti的elementIndexVariable属性为loopCounter变量重命名。

图示 Graphical notation

如果一个活动是多实例,将通过在该活动底部的三条短线表示。三条线代表实例会并行执行,而三条线代表顺序执行。

bpmn.multi.instance
XML表示 Xml representation

要将活动变成多实例,该活动的XML元素必须有multiInstanceLoopCharacteristics子元素

<multiInstanceLoopCharacteristics isSequential="false|true">
 ...
</multiInstanceLoopCharacteristics>

isSequential属性代表了活动的实例为顺序还是并行执行。

实例的数量在进入活动时,计算一次。有不同方法可以配置数量。一个方法是通过loopCardinality子元素,直接指定数字。

<multiInstanceLoopCharacteristics isSequential="false|true">
  <loopCardinality>5</loopCardinality>
</multiInstanceLoopCharacteristics>

也可以使用解析为正整数的表达式:

<multiInstanceLoopCharacteristics isSequential="false|true">
  <loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>

另一个定义实例数量的方法,是使用loopDataInputRef子元素,指定一个集合流程变量的名字。对集合中的每一个条目,都会创建一个实例。可以使用inputDataItem子元素,将集合中的该条目设置给实例。在下面的XML示例中展示:

<userTask id="miTasks" name="My Task ${loopCounter}" activiti:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false">
    <loopDataInputRef>assigneeList</loopDataInputRef>
    <inputDataItem name="assignee" />
  </multiInstanceLoopCharacteristics>
</userTask>

假设变量assigneeList包含[kermit, gonzo, fozzie]。在上面的代码中,会并行创建三个用户任务。每一个执行都有一个名为assignee的流程变量,含有集合中的一个值,并在这个例子中被用于指派用户任务。

loopDataInputRefinputDataItem的缺点是 1)名字很难记 2)由于BPMN 2.0概要的限制,不能使用表达式。Activiti通过在multiInstanceCharacteristics上提供collectionelementVariable属性解决了这些问题:

<userTask id="miTasks" name="My Task" activiti:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="true"
     activiti:collection="${myService.resolveUsersForTask()}" activiti:elementVariable="assignee" >
  </multiInstanceLoopCharacteristics>
</userTask>

多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时计算。当表达式计算为true时,销毁所有剩余的实例,并且结束多实例活动,继续流程。这个表达式必须通过completionCondition子元素定义。

<userTask id="miTasks" name="My Task" activiti:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false"
     activiti:collection="assigneeList" activiti:elementVariable="assignee" >
    <completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

在这个例子里,会为assigneeList集合中的每个元素创建并行实例。然而,当60%的任务完成时,其他的任务将被删除,流程继续运行。

边界事件与多实例 Boundary events and multi-instance

多实例是普通活动,因此可以在其边界定义边界事件。对于中断边界事件,当捕获事件时,活动中的所有实例都会被销毁。以下面的多实例子流程为例:

bpmn.multi.instance.boundary.event

当定时器触发时,子流程的所有实例都会被销毁,无论有多少实例,或者哪个内部活动还未完成。

多实例与执行监听器 Multi instance and execution listeners

(Activiti 5.18及以上版本可用)

有一个关于执行监听器与多实例一起使用的警告。以下面的BPMN 2.0 XML代码片段为例,其定义在multiInstanceLoopCharacteristics XML元素的相同级别:

<extensionElements>
    <activiti:executionListener event="start" class="org.activiti.MyStartListener"/>
		<activiti:executionListener event="end" class="org.activiti.MyEndListener"/>
</extensionElements>

对于普通的BPMN活动,会在活动开始于结束时调用一次监听器。

然而,当该活动为多实例时,行为有区别:

  • 当进入多实例活动时,在任何内部活动执行前,抛出启动事件。这时loopCounter变量还未设置(为null)。

  • 每个实际执行的活动,抛出一个启动事件。这时loopCounter变量已经设置。

对结束事件类似:

  • 当离开实际活动时,抛出一个结束事件。这时loopCounter变量已经设置。

  • 当多实例活动整体完成时,抛出一个结束事件。这时loopCounter变量未设置。

例如:

<subProcess id="subprocess1" name="Sub Process">
  <extensionElements>
    <activiti:executionListener event="start" class="org.activiti.MyStartListener"/>
    <activiti:executionListener event="end" class="org.activiti.MyEndListener"/>
  </extensionElements>
  <multiInstanceLoopCharacteristics isSequential="false">
    <loopDataInputRef>assignees</loopDataInputRef>
    <inputDataItem name="assignee"></inputDataItem>
  </multiInstanceLoopCharacteristics>
  <startEvent id="startevent2" name="Start"></startEvent>
  <endEvent id="endevent2" name="End"></endEvent>
  <sequenceFlow id="flow3" name="" sourceRef="startevent2" targetRef="endevent2"></sequenceFlow>
</subProcess>

在这个例子中,假设assignees有三个条目。在运行时会发生如下事情:

  • 多实例整体抛出一个启动事件。调用一次start执行监听器,loopCounterassignee变量均未设置(也就是说为null)。

  • 每一个活动实例抛出一个启动事件。调用三次start执行监听器,loopCounterassignee变量均已设置(也就是说不为null)。

  • 因此启动执行监听器总共被调用四次。

请注意当multiInstanceLoopCharacteristics不是定义在子元素上,也是如此。例如上面的简单用户任务的例子,也合理适用这一点。

8.5.15. 补偿处理器 Compensation Handlers

描述 Description

如果一个活动要用于补偿另一个活动的影响,可以声明为补偿处理器。补偿处理器不在普通流程中,只在抛出补偿事件时才会执行。

补偿处理器不得有入口或出口顺序流。

补偿处理器必须通过单向连接,关联一个补偿边界事件。

图示 Graphical notation

如果一个活动是补偿处理器,则会在下部中间显示补偿事件图标。 下面摘录的流程图展示了一个带有补偿边界事件的服务任务,并关联至一个补偿处理器。请注意补偿处理器图标显示在"cancel hotel reservation(取消酒店预订)"服务任务的下部中间。

bpmn.boundary.compensation.event
XML表示 XML representation

要将一个活动声明为补偿处理器,需要将isForCompensation属性设置为true:

<serviceTask id="undoBookHotel" isForCompensation="true" activiti:class="...">
</serviceTask>

8.6. 子流程与调用活动 Sub-Processes and Call Activities

8.6.1. 子流程 Sub-Process

描述 Description

子流程是包含其他的活动、网关、事件等的活动。其本身构成一个流程,并作为更大流程的一部分。子流程完全在父流程中定义(这就是为什么经常被称作嵌入式子流程)。

子流程有两个主要的使用场景:

  • 子流程可以分层建模。很多建模工具都可以折叠子流程,隐藏子流程的所有细节,而只显示业务流程的高层端到端总览。

  • 子流程创建了新的事件范围。在子流程执行中抛出的事件,可以通过子流程边界上的边界事件捕获。因此为该事件创建了限制在子流程内的范围。

使用子流程也要注意以下几点:

  • 子流程只能有一个空启动事件,而不允许有其他类型的启动事件。请注意BPMN 2.0规范允许省略子流程的启动与结束事件,然而当前的Activiti实现并不支持省略。

  • 顺序流不能跨越子流程边界。

图示 Graphical Notation

子流程表示为标准活动,即圆角矩形。若折叠了子流程,则只显示其名字与一个加号,提供了流程的高层概览:

bpmn.collapsed.subprocess

展开了子流程,则子流程的所有步骤都在子流程边界内显示:

bpmn.expanded.subprocess

使用子流程的一个主要原因,是为特定事件定义范围。下面的流程模型展示了这种用法:investigate software/investigate hardware(调查硬件/调查软件)两个任务需要并行执行,且需要在给定时限内,在Level 2 support(二级支持)响应前完成。在这里,定时器的范围(即需要按时完成的活动)通过子流程限制。

bpmn.subprocess.with.boundary.timer
XML表示 XML representation

子流程通过subprocess元素定义。子流程中的所有活动、网关、事件等,都需要附在这个元素内。

<subProcess id="subProcess">

  <startEvent id="subProcessStart" />

  ... other Sub-Process elements ...

  <endEvent id="subProcessEnd" />

 </subProcess>

8.6.2. 事件子流程 Event Sub-Process

描述 Description

事件子流程是BPMN 2.0新定义的。事件子流程,是通过事件触发的子流程。可以在流程级别,或者任何子流程级别,添加事件子流程。用于触发事件子流程的事件,使用启动事件配置。因此可知,不能在事件子流程中使用空启动事件。事件子流程可以通过例如消息事件、错误事件、信号时间、定时器事件或补偿事件触发。对启动事件的订阅,在事件子流程的宿主范围(流程实例或子流程)创建时创建。当该范围销毁时,删除订阅。

事件子流程可以是中断或不中断的。中断的子流程将取消当前范围内的任何执行。非中断的事件子流程将创建新的并行执行。宿主范围内的每个活动,只能触发一个中断事件子流程,而非中断事件子流程可以多次触发。子流程是否是中断的,通过触发事件子流程的启动事件配置。

事件子流程不能有任何入口或出口顺序流。事件子流程是由事件触发的,因此入口顺序流不合逻辑。当事件子流程结束时,要么同时结束当前范围(中断事件子流程的情况),要么是非中断子流程创建的并行执行结束。

目前的限制:

  • Activiti只支持中断事件子流程。

  • Activiti只支持错误启动事件与消息启动事件触发事件子流程。

图示 Graphical Notation

事件子流程,表示为点线边框的嵌入式子流程

bpmn.subprocess.eventSubprocess
XML表示 XML representation

事件子流程的XML表示格式,与嵌入式子流程相同。但需要将triggeredByEvent属性设置为true

<subProcess id="eventSubProcess" triggeredByEvent="true">
	...
</subProcess>
示例 Example

下面是使用错误启动事件触发事件子流程的例子。该事件子流程位与“流程级别”,即流程实例的范围:

bpmn.subprocess.eventSubprocess.example.1

事件子流程在XML是这样的:

<subProcess id="eventSubProcess" triggeredByEvent="true">
	<startEvent id="catchError">
		<errorEventDefinition errorRef="error" />
	</startEvent>
	<sequenceFlow id="flow2" sourceRef="catchError" targetRef="taskAfterErrorCatch" />
	<userTask id="taskAfterErrorCatch" name="Provide additional data" />
</subProcess>

前面已经指出,事件子流程也可以添加到嵌入式子流程内。若添加到嵌入式子流程内,将可替代边界事件的功能。考虑下面两个流程图,嵌入式子流程都抛出错误事件,该错误事件都被捕获,并由用户任务处理。

bpmn.subprocess.eventSubprocess.example.2a

对比:

bpmn.subprocess.eventSubprocess.example.2b

两种情况下都执行相同的任务。然而,两种模型选择有如下不同:

  • 嵌入式(事件)子流程使用其宿主范围的执行来执行。这意味着嵌入式(事件)子流程可以访问其范围的局部变量。当使用边界事件时,创建用于执行嵌入式子流程的执行,将被边界事件的出口顺序流删除。这意味着嵌入式子流程创建的变量将不再可用。

  • 使用事件子流程时,事件完全由其所在的子流程处理。当使用边界事件时,事件由其父流程处理。

这两个区别可以帮助你判断,使用边界事件还是嵌入式(事件)子流程,哪个更适合解决特定的流程建模/实现问题。

8.6.3. 事务子流程 Transaction subprocess

描述 Description

事务子流程是一种嵌入式子流程,可用于将多个活动组织在一个事务里。事务是工作的逻辑单元,可以组织一组独立活动,使得它们可以一起成功或失败。

事务的可能结果:事务有三种不同的结果:

  • 若未被取消,或被意外终止,则事务成功。若事务子流程成功,将使用出口顺序流离开。若流程后面抛出了补偿事件,成功的事务可以被补偿。请注意:与“普通”嵌入式子流程一样,可以使用补偿抛出中间事件,在事务成功完成后补偿。

  • 若执行到达取消结束事件时,事务被取消。在这种情况下,所有执行都将被终止并移除。只会保留一个执行,设置为取消边界事件,并将触发补偿。在补偿完成后,事务子流程通过取消边界事件的出口顺序流离开。

  • 若由于抛出了错误结束事件,且未被事务子流程所在的范围捕获,则事务会被意外终止(错误被事件子流程的边界捕获也一样)。在这种情况下,不会进行补偿。

下面的流程图展示了三种不同的结果:

bpmn.transaction.subprocess.example.1

与ACID事务的关系:要注意不要将BPMN事务子流程与技术(ACID)事务混淆。BPMN事务子流程不是划分技术事务范围的方法。要理解Acitivit中的事务管理,请阅读并发与事务章节。BPMN事务与技术事务有如下区别:

  • ACID事务技术上生存期短暂,而BPMN事务可以持续几小时,几天甚至几个月才完成。(考虑一个场景,事务包括的活动中有一个用户任务。通常人的响应时间要比程序长。或者,在另一个场景下,BPMN事务可能等待某些业务事件发生,像是特定订单的填写完成。)这些操作通常要比更新数据库字段,或者使用事务队列存储消息,花长得多的时间完成。

  • 因为不可能将业务活动的持续时间限定为技术事务的范围,一个BPMN事务通常会生成多个ACID事务。

  • 因为一个BPMN事务可以生成多个ACID事务,就不再使用ACID特性。例如,考虑上面的流程例子。假设"book hotel(预订酒店)"与"charge credit card(信用卡付款)"操作在分开的ACID事务中处理。再假设"book hotel(预订酒店)"活动已经成功。这时,因为已经进行了预订酒店操作,而还没有进行信用卡扣款,就处在中间不一致状态(intermediary inconsistent state)。在ACID事务中,会顺序进行不同的操作,因此也处在中间不一致状态。在这里不一样的是,不一致状态在事务范围外可见。例如,如果通过外部预订服务进行预定,则使用该预订服务的其他部分将能看到酒店已被预订。这意味着,当时用业务事务时,完全不使用隔离参数(的确,当使用ACID事务时,我们通常也释放隔离,以保证高并发级别。但可以细粒度地控制,而中间不一致状态也只会存在与一小段时间内)。

  • BPMN业务事务也不使用传统方式回滚。因为它生成多个ACID事务,在BPMN事务取消时,部分ACID事务可能已经提交。这样它们没法回滚。

因为BPMN事务天生需要长时间运行,因此就需要区别处理缺乏隔离与回滚机制。在实际使用中,通常只能通过领域特定(domain specific)的方式解决这些问题:

  • 回滚通过补偿实现。如果在事务范围内抛出了取消事件,所有成功执行,并带有补偿处理器的活动,带来的影响,将被补偿。

  • 缺乏隔离通常使用特定领域的解决方案来处理。例如,在上面的例子里,在我们确定第一个客户可以付款前,一个酒店房间可能被第二个客户预定。这可能不满足业务预期,预订服务可能会选择允许一定量的超量预定。

  • 另外,由于事务可以由于意外而终止,预订服务需要处理这种情况,酒店房间已经预定,但从未付款(因为事务可能已经终止)。在这种情况下,预定服务可能选择这种策略,一个酒店房间有最大预留时间,若到时还未付款,则取消预订。

总结一下:ACID事务提供了这些问题的通用解决方案(回滚,隔离级别,与探索输出 heuristic outcomes),但仍然需要在实现业务事务时,为这些问题寻找特定领域的解决方案。

目前的限制:

  • BPMN规范要求,流程引擎响应底层事务协议提交的事务。例如在底层协议中发生了取消事件,则取消事务。作为可嵌入的引擎,Activiti当前不支持这点。(查看下面关于一致性的段落,了解其后果。)

基于ACID事务与乐观锁(optimistic concurrency)的一致性:BPMN事务在如下情况保证一致性:所有活动都成功完成;或若部分活动不能执行,则所有已完成活动都被补偿。两种方法都可以得到一致性状态。然而,认识到这一点很重要:Activiti中,BPMN事务的一致性模型,位与流程执行的一致性模型之上。Activiti以事务的方式执行流程。通过乐观锁标记处理并发。在Activiti中,BPMN的错误、取消与补偿事件,都建立在相同的ACID事务与乐观锁之上。例如,只有在实际到达时,取消结束事件才能触发补偿。如果由于服务任务抛出了未检查异常,导致其未实际到达;或者,由于底层ACID事务中的其他操作,将事务设置为rollback-only(回滚)状态,导致补偿处理器的操作不能提交;或者,当两个并行执行到达一个取消结束事件时,补偿会被两次触发,并由于乐观锁异常而失败。所有这些都是想说明,当在Activiti中实现BPMN事务时,与实施“普通”流程与子流程,需要遵守相同的规则。因此要有效地保证一致性,需要将乐观锁、事务执行模型纳入考虑范围,以实现流程。

图示 Graphical Notation

事务子流程,使用带有两层边框的嵌入式子流程表示。

bpmn.transaction.subprocess
XML表示 XML representation

事务子流程,在XML中通过transaction标签表示:

<transaction id="myTransaction" >
	...
</transaction>
示例 Example

下面是一个事务子流程的例子:

bpmn.transaction.subprocess.example.2

8.6.4. 调用活动(子流程) Call activity (subprocess)

描述 Description

BPMN 2.0区分一般的子流程,通常也称作嵌入式子流程,与调用活动,尽管它们看起来很像。从概念上说,两者都在流程执行到达该活动时,调用一个子流程。

区别在于,调用活动引用一个流程定义外部的流程,而subprocess嵌入在原有流程定义内。调用活动的主要使用场景,是它有一个可重复使用的流程定义,可以在多个其他流程定义中调用。

当流程执行到达call activity时,会创建一个新的执行,作为到达调用活动的执行的子执行。这个子执行之后用于执行子流程,潜在地创建了类似普通流程的并行子执行。父执行将等待子流程完成,之后沿原流程继续执行。

图示 Graphical Notation

调用过程,表现为带有粗边框(折叠与展开都是)的子流程。取决于建模工具,调用过程可以展开,但默认表现形式为折叠形式。

bpmn.collapsed.call.activity
XML表现 XML representation

调用活动是一个普通活动,需要有通过其key引用流程定义的calledElement。在实际使用中,这通常意味着在calledElement中使用流程的id

<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />

请注意子流程的流程定义在运行时解析。这意味着如果需要的话,子流程可以与调用流程分别部署。

传递变量 Passing variables

可以向子流程传递流程变量,反之亦然。数据将在子流程启动时复制到子流程,并在其结束时复制回主流程。

<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
  <extensionElements>
	  <activiti:in source="someVariableInMainProcess" target="nameOfVariableInSubProcess" />
	  <activiti:out source="someVariableInSubProcess" target="nameOfVariableInMainProcess" />
  </extensionElements>
</callActivity>

使用Activiti扩展,作为BPMN标准元素dataInputAssociationdataOutputAssociation的扩展。它们需要按照BPMN 2.0标准的方式声明流程变量。

也可以在这里使用表达式:

<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
	<extensionElements>
	  <activiti:in sourceExpression="${x+5}" target="y" />
	  <activiti:out source="${y+5}" target="z" />
	</extensionElements>
</callActivity>

因此最终 z = y+5 = x+5+5

示例 Example

下面的流程图展示了简单的订单处理。因为检查客户的信用额度在许多其他流程中都常见,因此将check credit step(检查信用额度步骤)建模为调用活动。

bpmn.call.activity.super.process

流程像是下面这样:

<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="receiveOrder" />

<manualTask id="receiveOrder" name="Receive Order" />
<sequenceFlow id="flow2" sourceRef="receiveOrder" targetRef="callCheckCreditProcess" />

<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
<sequenceFlow id="flow3" sourceRef="callCheckCreditProcess" targetRef="prepareAndShipTask" />

<userTask id="prepareAndShipTask" name="Prepare and Ship" />
<sequenceFlow id="flow4" sourceRef="prepareAndShipTask" targetRef="end" />

<endEvent id="end" />

子流程像是下面这样:

bpmn.call.activity.sub.process

与子流程的流程定义相比没什么特别。也可以不通过其他流程调用而使用。

8.7. 事务与并发 Transactions and Concurrency

8.7.1. 异步延续 Asynchronous Continuations

Activiti以事务方式执行流程,并可按照你的需求配置。让我们从Activiti一般如何为事务划分范围开始介绍。如果Activiti被触发(也就是说,启动流程,完成任务,为执行发送信号),Activiti将沿流程继续,直到到达每个执行路径的等待状态。更具体地说,它以深度优先方式搜索流程图,并在每个执行分支都到达等待状态时返回。等待状态是“之后”再执行的任务,意味着Activiti将当前执行持久化,并等待再次触发。触发可以来自外部来源,例如用户任务或消息接受任务,也可以来自Activiti自身,例如定时器事件。以下面的图片说明:

activiti.async.example.no.async

这是一个BPMN流程的片段,有一个用户任务,一个服务任务,与一个定时器事件。完成用户任务与验证地址(validate address)在同一个工作单元内,因此需要原子性地(atomically)成功或失败。这意味着如果服务任务抛出了异常,我们会想要回滚当前事务,以便执行返回到用户任务,而用户任务仍然存在于数据库中。这也是Activit的默认行为。在(1)中,应用或客户端线程完成任务。在相同的线程中,Activiti执行服务并继续,直到到达等待状态,在这个例子中,是定时器事件(2)。然后将控制权返回至调用者(3),同时提交事务(如果事务由Activiti开启)。

在有的情况下,这不是我们想要的。有时我们需要在流程中,自定义地控制事务边界,以便为工作的逻辑单元划分范围。这就需要使用异步延续。考虑下面的流程(片段):

activiti.async.example.async

这次我们完成用户任务,生成发票,并将发票发送给客户。这次发票的生成不再是同一个工作单元的一部分,因此我们不希望当发票生成失败时,回滚用户任务。因此我们希望Activiti做的,是完成用户任务(1),提交事务,并将控制权返回给调用程序。然后我们希望在后台线程中,异步地生成发票。这个后台线程就是Activiti作业执行器(事实上是一个线程池),它周期性地将作业保存至数据库。因此在幕后,当到达"generate invoice(生成发票)"任务时,会为Activiti创建“消息”作业,以继续流程,并将其持久化到数据库中。这个作业之后会被作业执行器选中并执行。我们也会为本地的作业执行器进行提示,告知其有新作业到来,以提升性能。

要使用这个特性,可以使用activiti:async="true"扩展。因此,服务任务会像是这样:

<serviceTask id="service1" name="Generate Invoice" activiti:class="my.custom.Delegate" activiti:async="true" />

可以为下列BPMN任务类型指定activiti:async:任务,服务任务,脚本任务,业务规则任务,发送任务,接收任务,用户任务,子流程,调用活动

对于用户任务,接收任务与其他等待状态来说,异步延续允许我们在一个独立的线程/事务中启动执行监听器。

8.7.2. 失败重试 Fail Retry

默认配置下,如果作业执行中有任何异常,Activiti将3次重试执行作业。对异步任务作业也是这样。有时需要更灵活的配置。可以配置两个参数:

  • 重试的次数

  • 重试的间隔

这两个参数可以通过activiti:failedJobRetryTimeCycle元素配置。这有一个简单的例子:

<serviceTask id="failingServiceTask" activiti:async="true" activiti:class="org.activiti.engine.test.jobexecutor.RetryFailingDelegate">
	<extensionElements>
		<activiti:failedJobRetryTimeCycle>R5/PT7M</activiti:failedJobRetryTimeCycle>
	</extensionElements>
</serviceTask>

时间周期表达式遵循ISO 8601标准,与定时器事件表达式一样。上面的例子,让作业执行器重试5次,并在每次重试前等待7分钟。

8.7.3. 排他作业 Exclusive Jobs

从Activiti 5.9开始,JobExecutor确保同一个流程实例的作业永远不会并发执行。为什么这样?

为什么排他作业? Why exclusive Jobs?

考虑下面的流程定义:

bpmn.why.exclusive.jobs

我们有一个并行网关,之后是三个服务任务,都使用异步延续执行。其结果是,数据库中添加了三个作业。当作业储存在数据库后,就可以使用JobExecutor处理。JobExecutor获取作业,并将其代理至工作线程的线程池,由它们实际执行作业。这意味着通过使用异步延续,可以将工作分发至线程池(在集群场景下,甚至是在集群中跨越多个线程池)。通常这都是好事。然而,也有固有问题:一致性。考虑服务任务后的并行合并。当服务任务的执行完成时,到达并行合并,并需要决定等待其他执行,还是需要继续向前。这意味着,对于每一个到达并行合并的分支,都需要选择继续执行,还是需要等待其他分支上的一个或多个其他执行。

为什么这是问题呢?这是因为服务任务配置为使用一步延续,有可能所有相应的作业都同时被作业执行器处理,并代理至不同的工作线程。结果是服务执行的事务,与到达并行合并的3个独立执行所在的事务,会发生重叠。如果这样,每一个独立事务都“看”不到,其他事物并发地到达了同样的并行合并,并因此判断需要等待其他事务。然而,如果每个事务都判断需要等待其他事务,在并行合并后不会有继续流程的事务,而流程实例也就会永远保持这个状态。

Activiti如何解决这个问题呢?Activiti使用乐观锁,基于数据进行判断,而数据可能不是当前值(因为其他事务可能在我们提交前修改了这个数据,我们确保会在每个事务中都增加同一个数据库记录行的版本号)。这样,无论哪个事务第一个提交,都将成功,而其他的会抛出乐观锁异常并失败。这解决了上面流程中讨论的问题:如果多个执行并发到达并行合并,它们都判断需要等待,增加其父执行(流程实例)的版本号,并尝试提交。无论哪个执行第一个提交,都可以成功提交,而其他的将会抛出乐观锁异常并失败。因为这些执行由作业触发,Activiti会在等待给定时间后,重试执行相同的作业,期望这一次通过这个同步的网关。

这是好的解决方案么?我们已经看到,乐观锁使Activiti能够避免不一致。它确保了我们不会“在合并网关卡住”,意味着:要么所有的执行都通过网关,要么数据库中的作业能确保可以重试通过它。然而,尽管这是一个持久化与一致性角度的完美解决方案,仍然不一定总是更高层次的理想行为:

  • Activiti只会为同一个作业,重试一个固定的最大次数(默认配置为'3’次)。在这之后,作业仍然保存在数据库中,但不会再重试。这意味着需要手动操作来触发作业。

  • 如果一个作业有非事务性的副作用,将不会由于事务失败而回滚。例如,如果"book concert tickets(预定音乐会门票)"服务与Activiti不在同一个事务中,则如果重试执行作业,将预定多张票。

什么是排他作业? What are exclusive jobs?

排他作业不能与同一个流程实例中的其他排他作业同时执行。考虑上面展示的流程:如果我们将服务任务都声明为排他的,则JobExecutor将确保相关的作业都不会并发执行。相反,它将确保不论何时从特定流程实例中获取了排他作业,都将从同一个流程实例中获取所有其他的排他作业,并将它们代理至同一个工作线程。这保证了作业的顺序执行。

如何启用这个特性?从Activiti 5.9起,排他作业成为默认配置。所有异步延续与定时器事件,都因此默认成为排他的。另外,如果希望作业成为非排他的,可以使用activiti:exclusive="false"配置。例如,下面的服务任务是异步,但非排他的。

<serviceTask id="service" activiti:expression="${myService.performBooking(hotel, dates)}" activiti:async="true" activiti:exclusive="false" />

这是好的解决方案么?有很多人问我们这是否是好的解决方案。他们的顾虑是,这将阻止并行“操作”,因此会有性能问题。再一次,需要考虑以下两点:

  • 如果你是专家,并且知道你在做什么(并理解“为什么排他作业?”章节的内容),可以关掉排他。除此之外,对大多数用户来说,异步延续与定时器能够正常工作才更直观。

  • 事实上不会有性能问题。只有在重负载下才会有性能问题。重负载意味着作业执行器的所有的工作线程都一直忙碌。对于排他作业,Activiti会简单的根据负载不同进行分配。排他作业意味着同一个流程实例的作业,都将在同一个线程中顺序执行。但是请想一下:有多于一个流程实例。而其他流程实例的作业将被代理至其他线程,并将并发执行。这意味着Activiti不会并发执行同一个流程实例的排他作业,但仍然并发执行多个实例。从总吞吐量角度来看,可以期望大多数场景下都将导致独立的实例更快地完成。此外,执行同一个流程实例中下一个作业所需的数据,将已经在执行集群节点中缓存。如果作业与节点没有这种关系,则数据可能需要重新从数据库中获取。

8.8. 流程启动认证 Process Initiation Authorization

默认情况下,任何人都可以启动已部署流程定义的新流程实例。流程启动认证功能可以定义用户与组,这样Web客户端可以选择性的限制能够启动新流程实例的用户。请注意Activiti引擎不会用任何方式验证认证定义。这个功能只是为了开发人员可以简化Web客户端认证规则的实现。语法与为用户任务指派用户的语法类似。可以使用<activiti:potentialStarter>标签,将用户或组指派为流程的潜在启动者。这里有一个例子:

<process id="potentialStarter">
  <extensionElements>
    <activiti:potentialStarter>
       <resourceAssignmentExpression>
         <formalExpression>group2, group(group3), user(user3)</formalExpression>
       </resourceAssignmentExpression>
    </activiti:potentialStarter>
  </extensionElements>

  <startEvent id="theStart"/>
  ...

在上面摘录的XML中,user(user3)直接引用用户user3,而group(group3)引用组group3。组没有默认标志。也可以使用<process>标签,名为<activiti:candidateStarterUsers>与<activiti:candidateStarterGroups>的属性。这里有一个例子:

<process id="potentialStarter" activiti:candidateStarterUsers="user1, user2"
                               activiti:candidateStarterGroups="group1">
      ...

这些属性可以同时使用。

在流程启动认证定义后,开发者可以使用下列方法获取该认证定义。这段代码获取可以由给定用户启动的流程定义列表:

processDefinitions = repositoryService.createProcessDefinitionQuery().startableByUser("userxxx").list();

也可以获取给定流程定义中,所有定义为潜在启动者的身份联系

identityLinks = repositoryService.getIdentityLinksForProcessDefinition("processDefinitionId");

下面的例子展示了如何获取能够启动给定流程的用户列表:

List<User> authorizedUsers =  identityService().createUserQuery().potentialStarter("processDefinitionId").list();

用完全相同的方法,可以获取配置为给定流程定义的潜在启动者的组列表:

List<Group> authorizedGroups =  identityService().createGroupQuery().potentialStarter("processDefinitionId").list();

8.9. 数据对象 Data objects

BPMN提供了将数据对象定义为流程或子流程元素的一部分的可能性。根据BPMN规范,可以包含复杂的XML结构,并可以从XSD定义中引入。作为Activiti支持的第一批数据对象,支持下列XSD类型:

<dataObject id="dObj1" name="StringTest" itemSubjectRef="xsd:string"/>
<dataObject id="dObj2" name="BooleanTest" itemSubjectRef="xsd:boolean"/>
<dataObject id="dObj3" name="DateTest" itemSubjectRef="xsd:datetime"/>
<dataObject id="dObj4" name="DoubleTest" itemSubjectRef="xsd:double"/>
<dataObject id="dObj5" name="IntegerTest" itemSubjectRef="xsd:int"/>
<dataObject id="dObj6" name="LongTest" itemSubjectRef="xsd:long"/>

数据对象的定义,将使用’name’属性值作为新变量的名字,自动转换为流程变量。另外,Activiti也提供了扩展元素,用于为变量设置默认值。下面的BPMN代码片段提供了示例:

<process id="dataObjectScope" name="Data Object Scope" isExecutable="true">
  <dataObject id="dObj123" name="StringTest123" itemSubjectRef="xsd:string">
    <extensionElements>
      <activiti:value>Testing123</activiti:value>
    </extensionElements>
  </dataObject>
  ...

9. 表单 Forms

Activiti提供了一个方便灵活的方法,为你的业务流程的人工步骤添加表单。我们支持两种使用表单的方式:表单参数的内置表单渲染,以及外部表单渲染。

9.1. 表单参数 Form properties

所有与业务流程相关的信息,要么包含在流程变量里,要么可以通过流程变量引用。Activiti支持将复杂的Java对象,以Serializable对象的方式存储为流程变量,而JPA实体或者整个XML文档将存储为String

启动流程与完成用户任务是人参与流程的地方。与人交流需要使用某些用户界面技术渲染表单。为了简化多用户界面技术,流程定义可以包含转换逻辑,将流程变量中的复杂的Java对象转换为参数Map<String,String>

然后任何用户界面技术,都可以使用暴露这些参数信息的Activiti API方法,在这些参数的基础上构建表单。这些参数可以提供对流程变量的专门(也更受限)视图。用于显示表单的参数是FormData的返回值。例如

StartFormData FormService.getStartFormData(String processDefinitionId)

或者

TaskFormdata FormService.getTaskFormData(String taskId)

默认情况下,内建表单引擎能够“看到”参数与流程变量。因此如果任务表单参数1对1匹配流程变量,则不需要专门声明。例如,对于下列声明:

<startEvent id="start" />

当执行到达startEvent时,所有流程变量都可用。然而

formService.getStartFormData(String processDefinitionId).getFormProperties()

将为空,因为并未指定映射。

在上面的例子中,所有提交的参数将被存储为流程变量。这意味着简单地在表单中添加输入框,就可以存储新变量。

参数从流程变量衍生出来,但不是必须存储为流程变量。例如,流程变量可以是类地址的JPA实体。而用户界面技术使用的StreetName表单参数,可以通过#{address.street}表达式连接。

类似的,表单中用户需要提交的参数可以存储为流程变量,也可以作为某个流程变量的嵌套参数,使用UEL值表达式,如#{address.street}

提交的参数的默认行为,是存储为流程变量,除非使用formProperty声明指定。

流程也可以在表单参数与流程变量之间进行转换。

例如:

<userTask id="task">
  <extensionElements>
    <activiti:formProperty id="room" />
    <activiti:formProperty id="duration" type="long"/>
    <activiti:formProperty id="speaker" variable="SpeakerName" writable="false" />
    <activiti:formProperty id="street" expression="#{address.street}" required="true" />
  </extensionElements>
</userTask>
  • room表单参数将作为String,映射为room流程变量

  • duration表单参数将作为java.lang.Long,映射为duration流程变量

  • speaker表单参数将被映射为SpeakerName流程变量。将只在TaskFormData对象中可用。若提交了speaker参数,将抛出ActivitiException。类似的,使用readable="false"属性,可以将参数从FormData中排除,但仍然可以在提交时处理。

  • street表单参数将作为String,映射为address流程变量的Java bean参数street。如果在提交时没有提供这个字段,required="true"将抛出异常。

也可以提供类型元数据,作为StartFormData FormService.getStartFormData(String processDefinitionId)TaskFormdata FormService.getTaskFormData(String taskId)方法返回的FormData的一部分

我们支持下列表单参数类型:

  • string (org.activiti.engine.impl.form.StringFormType

  • long (org.activiti.engine.impl.form.LongFormType)

  • enum (org.activiti.engine.impl.form.EnumFormType)

  • date (org.activiti.engine.impl.form.DateFormType)

  • boolean (org.activiti.engine.impl.form.BooleanFormType)

对每个声明的表单参数,下列FormProperty信息都可以通过List<FormProperty> formService.getStartFormData(String processDefinitionId).getFormProperties()List<FormProperty> formService.getTaskFormData(String taskId).getFormProperties()方法获取

public interface FormProperty {
  /**
   * 在{@link FormService#submitStartFormData(String, java.util.Map)}
   * 或{@link FormService#submitTaskFormData(String, java.util.Map)}
   * 中提交参数时使用的key
   *
   * the key used to submit the property in {@link FormService#submitStartFormData(String, java.util.Map)}
   * or {@link FormService#submitTaskFormData(String, java.util.Map)} */
  String getId();

  /** 显示标签 the display label */
  String getName();

  /** 在本接口中定义的类型,例如{@link #TYPE_STRING}
   * one of the types defined in this interface like e.g. {@link #TYPE_STRING} */
  FormType getType();

  /** 这个参数需要显示的可选项
   * optional value that should be used to display in this property */
  String getValue();

  /** 这个参数是否需要读取用于在表单中显示,并可通过
   * {@link FormService#getStartFormData(String)}
   * 与{@link FormService#getTaskFormData(String)}
   * 方法访问。
   *
   * is this property read to be displayed in the form and made accessible with the methods
   * {@link FormService#getStartFormData(String)} and {@link FormService#getTaskFormData(String)}. */
  boolean isReadable();

  /** 用户提交表单时是否可以包含这个参数? is this property expected when a user submits the form? */
  boolean isWritable();

  /** 输入框中是否必填这个参数 is this property a required input field */
  boolean isRequired();
}

例如:

<startEvent id="start">
  <extensionElements>
    <activiti:formProperty id="speaker"
      name="Speaker"
      variable="SpeakerName"
      type="string" />

    <activiti:formProperty id="start"
      type="date"
      datePattern="dd-MMM-yyyy" />

    <activiti:formProperty id="direction" type="enum">
      <activiti:value id="left" name="Go Left" />
      <activiti:value id="right" name="Go Right" />
      <activiti:value id="up" name="Go Up" />
      <activiti:value id="down" name="Go Down" />
    </activiti:formProperty>

  </extensionElements>
</startEvent>

所有这些信息都可以通过API获取。类型名可以通过formProperty.getType().getName()获取,日期格式可以通过formProperty.getType().getInformation("datePattern")获取,枚举值可以通过formProperty.getType().getInformation("values")获取。

Activiti Explorer支持表单参数,并会按照表单定义渲染表单。下面的XML代码片段

<startEvent>
  <extensionElements>
    <activiti:formProperty id="numberOfDays" name="Number of days" value="${numberOfDays}" type="long" required="true"/>
    <activiti:formProperty id="startDate" name="First day of holiday (dd-MM-yyy)" value="${startDate}" datePattern="dd-MM-yyyy hh:mm" type="date" required="true" />
    <activiti:formProperty id="vacationMotivation" name="Motivation" value="${vacationMotivation}" type="string" />
  </extensionElements>
</userTask>

当使用Activiti Explorer时,将会渲染为流程启动表单

forms.explorer

9.2. 外部表单渲染 External form rendering

API也支持使用在Activiti引擎之外渲染的,你自己的任务表单。下面的步骤解释了在自行渲染任务表单时,可以使用的钩子。

本质上,渲染表单所需的所有数据,都组装在这两个方法之一中:StartFormData FormService.getStartFormData(String processDefinitionId)TaskFormdata FormService.getTaskFormData(String taskId)

提交表单参数可以通过ProcessInstance FormService.submitStartFormData(String processDefinitionId, Map<String,String> properties)void FormService.submitTaskFormData(String taskId, Map<String,String> properties)完成。

要了解表单参数如何映射为流程变量,查看表单参数 Form properties

可以将任何表单模板资源,放在部署的业务存档中(如果希望将它们按版本与流程存储在一起)。作为部署中的资源,可以使用String ProcessDefinition.getDeploymentId()与++InputStream RepositoryService.getResourceAsStream(String deploymentId, String resourceName);+获取。这就是你的模板定义文件,可以用于在你的应用中渲染/显示表单。

除了任务表单,也可以为任何目的,使用访问部署资源的能力。

<userTask activiti:formKey="…​"属性,由API通过String FormService.getStartFormData(String processDefinitionId).getFormKey()String FormService.getTaskFormData(String taskId).getFormKey()暴露。可以用它保存部署中模板的全名(如org/activiti/example/form/my-custom-form.xml),但并非必须。例如,也可以在表单参数中保存普通的key,并用算法或变换得到实际需要使用的模板。在你需要使用不同的用户界面技术,渲染不同的表单时很有用。例如,一个表单在普通屏幕尺寸的Web应用中使用,另一个表单在手机小屏幕中使用,甚至可以为IM表单或邮件表单提供模板。

10. JPA(Java Persistence API Java持久化API)

可以使用JPA实体作为流程变量,这样可以:

  • 基于流程变量更新已有JPA实体。流程变量可以在用户任务的表单中填写,或者通过服务任务生成。

  • 重用已有的领域模型,而不需要写专门的服务用于读取与更新实体值。

  • 基于已有实体做决策(网关)。

  • …​

10.1. 需求 Requirements

只能支持完全满足下列条件的实体:

  • 实体需要使用JPA注解配置,字段与参数访问器都支持。也可以使用映射的父类。

  • 实体需要有使用@Id注解的主键,不支持复合主键(@EmbeddedId@IdClass)。Id字段/参数可以是任何JPA规范支持的类型:原生类型与其包装器(除了boolean)、StringBigIntegerBigDecimaljava.util.Datejava.sql.Date

10.2. 配置 Configuration

要使用JPA实体,引擎必须引用EntityManagerFactory。可以通过配置引用,或者提供持久化单元名(Persistence Unit Name)来实现。用作变量的JPA实体将将被自动检测,并会按情况处理。

下面的示例配置使用jpaPersistenceUnitName:

<bean id="processEngineConfiguration"
  class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">

<!-- Database configurations -->
<property name="databaseSchemaUpdate" value="true" />
<property name="jdbcUrl" value="jdbc:h2:mem:JpaVariableTest;DB_CLOSE_DELAY=1000" />

<property name="jpaPersistenceUnitName" value="activiti-jpa-pu" />
<property name="jpaHandleTransaction" value="true" />
<property name="jpaCloseEntityManager" value="true" />

<!-- job executor configurations -->
<property name="jobExecutorActivate" value="false" />

<!-- mail server configurations -->
<property name="mailServerPort" value="5025" />
</bean>

下面的示例配置提供了我们自己定义的EntityManagerFactory(在这个例子里,是一个open-jpa实体管理器)。请注意这段代码只包含了与本例相关的bean,省略了其他的。带有open-jpa实体管理器的完整的可用示例,可以在activiti-spring-examples (/activiti-spring/src/test/java/org/activiti/spring/test/jpa/JPASpringTest.java)中找到。

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="persistenceUnitManager" ref="pum"/>
  <property name="jpaVendorAdapter">
    <bean class="org.springframework.orm.jpa.vendor.OpenJpaVendorAdapter">
      <property name="databasePlatform" value="org.apache.openjpa.jdbc.sql.H2Dictionary" />
    </bean>
  </property>
</bean>

<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
  <property name="dataSource" ref="dataSource" />
  <property name="transactionManager" ref="transactionManager" />
  <property name="databaseSchemaUpdate" value="true" />
  <property name="jpaEntityManagerFactory" ref="entityManagerFactory" />
  <property name="jpaHandleTransaction" value="true" />
  <property name="jpaCloseEntityManager" value="true" />
  <property name="jobExecutorActivate" value="false" />
</bean>

也可以在编程构建引擎时,使用相同的配置,例如:

ProcessEngine processEngine = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResourceDefault()
.setJpaPersistenceUnitName("activiti-pu")
.buildProcessEngine();

配置参数:

  • jpaPersistenceUnitName:要使用的持久化单元的名字。(要确保该持久化单元在classpath中可用。根据规范,默认位置为/META-INF/persistence.xml)。jpaEntityManagerFactoryjpaPersistenceUnitName二选一。

  • jpaEntityManagerFactory:对实现了javax.persistence.EntityManagerFactory的bean的引用,将用于载入实体,并刷入更新。jpaEntityManagerFactoryjpaPersistenceUnitName二选一。

  • jpaHandleTransaction:标示引擎是否需要启动事务,并在使用EntityManager实例后提交/回滚。当使用Java Transaction API (JTA)时,设置为false。

  • jpaCloseEntityManager:标示引擎是否需要关闭其从EntityManagerFactory获取的EntityManager实例。当EntityManager由容器管理时(例如,使用扩展持久化上下文 Extended Persistence Context时,不支持将范围限制为单一事务)设置为false。

10.3. 使用 Usage

10.3.1. 简单示例 Simple Example

可以在Activiti源代码的JPAVariableTest中找到使用JPA变量的例子。我们会一步一步解释JPAVariableTest.testUpdateJPAEntityValues

首先,基于META-INF/persistence.xml,为我们的持久化单元创建一个EntityManagerFactory。它包含了需要包含在持久化单元内的类,以及一些厂商特定配置。

在这个测试里我们使用简单实体,它有一个id以及一个String值参数,用于持久化。在运行测试前,先创建一个实体并保存。

@Entity(name = "JPA_ENTITY_FIELD")
public class FieldAccessJPAEntity {

  @Id
  @Column(name = "ID_")
  private Long id;

  private String value;

  public FieldAccessJPAEntity() {
    // JPA需要的空构造方法 Empty constructor needed for JPA
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getValue() {
    return value;
  }

  public void setValue(String value) {
    this.value = value;
  }
}

启动一个新的流程实例,将这个实体加入变量。与其他变量一样,它们都会在引擎中持久化存储。当下一次请求这个变量时,将会根据存储的类与Id,从EntityManager载入。

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("entityToUpdate", entityToUpdate);

ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("UpdateJPAValuesProcess", variables);

我们流程定义的第一个节点,是一个服务任务,将调用entityToUpdate上的setValue方法。它将解析为我们之前启动流程实例时设置的JPA变量,并使用当前引擎的上下文关联的++EntityManager+载入。

<serviceTask id='theTask' name='updateJPAEntityTask'
  activiti:expression="${entityToUpdate.setValue('updatedValue')}" />

当服务任务完成时,流程实例在流程定义中定义的用户任务处等待,让我们可以查看流程实例。在这时,EntityManager已经刷入,对实体的修改也已经存入数据库。当我们使用entityToUpdate变量的值时,将重新载入,我们会得到value参数设置为updatedValue的实体。

// 流程'UpdateJPAValuesProcess'中的服务任务应已设置了entityToUpdate的value。
// Servicetask in process 'UpdateJPAValuesProcess' should have set value on entityToUpdate.
Object updatedEntity = runtimeService.getVariable(processInstance.getId(), "entityToUpdate");
assertTrue(updatedEntity instanceof FieldAccessJPAEntity);
assertEquals("updatedValue", ((FieldAccessJPAEntity)updatedEntity).getValue());

10.3.2. 查询JPA流程变量 Query JPA process variables

可以查询以特定JPA实体作为变量值的流程实例执行请注意对于ProcessInstanceQueryExecutionQuery的JPA实体查询,只支持variableValueEquals(name, entity)。而variableValueNotEqualsvariableValueGreaterThanvariableValueGreaterThanOrEqualvariableValueLessThanvariableValueLessThanOrEqual方法都不支持,并会在值传递为JPA实体时,抛出ActivitiException

 ProcessInstance result = runtimeService.createProcessInstanceQuery()
    .variableValueEquals("entityToQuery", entityToQuery).singleResult();

10.3.3. 使用Spring bean与JPA的高级示例 Advanced example using Spring beans and JPA

可以在activiti-spring-examples中找到更高级的例子,JPASpringTest。它描述了下属简单用例:

  • 一个已有的Spring bean,使用已有的JPA实体,用于存储贷款申请。

  • 使用Activiti,可以通过该bean获取该实体,并将其用作流程中的变量。流程定义如下步骤:

    • 创建新的LoanRequest(贷款申请)的服务任务,使用已有的LoanRequestBean,并使用启动流程时接收的变量(例如,从启动表单)。创建的实体作为变量存储,使用activiti:resultVariable将表达式结果存储为变量。

    • 让经理可以审核申请并批准/驳回的用户任务,该选择将会存储为boolean变量approvedByManager

    • 更新贷款申请实体的服务任务,以便其可以与流程同步。

    • 依据approved实体参数的值,使用一个排他网关,选择下一步采用哪条路径:若申请被批准,结束流程;否则,产生一个额外任务(Send rejection letter 发送拒信),以便客户可以收到拒信得到通知。

请注意这个流程不包含任何表单,因为它只用于单元测试。

jpa.spring.example.process
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="taskAssigneeExample"
  xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:activiti="http://activiti.org/bpmn"
  targetNamespace="org.activiti.examples">

  <process id="LoanRequestProcess" name="Process creating and handling loan request">
    <startEvent id='theStart' />
    <sequenceFlow id='flow1' sourceRef='theStart' targetRef='createLoanRequest' />

    <serviceTask id='createLoanRequest' name='Create loan request'
      activiti:expression="${loanRequestBean.newLoanRequest(customerName, amount)}"
      activiti:resultVariable="loanRequest"/>
    <sequenceFlow id='flow2' sourceRef='createLoanRequest' targetRef='approveTask' />

    <userTask id="approveTask" name="Approve request" />
    <sequenceFlow id='flow3' sourceRef='approveTask' targetRef='approveOrDissaprove' />

    <serviceTask id='approveOrDissaprove' name='Store decision'
      activiti:expression="${loanRequest.setApproved(approvedByManager)}" />
    <sequenceFlow id='flow4' sourceRef='approveOrDissaprove' targetRef='exclusiveGw' />

    <exclusiveGateway id="exclusiveGw" name="Exclusive Gateway approval" />
    <sequenceFlow id="endFlow1" sourceRef="exclusiveGw" targetRef="theEnd">
      <conditionExpression xsi:type="tFormalExpression">${loanRequest.approved}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="endFlow2" sourceRef="exclusiveGw" targetRef="sendRejectionLetter">
      <conditionExpression xsi:type="tFormalExpression">${!loanRequest.approved}</conditionExpression>
    </sequenceFlow>

    <userTask id="sendRejectionLetter" name="Send rejection letter" />
    <sequenceFlow id='flow5' sourceRef='sendRejectionLetter' targetRef='theOtherEnd' />

    <endEvent id='theEnd' />
    <endEvent id='theOtherEnd' />
  </process>

</definitions>

尽管上面的例子很简单,但也展示了组合使用JPA与Spring以及带参数方法表达式的威力。这个流程完全不需要自定义Java代码(当然除了Spring bean),大幅加速了开发。

11. 历史 History

历史是捕获流程执行过程中发生的事情,并将其永久存储的组件。与运行时数据相反,历史数据在流程实例完成以后,仍会保持在数据库中。

有5个历史实体:

  • HistoricProcessInstance保存当前与已结束流程实例的信息。

  • HistoricVariableInstance保存流程变量或任务变量的最新值。

  • HistoricActivityInstance保存活动(流程中的节点)的单一执行信息。

  • HistoricTaskInstance保存当前与过去(完成并删除的)任务实例的信息。

  • HistoricDetail保存与历史流程实例,活动实例或任务实例有关的多种信息。

因为数据库为过去与当前进行中的实例都保存历史实体,因此你可能希望查询这些表,以减少访问运行时流程实例数据,并提高运行时执行性能。

之后,这些信息将在Activiti Explorer中暴露。并且,也将用于生成报告。

11.1. 查询历史 Querying history

可以使用API查询全部5种历史实体,HistoryService暴露的createHistoricProcessInstanceQuery()createHistoricVariableInstanceQuery()createHistoricActivityInstanceQuery()createHistoricDetailQuery()createHistoricTaskInstanceQuery()方法。

下面是一些例子,展示了历史查询API的一些用法。关于各用法的全部描述可以在javadoc中找到,在org.activiti.engine.history包中。

11.1.1. 历史流程实例查询 HistoricProcessInstanceQuery

取得所有流程中,前10个花费最多时间完成(最长持续时间)的,定义为’XXX',已完成的HistoricProcessInstances

historyService.createHistoricProcessInstanceQuery()
  .finished()
  .processDefinitionId("XXX")
  .orderByProcessInstanceDuration().desc()
  .listPage(0, 10);

11.1.2. 历史变量实例查询 HistoricVariableInstanceQuery

在已完成的,id为’XXX’的流程实例中,取得所有HistoricVariableInstances,以变量名排序。

historyService.createHistoricVariableInstanceQuery()
  .processInstanceId("XXX")
  .orderByVariableName.desc()
  .list();

11.1.3. 历史活动实例查询 HistoricActivityInstanceQuery

取得最新的,服务任务类型的,已完成的,流程定义的id为XXX的,HistoricActivityInstance

historyService.createHistoricActivityInstanceQuery()
  .activityType("serviceTask")
  .processDefinitionId("XXX")
  .finished()
  .orderByHistoricActivityInstanceEndTime().desc()
  .listPage(0, 1);

11.1.4. 历史详情查询 HistoricDetailQuery

下一个例子,取得id为123的流程中,所有变量的更新记录。这个查询只会返回HistoricVariableUpdate。请注意有可能某个变量名有多个HistoricVariableUpdate实体,代表流程中的每一次变量更新。可以使用orderByTime(变量更新的时间)或orderByVariableRevision(运行时变量更新时的版本号),按其发生顺序排序。

historyService.createHistoricDetailQuery()
  .variableUpdates()
  .processInstanceId("123")
  .orderByVariableName().asc()
  .list()

这个例子,取得流程id为"123"的,任何任务中或启动时提交的,所有表单参数。这个查询只返回HistoricFormProperties

historyService.createHistoricDetailQuery()
  .formProperties()
  .processInstanceId("123")
  .orderByVariableName().asc()
  .list()

最后一个例子,取得id为"123"的任务进行的所有变量更新操作。将返回该任务设置的所有变量(任务局部变量)的HistoricVariableUpdates,而不会返回流程实例中设置的。

historyService.createHistoricDetailQuery()
  .variableUpdates()
  .taskId("123")
  .orderByVariableName().asc()
  .list()

可以在TaskListener中使用TaskServiceDelegateTask设置任务局部变量:

taskService.setVariableLocal("123", "myVariable", "Variable value");
public void notify(DelegateTask delegateTask) {
  delegateTask.setVariableLocal("myVariable", "Variable value");
}

11.1.5. 历史任务示例查询 HistoricTaskInstanceQuery

取得所有任务中,前10个花费最多时间完成(最长持续时间)的,已完成的HistoricTaskInstance

historyService.createHistoricTaskInstanceQuery()
  .finished()
  .orderByHistoricTaskInstanceDuration().desc()
  .listPage(0, 10);

取得删除原因包含"invalid",最后一次指派给’kermit’用户的HistoricTaskInstance

historyService.createHistoricTaskInstanceQuery()
  .finished()
  .taskDeleteReasonLike("%invalid%")
  .taskAssignee("kermit")
  .listPage(0, 10);

11.2. 历史配置 History configuration

可以使用org.activiti.engine.impl.history.HistoryLevel枚举(或在5.11版本前,ProcessEngineConfiguration中定义的HISTORY常量),以编程方式配置历史级别:

ProcessEngine processEngine = ProcessEngineConfiguration
  .createProcessEngineConfigurationFromResourceDefault()
  .setHistory(HistoryLevel.AUDIT.getKey())
  .buildProcessEngine();

也可以在activiti.cfg.xml或Spring上下文中配置级别:

<bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
  <property name="history" value="audit" />
  ...
</bean>

可以配置下列历史级别:

  • none(无):跳过所有历史存档。对于运行时流程执行来说,是性能最高的配置,但是不会保存任何历史信息。

  • activity(活动):存档所有流程实例与活动实例。在流程实例结束时,顶级流程实例变量的最新值,将被复制为历史流程实例。不会存档细节。

  • audit(审计):默认级别。将存档所有流程实例,活动实例,并保持变量值以及所有提交的表单参数持续同步,以保证表单的所有用户操作都可追踪、可审计。

  • full(完全):历史存档的最高级别,因此也最慢。这个级别存储所有audit级别存储的信息,加上所有其他可用细节,主要是流程变量的更新。

在Activiti 5.11版本以前,历史级别保存在数据库中(ACT_GE_PROPERTY 表,参数名为historyLevel)。从5.11开始,这个值不再使用,并从数据库中忽略/删除。现在历史可以在2个引擎的启动间切换,而不会由于前一个引擎启动修改了级别,而抛出异常。

11.3. 审计目的历史 History for audit purposes

如果至少配置audit级别,则通过FormService.submitStartFormData(String processDefinitionId, Map<String, String> properties)FormService.submitTaskFormData(String taskId, Map<String, String> properties)方法提交的所有参数都将被记录。

表单参数可以通过查询API,像这样读取:

historyService
      .createHistoricDetailQuery()
      .formProperties()
      ...
      .list();

在这个情况下,只会返回HistoricFormProperty类型的历史详情。

如果在调用提交方法前,使用IdentityService.setAuthenticatedUserId(String)设置了认证用户,则该提交了表单的认证用户可以在历史中访问。对于启动表单使用HistoricProcessInstance.getStartUserId(),对于任务表单使用HistoricActivityInstance.getAssignee()

12. Eclipse Designer

Activiti提供了名为Activiti Eclipse Designer的Eclipse插件,可以用于图形化地建模、测试与部署BPMN 2.0流程。

12.1. 安装 Installation

下面的安装指导在Eclipse Kepler与Indigo进行了验证。请注意支持Eclipse Helios。

选择Help → Install New Software。在下图面板中,点击Add按钮,并填写下列字段:

  • *Name:*Activiti BPMN 2.0 designer

  • *Location:*http://activiti.org/designer/update/

designer.add.update.site

确保"Contact all updates sites.."复选框已选中,因为这样Eclipse就可以下载需要的所有插件。

12.2. Activiti Designer编辑器功能 Activiti Designer editor features

  • 创建Activiti项目与流程图(diagram)。

designer.create.activiti.project
  • Activiti Designer在创建新的Activiti流程图时,会创建一个.bpmn文件。当使用Activiti Diagram Editor(Activiti流程图编辑器)视图打开时,将提供图形化的模型画布与画板。这个文件也可以使用XML编辑器打开,将显示流程定义的BPMN 2.0 XML元素。因此Activiti Designer只用一个文件,既是流程图,也是BPMN 2.0 XML。请注意在Activiti 5.9版本中,还不支持使用.bpmn扩展名作为流程定义的部署包。因此Activiti Designer的"create deployment artifacts(创建部署包)"功能,将生成一个BAR文件,与一个包含.bpmn文件内容的.bpmn20.xml文件。也可以方便的自己重命名。请注意,也可以使用Activiti Diagram Editor打开.bpmn20.xml文件。

designer.bpmn.file
  • 可以将BPMN 2.0 XML文件导入Activiti Designer,会自动创建流程图。只需要将BPMN 2.0 XML文件复制到项目中,并使用Activiti Diagram Editor视图打开它。Activiti Designer使用文件中的BPMN DI信息来创建流程图。如果BPMN 2.0 XML文件中没有BPMN DI信息,则不会创建流程图。

designer.open.importedfile
  • 要进行部署,可以使用Activiti Designer创建BAR文件,或JAR文件。在包浏览器中的Activiti项目上点击右键,在弹出菜单的下方选择Create deployment artifacts(创建部署包)选项。要了解关于Designer部署功能的更多信息,请查看部署章节。

designer.create.deployment
  • 生成单元测试(在包浏览器中的BPMN 2.0 XML文件上点击右键,选择generate unit test 生成单元测试)。将创建一个单元测试及运行在嵌入式H2数据库上的Activiti配置。这样就可以运行单元测试,来测试你的流程定义。

designer.unittest.generate
  • Activiti项目可以生成为Maven项目。要配置依赖,需要运行mvn eclipse:eclipse。请注意在流程设计时,不需要Maven依赖。只在运行单元测试时才需要依赖。

designer.project.maven

12.3. Activiti Designer BPMN功能 Activiti Designer BPMN features

  • 支持空启动事件,错误启动事件,定时器启动事件,空结束事件,错误结束事件,顺序流,并行网关,排他网关,包容网关,事件网关,嵌入式子流程,事件子流程,调用活动,泳池,泳道,脚本任务,用户任务,服务任务,邮件任务,手动任务,业务规则任务,接收任务,定时器边界事件,错误边界事件,信号边界事件,定时器捕获事件,信号捕获事件,信号抛出事件,空抛出事件,与四个Alfresco特有元素(用户,脚本,邮件任务与启动事件)。

designer.model.process
  • 可以在元素上悬停并选择新的任务类型,快速改变任务的类型。

designer.model.quick.change
  • 可以在元素上悬停并选择新的元素类型,快速添加新的元素。

designer.model.quick.new
  • Java服务任务支持Java类,表达式或代理表达式配置。另外也可以配置字段扩展。

designer.servicetask.property
  • 支持泳池与泳道。但因为Activiti将不同的泳池认作不同的流程定义,因此最好只使用一个泳池。如果使用多个泳池,要小心不要在泳池间画顺序流,否则会在Activiti引擎中部署流程时发生错误。可以在一个泳池中添加任意多的泳道。

designer.model.poolandlanes
  • 可以通过填写name参数,为顺序流添加标签。可以决定放置标签的位置,位置将保存为BPMN 2.0 XML DI信息的一部分。

designer.model.labels
  • 支持事件子流程。

designer.model.eventsubprocess
  • 支持展开嵌入式子流程。也可以在一个嵌入式子流程中加入另一个嵌入式子流程。

designer.embeddedprocess.canvas
  • 支持在任务与嵌入式子流程上的定时器边界事件。然而,在Activiti Designer中,在用户任务或嵌入式子流程上使用定时器边界事件最合理。

designer.timerboundary.canvas
  • 支持额外的Activiti扩展,例如邮件任务,用户任务的候选人配置,或脚本任务配置。

designer.mailtask.property
  • 支持Activiti执行与任务监听器。也可以为执行监听器添加字段扩展。

designer.listener.configuration
  • 支持在顺序流上添加条件。

designer.sequence.condition

12.4. Activiti Designer部署功能 Activiti Designer deployment features

在Activiti引擎上部署流程定义与任务表单并不困难。需要有一个包含有流程定义BPMN 2.0 XML文件的BAR文件,与可选的用于在Activiti Explorer中查看的任务表单和流程图片。在Activiti Designer中,创建BAR文件十分简单。在完成流程实现后,只要在包浏览器中的Activiti项目上点击右键,在弹出菜单下方选择Create deployment artifacts(创建部署包)选项。

designer.create.deployment

然后就会创建一个部署目录,包含BAR文件,与可能的JAR文件。其中JAR文件包含Activiti项目中的Java类。

designer.deployment.dir

这样就可以在Activiti Explorer的部署页签中,将这个文件上传至Activiti引擎。

如果项目包含Java类,部署时要多做一些工作。在这种情况下,Activiti Designer的Create deployment artifacts(创建部署包)操作也会创建包含编译后类的JAR文件。这个JAR文件必须部署在Activiti Tomcat安装目录的activiti-XXX/WEB-INF/lib目录下。这将为Activiti引擎的classpath添加这些类。

12.5. 扩展Activiti Designer (Extending Activiti Designer)

可以扩展Activiti Designer提供的默认功能。这段文档介绍了可以使用哪些扩展,如何使用,并提供了一些例子。在建模业务流程时,如果默认功能不能满足需要,需要额外的功能,或有领域专门需求的时候,扩展Activiti Designer就很有用。扩展Activiti Designer分为两个不同领域,扩展画板与扩展输出格式。两种方式都需要专门的方法,与不同的技术知识。

扩展Activiti Designer需要专业知识,更确切地说,Java编程的知识。取决于你想要创建的扩展类型,你可能需要熟悉Maven,Eclipse,OSGi,Eclipse扩展与SWT。

12.5.1. 自定义画板 Customizing the palette

可以自定义为用户建模流程提供的画板。画板是形状的集合,显示在画布的右侧,可以将形状拖放至画布中的流程图上。在默认画板中可以看到,默认形状进行了分组(被称为“抽屉 drawer”),如事件,网关,等等。Activiti Designer提供了两种选择,用于自定义画板中的抽屉与形状:

  • 将你自己的形状/节点添加到已有或新建的抽屉

  • 禁用Activiti Designer提供的部分或全部BPMN 2.0默认形状,除了连线与选择工具

要自定义画板,需要创建一个JAR文件,并加入Activiti Designer安装目录(后面介绍如何做)。这个JAR文件叫做扩展(extension)。通过编写扩展中包含的类,就能让Activiti Designer知道你需要自定义什么。要做到这个,你的类需要实现特定的接口。有一个集成类库,包含这些接口以及需要加入classpath的用于扩展的基类。

可以在下列地方找到代码示例:Activiti源码的projects/designer目录下的examples/money-tasks目录。

可以使用你喜欢的任何工具设置项目,并使用你选择的构建工具构建JAR。在下面的介绍中,假设使用Eclipse Kepler或Indigo,并使用Maven(3.x)作为构建工具。但任何设置都可以创建相同的结果。

设置扩展 Extension setup (Eclipse/Maven)

下载并解压缩Eclipse(应该可以使用最新版本),与Apache Maven近期的版本(3.x)。如果使用2.x版本的Maven,可能会在构建项目时遇到错误,因此请确保版本是最新的。我们假设你已经熟悉Eclipse中的基本功能以及Java编辑器。可以使用Eclipse的Maven功能,或直接从命令行运行Maven命令。

在Eclipse中创建一个新项目。可以是通用类型项目。在项目的根路径创建一个pom.xml文件,以包含Maven项目配置。同时创建src/main/javasrc/main/resources目录,这是Maven约定的Java源文件与资源文件目录。打开pom.xml文件并添加下列行:

<project
  xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>org.acme</groupId>
  <artifactId>money-tasks</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <name>Acme Corporation Money Tasks</name>
...
</project>

可以看到,这只是一个基础的pom.xml文件,为项目定义了一个groupIdartifactIdversion。我们会创建一个定制项,包含一个money业务的自定义节点。

pom.xml文件中添加这些依赖,将集成库添加至项目依赖:

<dependencies>
  <dependency>
    <groupId>org.activiti.designer</groupId>
    <artifactId>org.activiti.designer.integration</artifactId>
    <version>5.12.0</version> <!-- Use the current Activiti Designer version -->
    <scope>compile</scope>
  </dependency>
</dependencies>
...
<repositories>
  <repository>
      <id>Activiti</id>
      <url>https://maven.alfresco.com/nexus/content/groups/public/</url>
   </repository>
</repositories>

最后,在pom.xml文件中,添加maven-compiler-plugin配置,设置Java源码级别为1.5以上(参见下面的代码片段)。要使用注解需要这个配置。也可以为Maven包含用于生成JAR的MANIFEST.MF文件。这不是必须的,但可以在这个manifest中使用特定参数,为你的扩展提供名字(这个名字可以在设计器的特定位置显示,主要用于在设计器中有多个扩展时使用)。如果想要这么做,在pom.xml中添加下列代码片段:

<build>
  <plugins>
        <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <source>1.5</source>
        <target>1.5</target>
        <showDeprecation>true</showDeprecation>
        <showWarnings>true</showWarnings>
        <optimize>true</optimize>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>2.3.1</version>
      <configuration>
        <archive>
          <index>true</index>
          <manifest>
            <addClasspath>false</addClasspath>
            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
          </manifest>
          <manifestEntries>
            <ActivitiDesigner-Extension-Name>Acme Money</ActivitiDesigner-Extension-Name>
          </manifestEntries>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

扩展的名字使用ActivitiDesigner-Extension-Name参数描述。现在只剩下让Eclipse按照pom.xml的指导设置项目。因此打开命令行,并转到Eclipse工作空间中你项目的根目录。然后执行下列Maven命令:

mvn eclipse:eclipse

等待构建完成。刷新项目(使用项目上下文菜单(右键点击),并选择Refresh 刷新)。现在Eclipse项目中应该已经建立了src/main/javasrc/main/resources源码目录。

当然也可以使用m2eclipse插件,并简单地在项目的上下文菜单(右键点击)中启用Maven依赖管理。然后在项目的上下文菜单中选择Maven > Update project configuration(更新项目配置)。这也将配置源代码目录。

这就完成了配置。现在可以开始为Activiti Designer创建自定义项了!

在Activiti Designer中应用你的扩展 Applying your extension to Activiti Designer

你也许想知道如何将你的扩展加入Activiti Designer,以便应用你的自定义项。需要这些步骤:

  • 创建扩展JAR(例如,使用Maven构建时,在项目中运行mvn install)后,需要将扩展传递至Activiti Designer安装的计算机;

  • 将扩展存储在硬盘上,方便记忆的位置。请注意:必须保存在Activiti Designer的Eclipse工作空间之外——将扩展保存在工作空间内,会导致弹出错误消息弹框,扩展将不可用;

  • 启动Activiti Designer,从菜单中,选择Window > Preferences

  • 在Preferences界面,键入user作为关键字。将可以看到在Eclipse中Java段落内,User Libraries的选项。

designer.preferences.userlibraries
  • 选择User Libraries选项,将在右侧显示树形界面,可以添加库。应该可以看到一个默认组,可以用于添加Activiti Designer的扩展(根据Eclipse安装不同,也可能看到几个其他的)。

designer.preferences.userlibraries.activiti.empty
  • 选择Activiti Designer Extensions组,并点击Add JARs…​按钮。跳转至存储扩展的目录,并选择希望添加的扩展文件。完成后,配置界面会将扩展作为Activiti Designer Extensions组的成员进行显示,像下面这样。

designer.preferences.userlibraries.activiti.moneytasks
  • 点击OK按钮保存并关闭配置对话框。Activiti Designer Extensions会自动添加至你创建的新Activiti项目。可以在导航条或包管理器的项目树下的用户库条目中看到。如果工作空间中已经有了Activiti项目,也可以看到组中显示了新扩展,像下面这样。

designer.userlibraries.project

打开的流程图将在其画板上显示新扩展的图形(或者禁用部分图形,取决于扩展中的配置)。如果已经打开了流程图,关闭并重新打开就能在画板上看到变化。

为画板添加图形 Adding shapes to the palette

项目配置完后,可以很轻松的为画板添加图形。每个添加的图形都表现为JAR中的一个类。请注意这些类并不是Activiti引擎运行时会使用的类。在扩展中可以为每个图形描述Activiti Designer可用的参数。在这些图形中,也可以定义运行时特性,并将由引擎在流程实例到达该节点时使用。运行时特性可以使用任何Activiti对普通ServiceTask支持的选项。查看这个章节了解更多信息。

图形的类是简单的Java类,加上一些注解。这个类需要实现CustomServiceTask接口,但不应该直接实现这个接口,而应该扩展AbstractCustomServiceTask基类(目前必须直接扩展这个类,而不能在中间使用abstract类)。在这个类的Javadoc中,可以看到其默认提供的,与需要覆盖的方法介绍。覆盖可以实现很多功能,例如为画板及画布中的图形提供图标(两个可以不一样),或者指定你希望节点实现的基图形(活动,时间,网关)。

/**
 * @author John Doe
 * @version 1
 * @since 1.0.0
 */
public class AcmeMoneyTask extends AbstractCustomServiceTask {
...
}

需要实现getName()方法,来决定节点在画板上的名字。也可以将节点放在自己的抽屉中,并提供图标,只需要覆盖AbstractCustomServiceTask的对应方法就可以。如果希望提供图标,请确保放在JAR的src/main/resources包中,需要是16X16像素的JPEG或PNG格式图片。你要提供的路径是到这个目录的相对路径。

可以通过在类中添加成员,并使用@Property注解,来为形状添加参数。像这样:

@Property(type = PropertyType.TEXT, displayName = "Account Number")
@Help(displayHelpShort = "提供一个账户编码 Provide an account number", displayHelpLong = HELP_ACCOUNT_NUMBER_LONG)
private String accountNumber;

可以使用多种PropertyType值,在这个章节中详细描述。可以通过将required属性设置为true,将一个字段设为必填。如果用户没有填写这个字段,将会提示消息,背景也会变红。

如果想要确保类中多个参数在参数界面上的显示顺序,需要指定@Property注解的order属性。

可以看到有个@Help注解,它用于为用户提供一些填写字段的指导。也可以在类本身上使用@Help注解——这个信息将在显示给用户的参数表格最上面显示。

下面是MoneyTask详细介绍的列表。添加了一个备注字段,也可以看到节点包含了一个图标。

/**
 * @author John Doe
 * @version 1
 * @since 1.0.0
 */
@Runtime(javaDelegateClass = "org.acme.runtime.AcmeMoneyJavaDelegation")
@Help(displayHelpShort = "创建一个新的账户 Creates a new account", displayHelpLong = "使用给定的账户编码,创建一个新的账户 Creates a new account using the account number specified")
public class AcmeMoneyTask extends AbstractCustomServiceTask {

  private static final String HELP_ACCOUNT_NUMBER_LONG = "提供一个可用作账户编码的编码。 Provide a number that is suitable as an account number.";

  @Property(type = PropertyType.TEXT, displayName = "Account Number", required = true)
  @Help(displayHelpShort = "提供一个账户编码 Provide an account number", displayHelpLong = HELP_ACCOUNT_NUMBER_LONG)
  private String accountNumber;

  @Property(type = PropertyType.MULTILINE_TEXT, displayName = "Comments")
  @Help(displayHelpShort = "提供备注 Provide comments", displayHelpLong = "可以为节点添加备注,以提供详细说明。 You can add comments to the node to provide a brief description.")
  private String comments;

  /*
   * (non-Javadoc)
   *
   * @see org.activiti.designer.integration.servicetask.AbstractCustomServiceTask #contributeToPaletteDrawer()
   */
  @Override
  public String contributeToPaletteDrawer() {
    return "Acme Corporation";
  }

  @Override
  public String getName() {
    return "Money node";
  }

  /*
   * (non-Javadoc)
   *
   * @see org.activiti.designer.integration.servicetask.AbstractCustomServiceTask #getSmallIconPath()
   */
  @Override
  public String getSmallIconPath() {
    return "icons/coins.png";
  }
}

如果使用这个图形扩展Activiti Designer,画板与相应的图形将像是这样:

designer.palette.add.money

money任务的参数界面在下面显示。请注意accountNumber字段的必填信息。

designer.palette.add.money.properties.required

在创建流程图、填写参数字段时,用户可以使用静态文本,或者使用流程变量的表达式(如"This little piggy went to ${piggyLocation}")。一般来说,用户可以在text字段自由填写任何文本。如果你希望用户使用表达式,并(使用@Runtime)为CustomServiceTask添加运行时行为,请确保在代理类中使用Expression字段,以便表达式可以在运行时正确解析。可以在这个章节找到更多关于运行时行为的信息。

字段的帮助信息由每个参数右侧的按钮提供。点击该按钮将弹出显示下列内容。

designer.palette.add.money.help
配置自定义服务任务的运行时执行 Configuring runtime execution of Custom Service Tasks

当设置好字段,并将扩展应用至Designer后,用户就可以在建模流程时,配置服务任务的这些参数。在大多数情况下,会希望在Activiti执行流程时,使用这些用户配置参数。要做到这一点,必须告诉Activiti,当流程到达你CustomServiceTask时,需要使用哪个类。

有一个特别的注解,@Runtime,用于指定CustomServiceTask的运行时特性。这里有些如何使用的例子:

@Runtime(javaDelegateClass = "org.acme.runtime.AcmeMoneyJavaDelegation")

使用时,CustomServiceTask将会表现为流程建模BPMN中的一个普通的ServiceTask。Activiti提供了多种方法定义ServiceTask的运行时特性。因此,@Runtime可以使用Activiti提供的三个属性中的一个:

  • javaDelegateClass在BPMN输出中映射为activiti:class。指定一个实现了JavaDelegate的类的全限定类名。

  • expression在BPMN输出中映射为activiti:expression。指定一个需要执行的方法的表达式,例如一个Spring Bean中的方法。当使用这个选项时,不应在字段上指定任何@Property注解。下面有更详细的说明。

  • javaDelegateExpression在BPMN输出中映射为activiti:delegateExpression。指定一个实现了JavaDelegate的类的表达式。

如果在类中为Activiti提供了可以注入的成员,就可以将用户的参数至注入到运行时类中。名字需要与CustomServiceTask的成员名一致。查看用户手册的这个部分了解更多信息。请注意从Designer的5.11.0版本开始,可以为动态字段值使用Expression接口。这意味着Activiti Designer中参数的值必须要是表达式,并且这个表达式将在之后注入JavaDelegate实现类的Expression参数中。

可以在CustomServiceTask的成员上使用@Property注解,但如果使用@Runtimeexpression属性,则@Property注解将不会生效。原因是指定的表达式将被Activiti尝试解析为方法,而不是类。因此,不会有对类的注入。如果在@Runtime注解中使用expression,则注解为@Property的成员将被Designer忽略。Designer不会将它们渲染为节点参数页面的可编辑字段,也不会为这些参数在流程的BPMN中生成输出。

请注意不应该在你的扩展JAR中包括运行时类,因为它与Activiti库是分离的。Activiti需要在运行时能够找到它们,因此需要将其放在Activiti引擎的clsspath中。

Designer代码树中的示例项目包含了配置@Runtime的不同选项的例子。可以从查看money-tasks项目开始。引用代理类的示例在money-delegates项目中。

参数类型 Property types

这个章节介绍了CustomServiceTask能够使用的参数类型,可以将类型设置为PropertyType的值。

PropertyType.TEXT

创建如下所示的单行文本字段。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将字段的背景变为浅红色。

designer.property.text.invalid
PropertyType.MULTILINE_TEXT

创建如下所示的多行文本字段(高度固定为80像素)。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将字段的背景变为浅红色。

designer.property.multiline.text.invalid
PropertyType.PERIOD

创建一个组合编辑框,可以使用转盘控件编辑每一个单位的数量,来指定一段时间长度,结果如下所示。可以是必填字段(含义是不能所有的值都是0,也就是至少有一个部分要有非零值),并将验证消息作为提示信息显示。验证失败会将整个字段的背景变为浅红色。字段的值保存为1y 2mo 3w 4d 5h 6m 7s格式的字符串,代表1年,2月,3周,4天,6分钟及7秒。即使有部分为0,也总是存储整个字符串。

designer.property.period
PropertyType.BOOLEAN_CHOICE

创建一个单独的boolean复选框,或者开关选择。请注意可以在Property注解上指定required属性,但不会生效,不然用户就无法选择是否选中复选框。流程图中存储的值为java.lang.Boolean.toString(boolean),其结果为"true"或"false"。

designer.property.boolean.choice
PropertyType.RADIO_CHOICE

创建如下所示的一组单选按钮。选中任何一个单选按钮都自动排除任何其他的选择(也就是说,单选)。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将组的背景变为浅红色。

这个参数类型需要注解的类成员同时使用@PropertyItems注解(例如如下所示)。可以使用这个额外的注解,以字符串数组的方式,指定条目的列表。需要为每一个条目添加两个数组项:第一个,用于显示的标签;第二个,用于存储的值。

@Property(type = PropertyType.RADIO_CHOICE, displayName = "提款限额 Withdrawl limit", required = true)
@Help(displayHelpShort = "最大每日提款限额 The maximum daily withdrawl amount ", displayHelpLong = "选择从该账户中每日最大能提取的额度。 Choose the maximum daily amount that can be withdrawn from the account.")
@PropertyItems({ LIMIT_LOW_LABEL, LIMIT_LOW_VALUE, LIMIT_MEDIUM_LABEL, LIMIT_MEDIUM_VALUE, LIMIT_HIGH_LABEL, LIMIT_HIGH_VALUE })
private String withdrawlLimit;
designer.property.radio.choice
designer.property.radio.choice.invalid
PropertyType.COMBOBOX_CHOICE

创建如下所示的,带有固定选项的下拉框。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将下拉框的背景变为浅红色。

这个参数类型需要注解的类成员同时使用@PropertyItems注解(例如如下所示)。可以使用这个额外的注解,以字符串数组的方式,指定条目的列表。需要为每一个条目添加两个数组项:第一个,用于显示的标签;第二个,用于存储的值。

@Property(type = PropertyType.COMBOBOX_CHOICE, displayName = "账户类型 Account type", required = true)
@Help(displayHelpShort = "账户的类型 The type of account", displayHelpLong = "从选项列表中选择账户的类型 Choose a type of account from the list of options")
@PropertyItems({ ACCOUNT_TYPE_SAVINGS_LABEL, ACCOUNT_TYPE_SAVINGS_VALUE, ACCOUNT_TYPE_JUNIOR_LABEL, ACCOUNT_TYPE_JUNIOR_VALUE, ACCOUNT_TYPE_JOINT_LABEL,
  ACCOUNT_TYPE_JOINT_VALUE, ACCOUNT_TYPE_TRANSACTIONAL_LABEL, ACCOUNT_TYPE_TRANSACTIONAL_VALUE, ACCOUNT_TYPE_STUDENT_LABEL, ACCOUNT_TYPE_STUDENT_VALUE,
  ACCOUNT_TYPE_SENIOR_LABEL, ACCOUNT_TYPE_SENIOR_VALUE })
private String accountType;
designer.property.combobox.choice
designer.property.combobox.choice.invalid
PropertyType.DATE_PICKER

创建如下所示的日期选择控件。可以是必填字段,并将验证消息作为提示信息显示(请注意,这个控件会自动填入当前系统时间,因此值很难为空)。验证失败会将控件的背景变为浅红色。

这个参数类型需要注解的类成员同时使用@DatePickerProperty注解(例如如下所示)。可以使用这个额外的注解,指定在流程图中存储日期时使用的日期格式,以及要用于显示的日期选择类型。这些属性都是可选的,当没有指定时会使用默认值(DatePickerProperty注解的静态变量)。dateTimePattern属性应该使用SimpleDateFormat类支持的格式。当使用swtStyle属性时,应该指定SWTDateTime控件支持的整形值,因为将使用这个控件渲染这个类型的参数。

@Property(type = PropertyType.DATE_PICKER, displayName = "过期日期 Expiry date", required = true)
@Help(displayHelpShort = "账户过期的日期 The date the account expires ", displayHelpLong = "选择一个日期,如果账户未在该日期前展期,则将过期。 Choose the date when the account will expire if no extended before the date.")
@DatePickerProperty(dateTimePattern = "MM-dd-yyyy", swtStyle = 32)
private String expiryDate;
designer.property.date.picker
PropertyType.DATA_GRID

创建一个如下所示的数据表格控件。数据表格可以让用户输入任意行数据,并为每一行输入固定列数的值(每一组行列的组合代表一个单元格)。用户可以添加与删除行。

这个参数类型需要注解的类成员同时使用@DataGridProperty注解(例如如下所示)。可以使用这个额外的注解,指定数据表格的细节属性。需要用itemClass属性引用另一个类,来决定表格中有哪些列。Activiti Designer期望其成员类型为List。按照约定,可以将itemClass属性的类用作其泛型类型。如果,例如,在表格中编辑一个杂货清单,用GroceryListItem类定义表格的列。在CustomServiceTask中,可以这样引用它:

@Property(type = PropertyType.DATA_GRID, displayName = "杂货清单 Grocery List")
@DataGridProperty(itemClass = GroceryListItem.class)
private List<GroceryListItem> groceryList;

CustomServiceTask一样,当使用数据表格时,"itemClass"可以使用相同的注解指定字段类型,目前支持TEXTMULTILINE_TEXTPERIOD。你会注意到不论其PropertyType是什么,表格都会为每个字段创建一个单行文本控件。这是为了表格保持整洁与可读。如果考虑下PERIOD这种PropertyType的显示模式,就可以想象出它绝不适合在表格的单元格中显示。对于 MULTILINE_TEXTPERIOD,会为每个字段添加双击机制,并会为该PropertyType弹出更大的编辑器。数值将在用户点击OK后存储至字段,因此可以在表格中显示。

必选属性使用与普通TEXT字段类似的方式处理,当任何字段失去焦点时,会验证整个表格。验证失败的单元格,背景色将变为浅红色。

默认情况下,这个组件允许用户添加行,但不能决定行的顺序。如果希望允许排序,需要将orderable属性设置为true,这将在每一行末尾启用按钮,以将该行在表格内上移或下移。

目前,这个参数类型不能正确注入运行时类。

designer.property.datagrid
在画板中禁用默认图形 Disabling default shapes in the palette

这种自定义需要在你的扩展中引入一个实现了DefaultPaletteCustomizer接口的类。不应该直接实现这个接口,而要扩展AbstractDefaultPaletteCustomizer基类。目前,这个类不提供任何功能,但DefaultPaletteCustomizer未来的版本中会提供更多功能,这样基类将提供更多合理的默认值,这样你的扩展将在未来的版本中更好用。

扩展AbstractDefaultPaletteCustomizer需要实现一个方法,disablePaletteEntries(),并必须返回一个PaletteEntry值的list。请注意如果从默认集合中移除图形,导致某个抽屉中没有图形,则该抽屉也会被移除。如果需要禁用所有的默认图形,只需要在结果中添加PaletteEntry.ALL。作为例子,下面的代码禁用了画板中的手动任务和脚本任务图形。

public class MyPaletteCustomizer extends AbstractDefaultPaletteCustomizer {

  /*
   * (non-Javadoc)
   *
   * @see org.activiti.designer.integration.palette.DefaultPaletteCustomizer#disablePaletteEntries()
   */
  @Override
  public List<PaletteEntry> disablePaletteEntries() {
    List<PaletteEntry> result = new ArrayList<PaletteEntry>();
    result.add(PaletteEntry.MANUAL_TASK);
    result.add(PaletteEntry.SCRIPT_TASK);
    return result;
  }

}

应用这个扩展的结果在下图显示。可以看到,在Tasks抽屉中不再显示手动任务与脚本任务图形。

designer.palette.disable.manual.and.script

要禁用所有默认图形,需要使用类似下面的代码。

public class MyPaletteCustomizer extends AbstractDefaultPaletteCustomizer {

  /*
   * (non-Javadoc)
   *
   * @see org.activiti.designer.integration.palette.DefaultPaletteCustomizer#disablePaletteEntries()
   */
  @Override
  public List<PaletteEntry> disablePaletteEntries() {
    List<PaletteEntry> result = new ArrayList<PaletteEntry>();
    result.add(PaletteEntry.ALL);
    return result;
  }

}

结果像是这样(请注意画板中不再显示默认图形所在的抽屉):

designer.palette.disable.all

12.5.2. 验证流程图与输出为自定义格式 Validating diagrams and exporting to custom output formats

除了自定义画板,也可以为Activiti Designer创建扩展,来进行流程图验证,以及将流程图的信息保存为Eclipse工作空间中的自定义资源。可以通过内建的扩展点实现 ,这个章节将介绍如何做。

保存功能最近正在重构。我们仍在开发验证功能。下面的文档记录的是旧的情况,并将在新功能可用后更新。

Activiti Designer可以编写用于验证流程图的扩展。默认情况已经可以在工具中验证BPMN结构,但你也可以添加自己的,如果希望验证额外的条目,例如建模约定,或者CustomServiceTask中的参数值。这些扩展被称作Process Validators

也可以在Activiti Designer保存流程图时,发布为其它格式。这些扩展被称作Export Marshallers,将在每次用户进行保存操作时,由Activiti Designer自动调用。这个行为可以在Eclipse配置对话框中,为每一种扩展检测出的格式,分别启用或禁用。Designer会根据用户的配置,确保在保存流程图时,调用你的ExportMarshaller

通常,会想要将ProcessValidatorExportMarshaller一起使用。例如有一些CustomServiceTask,带有一些希望在流程中使用的参数。然而,在生成流程前,希望验证其中一些值。联合使用ProcessValidatorExportMarshaller是最佳的方式,Activiti Designer也允许你无缝拼接扩展。

要创建一个ProcessValidatorExportMarshaller,需要创建与扩展画板不同的扩展类型。原因很简单:你的代码会需访问比集成库中提供的更多的API。特别是,会需要使用Eclipse的类。因此从一开始,就需要创建一个Eclipse插件(可以使用Eclipse的PDE支持完成),并将其打包为自定义Eclipse产品或特性。解释开发Eclipse插件的所有细节,已经不是本用户手册的范畴,因此下面的介绍仅限于扩展Activiti Designer的功能。

扩展包需要依赖下列库:

  • org.eclipse.core.runtime

  • org.eclipse.core.resources

  • org.activiti.designer.eclipse

  • org.activiti.designer.libs

  • org.activiti.designer.util

可选的,如果希望在扩展中使用,可以通过Designer使用org.apache.commons.lang包。

ProcessValidatorExportMarshaller都是通过扩展基类创建的。这些基类从其父类AbstractDiagramWorker继承了一些有用的方法。使用这些方法,可以创建在Eclipse问题视图中显示的提示信息,警告,错误标记,以便用户了解错误与重要的信息。可以以ResourcesInputStreams的格式获取流程图的信息,这些信息由DiagramWorkerContext提供,在AbstractDiagramWorker中可用。

不论是ProcessValidator还是ExportMarshaller中,做任何事情前最好调用clearMarkers();这将清除任何已有的标记(标记自动连接至操作,清除一个操作的标记不会影响其他操作的标记)。例如:

// 首先清除流程图的标记 Clear markers for this diagram first
clearMarkersForDiagram();

也需要使用(DiagramWorkerContext中)提供的进度监控,将你的进度报告给用户,因为验证与/或保存操作可能花费很多时间,而用户只能等待。报告进度需要了解如何使用Eclipse的功能。查看这篇文章了解详细概念与用法。

创建ProcessValidator扩展 Creating a ProcessValidator extension

审核中!

在你的plugin.xml文件中,创建一个org.activiti.designer.eclipse.extension.validation.ProcessValidator扩展点的扩展。这个扩展点需要扩展AbstractProcessValidator类。

<?eclipse version="3.6"?>
<plugin>
  <extension
    point="org.activiti.designer.eclipse.extension.validation.ProcessValidator">
    <ProcessValidator
      class="org.acme.validation.AcmeProcessValidator">
    </ProcessValidator>
  </extension>
</plugin>
public class AcmeProcessValidator extends AbstractProcessValidator {
}

需要实现一些方法。最重要的是实现getValidatorId(),为验证器返回全局唯一ID。这将使你可以在ExportMarshaller中调用它,或者在其他ExportMarshaller中让其他人调用你的验证器。实现getValidatorName(),为验证器返回逻辑名字。这个名字将在对话框中显示给用户。getFormatName()可以返回这个验证器通常验证的流程图类型。

验证工作通过validateDiagram()方法实现。从这里开始,就是你自己的功能代码了。然而,通常你会想从获取流程中的所有节点开始,这样就可以迭代访问,收集、比较与验证数据了。这段代码展示了如何进行这些操作:

final EList<EObject> contents = getResourceForDiagram(diagram).getContents();
for (final EObject object : contents) {
  if (object instanceof StartEvent ) {
  // 验证启动事件 Perform some validations for StartEvents
  }
  // 其它节点类型与验证 Other node types and validations
}

别忘了在验证过程中调用addProblemToDiagram()与/或addWarningToDiagram()等等。确保在结束时返回正确的boolean结果,以指示验证成功还是失败。可以由后续调用的ExportMarshaller判断下一步操作。

创建ExportMarshaller扩展 Creating an ExportMarshaller extension

在你的plugin.xml文件中,创建一个org.activiti.designer.eclipse.extension.ExportMarshaller扩展点的扩展。这个扩展点需要扩展AbstractExportMarshaller类。这个基类提供了一些在保存为你自己的格式时有用的方法,但最重要的是提供了将资源保存至工作空间,以及调用验证器的功能。

Designer的示例目录下有一个示例实现。这个示例展示了如何使用基类中的方法完成基本操作,例如访问流程图的InputStream,使用其BpmnModel,以及将资源保存至工作空间。

<?eclipse version="3.6"?>
<plugin>
  <extension
    point="org.activiti.designer.eclipse.extension.ExportMarshaller">
    <ExportMarshaller
      class="org.acme.export.AcmeExportMarshaller">
    </ExportMarshaller>
  </extension>
  </plugin>
public class AcmeExportMarshaller extends AbstractExportMarshaller {
}

需要实现一些方法,例如getMarshallerName()getFormatName()。这些方法用来为用户显示选项,并在流程对话框中显示信息,因此请确保你返回的描述反映了正在进行的操作。

大部分工作主要在doMarshallDiagram()方法中进行。

如果需要先进行一些验证,可以直接从保存器中调用验证器。从验证器可以获得boolean结果,就可以知道验证是否成功。在大多数情况下,在流程图验证失败时不会想要进行保存,但你也可以选择仍然继续,甚至在验证失败时创建不同的资源。

一旦获取了所有需要的数据,就可以调用saveResource()方法创建保存有数据的文件。在一个保存器中,可以调用saveResource()任意多次;因此一个验证器可以创建多于一个输出文件。

可以使用AbstractDiagramWorker类的saveResource()方法构建输出资源的文件名。可以使用一些有用的变量用于创建文件名,例如_original-filename__my-format-name.xml。这些变量在Javadocs中描述,通过ExportMarshaller接口定义。如果希望自行解析保存位置,也可以在一个字符串(例如一个路径)上使用resolvePlaceholders()getURIRelativeToDiagram()会为你调用它。

应该使用提供的进度监控将你的进度报告给用户。这个文章描述了如何做。

13. Activiti Explorer

Activiti Explorer是一个web应用,包含在从Activiti网站上下载的Activiti中。Explorer不是一个完成的,最终用户可用的应用,而是用于实践与展示Activiti的功能。因此Explorer更像是一个示例,或者为在自己的应用中使用Activiti的用户提供灵感。另外,Explorer使用内存数据库,但也可以轻松切换至你自己的数据库(查看WEB-INF目录下的applicationContext文件)。

登录应用后,将看到这些大图标,展示了主要功能。

explorer.tabs
  • Tasks: 任务管理功能。在这里可以看到指派给你的运行中用户任务的表单,或你可以申领的组任务。Explorer可以关联内容,将工作分为子任务,将人作为不同角色引入,等等。Explorer也可以创建不关联至任何流程的独立任务。

  • Process: 展示已部署的流程定义,也可以启动新的流程实例。

  • Reporting: 生成报告以及浏览之前保存的报告。查看报告章节了解更多细节。

  • Manage: 只有当用户登陆为管理员权限时才可见。可以管理Activiti引擎:管理用户与组,执行与查看卡住的作业,查看数据库,以及部署新的流程定义。

13.1. 配置 Configuration

Activiti Explorer使用Spring Java配置启动Activiti引擎。可以修改WEB-INF/classes目录下的engine.properties文件,定义小部分参数。如果需要高级配置选项,可以修改同在WEB-INF/classes目录下的activiti-custom-context.xml文件,来定义Activiti流程引擎配置。该文件已经以注释形式提供了示例配置。

13.2. 流程图 Process diagram

Explorer包含了动态生成流程定义的总览的功能,使用Raphaël JavaScript框架。这个流程图只能在流程定义XML中包含BPMN DI信息时才能生成。如果流程定义XML中没有BPMN DI信息,而部署中包含有流程定义图片,则会显示该图片。

explorer.process.definition.image

如果不希望使用JavaScript流程定义总览,可以在ui.properties文件中禁用

activiti.ui.jsdiagram = false

另外,要在Explorer中显示流程图,也可以在任何你想要的地方引入流程图。下面的URL将基于流程定义id,显示流程定义图:

http://localhost:8080/activiti-explorer/diagram-viewer/index.html?processDefinitionId=reviewSaledLead:1:36

也可以通过添加processInstanceId请求参数,显示流程实例的当前状态,像是这样:

http://localhost:8080/activiti-explorer/diagram-viewer/index.html?processDefinitionId=reviewSaledLead:1:36&processInstanceId=41

13.3. 任务 Tasks

explorer.tasks
  • Inbox: 展示指派给当前登录用户的任务。

  • My tasks: 展示当前登录用户作为属主的任务。当创建独立任务时,当前用户自动成为该任务的属主。

  • Queued: 展示你所在的不同组。这里的任务必须先进行申领,然后才能完成。

  • Involved: 展示当前登陆用户作为下列角色之一的任务:(1)相关人员(也就是说候选用户或参与者),(2)办理人,或者(3)属主。

  • Archived 包含过去(历史)的任务。

13.4. 启动流程实例 Start process instances

Process definitions(流程定义)页签可以查看Activiti引擎中部署的所有流程定义。可以使用右上角的按钮启动新的流程实例。如果流程定义了启动表单,则会在启动流程实例前显示该表单。

explorer.process.definitions

13.5. 我的实例 My instances

My instances页签,展示你当前有未完成用户任务的所有流程实例。同时也显示该流程实例的当前活动,以及存储的流程变量。

explorer.my.instances

13.6. 管理 Administration

管理功能只有在登录用户是安全组admin的成员时才可用。当点击Manage图标时,可用下列页签:

  • Database: 展示数据库内容。当部署流程或排错时十分有用。

explorer.database
  • Deployments: 展示引擎当前的部署,并查看部署的内容(流程定义,图片,业务规则,等等)。

explorer.deployments

点击deployment页签也可以上传新的部署。选择电脑中的一个业务存档或者一个bpmn20.xml文件,或者简单地拖放至特定区域,就可以部署新的业务流程。

explorer.upload.deployment
  • Jobs: 在左侧展示当前的作业(定时器,等等),也可以手动执行它们(例如,在到时前触发定时器)。如果作业执行失败(例如邮件服务器无法连接),也会显示异常

explorer.jobs
  • Users and Groups: 管理用户与组:创建、编辑与删除用户与组。将用户关联至组,以赋予更多权限,或使他们可以查看分派给特定组的任务。

explorer.users

13.7. 报告 Reporting

Activiti Explorer提供了一些报告的例子,也可以很容易地为系统添加新的报告。报告功能组织在'Reports'页签下。

explorer.reporting

重要:要使报告能工作,Explorer需要配置为不是none的历史级别。默认配置满足这个要求。

报告页签目前有两个子页签:

  • Generate reports: 展示系统已有的所有报告类型的列表。可以运行来生成报告。

  • Saved reports: 展示之前保存的所有报告的列表。请注意这些是个人保存的报告,不能查看其他人保存的报告。

用于创建报告中列表与图表的数据由流程生成。虽然初看有些奇怪,但使用流程生成报告数据有几个优点

  • 流程可以直接访问Activiti引擎内部,并可以直接访问引擎使用的数据库。

  • 作业执行器可以用于任何其他流程。这意味着可以异步生成流程,或者同步地执行一些步骤。也意味着可以使用定时器,例如,在特定时间点生成报告。

  • 可以使用已有工具与已有概念创建新报告。并且,不需要新的概念、服务或应用。部署或上传新的报告与部署一个新流程是一样的。

  • 可以使用BPMN 2.0结构。这意味着所有的东西,比如并行步骤,基于数据做分支选择,或者甚至是生成过程中请求用户输入都可以使用。

用于生成报告数据的流程定义需要是'activiti-report’类型的,这样才能在Explorer的已有报告列表中看到。“报告流程”可以简单,也可以任意复杂。唯一的要求,是流程要生成一个叫做reportData的变量,这个变量必须是一个表示JSON对象的字节数组。该变量存储在Activiti的历史表中(因此要求引擎必须启用历史),用于后续保存报告时获取。

13.7.1. 报告数据JSON (Report data JSON)

报告流程必须生成一个reportData变量,这是一个代表了需要显示给用户的数据的JSON,要像下面这样:

{
  "title": "My Report",
  "datasets": [
    {
      "type" : "lineChart",
      "description" : "My first chart",
      "xaxis" : "Year"
      "yaxis" : "Total sales"
      "data" :
      {
        "2010" : 50,
        "2011" : 33,
        "2012" : 17,
        "2013" : 87,
      }
    }
  ]
}

这个JSON将在Explorer运行时获取,并将用于生成图表或列表。JSON中的元素是:

  • title: 整个报告的总标题

  • datasets: 与报告中各图表与列表对应的数据集的数组。

  • type: 每个数据集都有一个类型。这个类型将用于决定如何渲染数据。目前支持的值有:pieChart,lineChart,barChart与list

  • description: 每个图表都可选一个描述,将显示在报告中。

  • x与yaxis: 只对lineChart类型可用。描述图表坐标轴名字的可选参数。

  • data: 实际的数据,是一个带有键值对元素的JSON对象。

13.7.2. 实例流程 Example process

下面的例子展示了一个’process instance overview (流程实例总览)'报告。流程本身十分简单,只有一个使用JavaScript生成JSON数据集的脚本任务(除了启动与结束)。尽管Explorer中所有的例子都使用脚本,但也完全可以使用Java服务任务。运行流程的结果就是包含数据的reportData变量。

重要提示:下面的例子只能在JDK 7+使用。原因是旧JDK版本中的JavaScript引擎(Rhino)不够先进,不能使用类似下面使用的结构撰写脚本。之后有一个兼容JDK 6+的例子。

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
    xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
    xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
    expressionLanguage="http://www.w3.org/1999/XPath"
    targetNamespace="activiti-report">

    <process id="process-instance-overview-report" name="Process Instance Overview" isExecutable="true">

        <startEvent id="startevent1" name="Start" />
        <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="generateDataset" />

        <scriptTask id="generateDataset" name="Execute script" scriptFormat="JavaScript" activiti:autoStoreVariables="false">
          <script><![CDATA[

               importPackage(java.sql);
               importPackage(java.lang);
               importPackage(org.activiti.explorer.reporting);

               var result = ReportingUtil.executeSelectSqlQuery("SELECT PD.NAME_, PD.VERSION_ , count(*) FROM ACT_HI_PROCINST PI inner join ACT_RE_PROCDEF PD on PI.PROC_DEF_ID_ = PD.ID_ group by PROC_DEF_ID_");

               var reportData = {};
               reportData.datasets = [];

               var dataset = {};
               dataset.type = "pieChart";
               dataset.description = "Process instance overview (" + new java.util.Date() + ")";
               dataset.data = {};

               while (result.next()) { // process results one row at a time
                 var name = result.getString(1);
                 var version = result.getLong(2)
                 var count = result.getLong(3);
                 dataset.data[name + " (v" + version + ")"] = count;
               }
               reportData.datasets.push(dataset);

               execution.setVariable("reportData", new java.lang.String(JSON.stringify(reportData)).getBytes("UTF-8"));
          ]]></script>
        </scriptTask>
        <sequenceFlow id="flow3" sourceRef="generateDataset" targetRef="theEnd" />

        <endEvent id="theEnd" />

    </process>

</definitions>

除了流程XML顶端的标准的XML行,主要的区别是targetNamespace设置为activiti-report,为部署的流程定义添加了同名的类型。

脚本的前几行是为了避免重复写包名而进行的引入。要关注的第一行是使用ReportingUtil查询Activiti数据库,其结果是生成了一个普通的JDBC Resultset。在查询下面的几行,JavaScript功能轻松地创建了要用的JSON。这个JSON按照要求生成。

脚本的最后一行看起来有些奇怪。首先是使用JavaScript函数JSON.stringify()将JSON对象转换为字符串,然后将这个字符串保存为一个字节数组变量。原因是技术性的:字节数组没有大小限制,而字符串有。这就是为什么JavaScript字符串必须要转换为Java字符串,因为这样就可以转换为字节形式。

兼容JDK 6(与更高)的相同流程看起来有点区别。不能使用原生的JSON功能,因此需要提供一些辅助类(ReportDataDataset):

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn"
    xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
    xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema"
    expressionLanguage="http://www.w3.org/1999/XPath"
    targetNamespace="activiti-report">

    <process id="process-instance-overview-report" name="Process Instance Overview" isExecutable="true">

        <startEvent id="startevent1" name="Start" />
        <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="generateDataset" />

        <scriptTask id="generateDataset" name="Execute script" scriptFormat="js" activiti:autoStoreVariables="false">
          <script><![CDATA[

               importPackage(java.sql);
               importPackage(java.lang);
               importPackage(org.activiti.explorer.reporting);

               var result = ReportingUtil.executeSelectSqlQuery("SELECT PD.NAME_, PD.VERSION_ , count(*) FROM ACT_HI_PROCINST PI inner join ACT_RE_PROCDEF PD on PI.PROC_DEF_ID_ = PD.ID_ group by PROC_DEF_ID_");


               var reportData = new ReportData;
               var dataset = reportData.newDataset();
               dataset.type = "pieChart";
               dataset.description = "Process instance overview (" + new java.util.Date() + ")"


               while (result.next()) { // process results one row at a time
                 var name = result.getString(1);
                 var version = result.getLong(2);
                 var count = result.getLong(3);
                 dataset.add(name + " (v" + version + ")", count);
               }

               execution.setVariable("reportData", reportData.toBytes());

          ]]></script>
        </scriptTask>
        <sequenceFlow id="flow3" sourceRef="generateDataset" targetRef="theEnd" />

        <endEvent id="theEnd" />

    </process>

</definitions>

13.7.3. 报告启动表单 Report start forms

报告通过普通流程生成,因此可以使用普通的表单功能。简单的为启动事件添加启动表单,这样Explorer就将在生成流程前为用户显示表单。

<startEvent id="startevent1" name="Start">
  <extensionElements>
    <activiti:formProperty id="processDefinition" name="Select process definition" type="processDefinition" required="true" />
      <activiti:formProperty id="chartType" name="Chart type" type="enum" required="true">
        <activiti:value id="pieChart" name="Pie chart" />
        <activiti:value id="barChart" name="Bar chart" />
      </activiti:formProperty>
  </extensionElements>
</startEvent>

为用户渲染的是一个典型的表单:

explorer.reporting.start.form

表单的参数将在流程启动时提交,与普通执行变量一样,可以在生成数据的脚本中使用:

 var processDefinition = execution.getVariable("processDefinition");

13.7.4. 示例流程 Example processes

默认情况下,Explorer包含四个示例报告:

  • Employee productivity(雇员生产力):这个报告演示了折线图的使用,并使用了启动表单。这个报告中使用的脚本也比其他例子中的复杂,因为获取的数据,在存储至报告数据前,会由脚本进行处理。

  • Helpdesk - firstline vs escalated(帮助中心——一线对比升级):展示了饼图的使用,并结合了两个不同数据库查询的结果。

  • Process instance overview(流程实例总览):使用多个数据集的示例报告。这个报告包含有一个饼图,以及相同数据的列表视图,这样展示了如何使用多个数据集生成一个带有多个图表的页面。

  • Task duration(任务持续时间):另一个使用了启动表单的例子,并使用了相应的数据动态建立SQL查询。

13.8. 改变数据库 Changing the database

要修改Explorer在演示配置中使用的数据库,需要修改apps/apache-tomcat-6.x/webapps/activiti-explorer/WEB-INF/classes/db.properties配置文件。并且,在classpath中放入合适的数据库驱动(Tomcat共享库,或放在apps/apache-tomcat-6.x/webapps/activiti-explorer/WEB-INF/lib/下)。

14. Activiti Modeler

Activiti Modeler是一个BPMN web建模器组件,内置在Activiti Explorer web应用中。Modeler是 Signavio核心组件的分支项目。Activiti Modeler从5.17.0版本起基于Angular JS(之前是基于Ext-JS的应用)。Activiti Modeler的Angular JS部分基于LGPL协议发布。与之前版本的Activiti Modeler(Signavio核心组件)的主要区别,是新的Modeler作为Activiti项目的一部分维护与开发。Activiti Modeler的目标,是支持所有BPMN元素与Activiti支持的扩展。

使用默认配置运行Activiti Explorer时,在模型工作空间内将有一个示例流程。

modeler.example.process

14.1. 编辑模型 Model editing

在模型工作空间点击edit按钮时,会在建模器中打开模型。屏幕的左侧是BPMN元素与Activiti扩展的画板。在需要时可以将新元素拖放至画布上。在屏幕底部可以填写选中元素的属性。在示例截图中选中了一个用户任务,可以填写用户任务属性,例如办理人,表单参数与到期日期。要返回Activiti Explorer,可以点击屏幕右上角的关闭按钮。

modeler.editor.canvas

14.2. 导入现有模型 Importing existing models

也可以将现有模型导入模型工作空间,以在Activiti Modeler中编辑它们。点击import按钮,选择一个.bpmn或者.bpmn20.xml文件。请注意这个BPMN XML文件需要包含BPMN DI信息。

modeler.import

14.3. 将已部署定义转换为可编辑模型 Convert deployed definitions to a editable model

已部署的流程定义可以转换为能够使用Activiti Modeler编辑的模型。请注意该流程定义需要包含BPMN DI信息。

modeler.convert

14.4. 将模型导出为BPMN XML (Export model to BPMN XML)

模型工作空间中的模型可以导出为BPMN XML文件。在模型动作选项框中选择导出选项。

modeler.export

14.5. 将模型部署至Activiti引擎 Deploy model to the Activiti Engine

当模型包含了运行所需的所有参数,就可以部署至Activiti引擎。在模型动作选项框中选择部署选项。

modeler.deploy

15. REST API

15.1. Activiti REST一般原则 General Activiti REST principles

15.1.1. 安装与认证 Installation and Authentication

Activiti在引擎中包含了REST API,可以通过在servlet容器如Apache Tomcat中,部署activiti-rest.war文件来安装。但是也可以在其他的web应用中使用,只要在你的应用中包含这些servlet与其映射,并在classpath中添加所有activiti-rest依赖即可。

默认情况下Activiti引擎连接至一个H2内存数据库。可以修改WEB-INF/classes文件夹下的db.properties文件中的数据库设置。REST API使用JSON格式 (http://www.json.org) ,基于Spring MVC (http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html) 构建。

默认情况下,所有REST资源都需要有一个有效的的Activiti已认证用户,使用基础HTTP访问认证,因此在请求时,可以在HTTP头添加Authorization: Basic …​==,也可以在请求url中包含用户名与密码(例如http://username:password@localhost…​;)。

建议使用基础认证时,同时使用HTTPS。

15.1.2. 配置 Configuration

Activiti REST web应用使用Spring Java Configuration来启动Activiti引擎、定义基础认证安全使用Spring security,以及为特定的变量处理定义变量转换。可以修改WEB-INF/classes目录下的engine.properties文件,定义少量参数。如果需要高级配置选项,可以在activiti-custom-context.xml文件中覆盖默认的Spring bean,这个文件也在WEB-INF/classes目录下。该文件中已经以注释形式提供了示例配置。也可以在这里通过定义一个新的命名为restResponsefactory的Spring bean,覆盖默认的RestResponseFactory,并使用自定义实现类。

15.1.3. 在Tomcat中使用 Usage in Tomcat

由于Tomcat中的默认安全参数默认不能使用已转义斜线符(%2F%5C)(返回400结果)。这可能会影响部署资源与其数据URL,因为URL可能隐含已转义斜线符。

当发现非预期的400结果时,设置下列系统参数 -Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true.

最佳实践是(post/put JSON时),在下面描述的HTTP请求中,永远将AcceptContent-Type头设置为application/json

15.1.4. 方法与返回码 Methods and return-codes

Table 2. HTTP方法与相应操作
方法 操作

GET

获取单个资源,或获取一组资源。

POST

创建一个新资源。在查询结构太复杂,不能放入GET请求的查询URL中时,也用于执行资源查询。

PUT

更新一个已有资源的参数。也用于在已有资源上的调用动作。

DELETE

删除一个已有资源。

Table 3. HTTP方法响应码
响应 描述

200 - Ok

操作成功,返回响应(GETPUT请求)。

201 - 已创建

操作成功,已经创建了实体,并在响应体中返回(POST请求)。

204 - 无内容

操作成功,已经删除了实体,因此没有返回的响应体(DELETE请求)。

401 - 未认证

操作失败。操作要求设置认证头。如果请求中有认证头,则提供的鉴证并不合法,或者用户未被授权进行该操作。

403 - 禁止

操作被禁止,且不应重试。这不是鉴证或授权的问题,而是说明不允许该操作。例如:删除一个运行中流程的任务是且永远是不允许的,无论该用户或流程/任务的状态。

404 - 未找到

操作失败。请求的资源未找到。

405 - 不允许的方法

操作失败。使用的方法不能用于该资源。例如,更新(PUT)部署资源将导致405状态。

409 - 冲突

操作失败。该操作导致更新一个已被其他操作更新的资源,因此本更新不再有效。也可以表明正在为一个集合创建一个资源,但该集合中已经使用了该标识符。

415 - 不支持的媒体类型

操作失败。请求提包含了不支持的媒体类型。也会发生在请求体JSON中包含了未知的属性或值,但没有正确的格式/类型来接受的情况下。

500 - 服务器内部错误

操作失败。执行操作时发生了未知异常。响应体中包含了错误的细节。

HTTP响应的media-type总是application/json,除非请求的是二进制内容(例如部署资源数据)。这时将使用内容的media-type。

15.1.5. 错误响应体 Error response body

当发生错误时(客户端与服务器端都可能,4XX及5XX状态码),响应体会包含一个描述了发生的错误的对象。任务未找到时的404状态的例子:

{
  "statusCode" : 404,
  "errorMessage" : "Could not find a task with id '444'."
}

15.1.6. 请求参数 Request parameters

URL片段 URL fragments

作为url的一部分的参数(例如,http://host/actviti-rest/service/repository/deployments/{deploymentId}中的deploymentId参数),如果包含特殊字符,则需要进行合适的转义(参见URL编码或百分号编码)。大多数框架都内建了这个功能,但要记得考虑它。特别是对可能包含斜线符的段落(例如部署资源),就是必须的。

Rest URL查询参数 Rest URL query parameters

作为查询字符串添加在URL中的参数(例如http://host/activiti-rest/service/deployments?name=Deployment中的name参数)可以使用下列类型,也会在相应的REST-API文档中提到:

Table 4. URL查询参数类型
类型 格式

String

纯文本参数。可以包含任何URL允许的合法字符。对于XXXLike参数,字符串可能会包含通配符%(需要进行URL编码)。可以进行like搜索,例如,'Tas%'将匹配所有以’Tas’开头的值。

Integer

整形参数。只能包含数字型非十进制值(原文如此,下同),在-2.147.483.648至2.147.483.647之间。

Long

长整形参数。只能包含数字型非十进制值,在-9.223.372.036.854.775.808至9.223.372.036.854.775.807之间。

Boolean

boolean型参数。可以为truefalse。任何其他值都会导致'405 - 错误请求'响应。

Date

日期型参数。使用ISO-8601日期格式(参考wikipedia中的ISO-8601),使用时间与日期组分(例如2013-04-03T23:45Z)。

JSON体参数 JSON body parameters
Table 5. JSON参数类型
类型 格式

String

纯文本参数。对于XXXLike参数,字符串可能会包含通配符%。可以进行like搜索。例如,'Tas%'将匹配所有以’Tas’开头的值。

Integer

整形参数,使用JSON数字。只能包含数字型非十进制值(原文如此,下同),在-2.147.483.648至2.147.483.647之间。

Long

长整形参数,使用JSON数字。只能包含数字型非十进制值,在-9.223.372.036.854.775.808至9.223.372.036.854.775.807之间。

Date

日期型参数,使用JSON文本。使用ISO-8601日期格式(参考wikipedia中的ISO-8601),使用时间与日期组分(例如2013-04-03T23:45Z)。

分页与排序 Paging and sorting

分页与排序参数可以作为查询字符串加入URL中(例如http://host/activiti-rest/service/deployments?sort=name中的name参数)。

Table 6. 查询变量参数
参数 默认值 描述

sort

各查询实现不同

排序键的名字,在各查询实现中默认值与可用值都不同。

order

asc

排序顺序,可以是’asc'(顺序)或’desc'(逆序)。

start

0

对结果分页的参数。默认结果从0开始。

size

10

对结果分页的参数。默认大小为10.

JSON查询变量格式
{
  "name" : "variableName",
  "value" : "variableValue",
  "operation" : "equals",
  "type" : "string"
}
Table 7. JSON查询变量参数
参数 必填 描述

name

包含在查询中的变量名。在有些使用'equals'的查询中可以为空,查询任意变量名为给定值的资源。

value

包含在查询中的变量值,需要使用给定类型的正确格式。

operator

查询使用的操作,可以为下列值:equals, notEquals, equalsIgnoreCase, notEqualsIgnoreCase, lessThan, greaterThan, lessThanOrEquals, greaterThanOrEquals, likelikeIgnoreCase

type

所用变量的类型。当省略时,会从value参数推理类型。任何JSON文本值都认为是string类型,JSON boolean值认为是boolean类型,JSON数字认为是longinteger,取决于数字的大小。建议在有疑惑时明确指定类型。其他支持的类型列在下面。

Table 8. 默认查询JSON类型
类型名 描述

string

值处理转换为java.lang.String

short

值处理转换为java.lang.Integer

integer

值处理转换为java.lang.Integer

long

值处理转换为java.lang.Long

double

值处理转换为java.lang.Double

boolean

值处理转换为java.lang.Boolean

date

值处理转换为java.util.Date。JSON字符串将使用ISO-8601日期格式转换。

变量表示 Variable representation

当使用变量时(执行/流程与任务),读取与写入时REST-api都使用一些通用原则与JSON格式。变量的JSON表示像是这样:

{
  "name" : "variableName",
  "value" : "variableValue",
  "valueUrl" : "http://...",
  "scope" : "local",
  "type" : "string"
}
Table 9. 变量的JSON属性
参数 必填 描述

name

变量名。

value

变量的值。当写入变量且省略了value时,会使用null作为value。

valueUrl

当读取binaryserializable类型的变量时,这个属性将指向可用于获取原始二进制数据的URL。

scope

变量的范围。如果值为'local,则变量明确定义在其请求的资源上。如果值为global',则变量定义在其父上(或者父树中的任意父)。当写入变量且省略了scope时,使用global

type

变量的类型。查看下面的表格了解类型的更多信息。当写入变量且省略了这个值时,将使用请求的原始JSON属性类型推断,限制在string, double, integerboolean中。建议总是包含类型,以确保不会错误推断类型。

Table 10. 变量类型
类型名 描述

string

值按照java.lang.String处理。写入变量时使用原始JSON文本。

integer

值按照java.lang.Integer处理。按约定写入变量时使用JSON数字,失败则退回JSON文本。

short

值按照java.lang.Short处理。按约定写入变量时使用JSON数字,失败则退回JSON文本。

long

值按照java.lang.Long处理。按约定写入变量时使用JSON数字,失败则退回JSON文本。

double

值按照java.lang.Double处理。按约定写入变量时使用JSON数字,失败则退回JSON文本。

boolean

值按照java.lang.Boolean处理。按约定写入变量时使用JSON boolean。

date

值按照java.util.Date处理。写入变量时将转换为ISO-8601日期格式。

binary

二进制变量,按照字节数组处理。value属性为null,valueUrl包含指向原始二进制流的URL。

serializable

代表序列化的Java对象。与binary类型一样,value属性为null,valueUrl包含指向原始二进制流的URL。所有可序列化的变量(不是上述任意类型的)将被暴露为这个类型的变量。

可以使用自定义JSON表示,以支持额外的变量类型(既可以是简单值,也可以是复杂/嵌套的JSON对象)。通过扩展org.activiti.rest.service.api.RestResponseFactoryinitializeVariableConverters()方法,可以添加额外的org.activiti.rest.service.api.engine.variable.RestVariableConverter类,来将你的POJO转换为适合通过REST传输的格式,以及将REST值转换为POJO。实际转换JSON使用Jackson。

15.2. 部署 Deployment

使用tomcat时,请阅读在Tomcat中使用

15.2.1. 部署的列表 List of Deployments

GET repository/deployments
Table 11. URL查询参数
参数 必填 描述

name

String

只返回给定名字的部署。

nameLike

String

只返回名字like给定名字的部署。

category

String

只返回给定分类的部署。

categoryNotEquals

String

只返回不是给定分类的部署。

tenantId

String

只返回给定tenantId的部署。

tenantIdLike

String

只返回tenantId like给定值的部署。

withoutTenantId

Boolean

如果值为true,则只返回没有设置tenantId的部署。如果值为false,则忽略withoutTenantId参数。

sort

id(默认), name, deploytime’或’tenantId

用于排序的参数,与’order’一起使用。

可以在这个URL中使用通用分页与排序查询参数

Table 12. REST返回码
返回码 描述

200

代表请求成功。

成功响应体:

{
  "data": [
    {
      "id": "10",
      "name": "activiti-examples.bar",
      "deploymentTime": "2010-10-13T14:54:26.750+02:00",
      "category": "examples",
      "url": "http://localhost:8081/service/repository/deployments/10",
      "tenantId": null
    }
  ],
  "total": 1,
  "start": 0,
  "sort": "id",
  "order": "asc",
  "size": 1
}

15.2.2. 获取一个部署 Get a deployment

GET repository/deployments/{deploymentId}
Table 13. 获取一个部署 - URL参数
参数 必填 描述

deploymentId

String

要获取的部署的id。

Table 14. 获取一个部署 - 响应码
响应码 描述

200

代表已找到并返回部署。

404

代表未找到请求的部署。

成功响应体:

{
  "id": "10",
  "name": "activiti-examples.bar",
  "deploymentTime": "2010-10-13T14:54:26.750+02:00",
  "category": "examples",
  "url": "http://localhost:8081/service/repository/deployments/10",
  "tenantId" : null
}

15.2.3. 创建一个新部署 Create a new deployment

POST repository/deployments

请求体:

请求体需要包含multipart/form-data类型的数据。请求中需要只有一个文件,多余的文件将被忽略。部署名是传入的文件字段的名字。如果要在一个部署中部署多个资源,需要将资源压缩为zip文件,并确保文件名以.bar.zip结尾。

可以在请求体中传递名为tenantId的额外参数(表单字段)。这个字段的值将指定部署所在的租户(tenant)的id。

Table 15. 创建一个新部署 - 响应码
响应码 描述

201

代表成功创建部署

400

代表请求体中没有内容,或部署不支持content的mime-type。状态描述中包含了额外信息。

成功响应体:

{
  "id": "10",
  "name": "activiti-examples.bar",
  "deploymentTime": "2010-10-13T14:54:26.750+02:00",
  "category": null,
  "url": "http://localhost:8081/service/repository/deployments/10",
  "tenantId" : "myTenant"
}

15.2.4. 删除一个部署 Delete a deployment

DELETE repository/deployments/{deploymentId}
Table 16. 删除一个部署 - URL参数
参数 必填 描述

deploymentId

String

要删除的部署的id。

Table 17. 删除一个部署 - 响应码
响应码 描述

204

代表已找到并删除了部署。响应体设置为空。

404

代表未找到请求的部署。

15.2.5. 列表一个部署中的资源 List resources in a deployment

GET repository/deployments/{deploymentId}/resources
Table 18. 列表一个部署中的资源 - URL参数
参数 必填 描述

deploymentId

String

要获取资源的部署的id。

Table 19. 列表一个部署中的资源 - 响应码
响应码 描述

200

代表已找到部署,并已返回资源的列表。

404

代表未找到请求的部署。

成功响应体:

[
  {
    "id": "diagrams/my-process.bpmn20.xml",
    "url": "http://localhost:8081/activiti-rest/service/repository/deployments/10/resources/diagrams%2Fmy-process.bpmn20.xml",
    "contentUrl": "http://localhost:8081/activiti-rest/service/repository/deployments/10/resourcedata/diagrams%2Fmy-process.bpmn20.xml",
    "mediaType": "text/xml",
    "type": "processDefinition"
  },
  {
    "id": "image.png",
    "url": "http://localhost:8081/activiti-rest/service/repository/deployments/10/resources/image.png",
    "contentUrl": "http://localhost:8081/activiti-rest/service/repository/deployments/10/resourcedata/image.png",
    "mediaType": "image/png",
    "type": "resource"
  }
]
  • mediaType: 包含了资源的媒体类型。使用(可插入的)MediaTypeResolver解析,默认包含有限数量的媒体类型映射。

  • type: 资源的类型,可用值为:

    • resource: 原始资源。

    • processDefinition: 包含一个或多个流程定义的资源。通过部署器挑选。

    • processImage: 代表流程定义的图形化输出的资源。

结果JSON中的contentUrl参数包含了获取该二进制资源的实际URL。

15.2.6. 获取一个部署资源 Get a deployment resource

GET repository/deployments/{deploymentId}/resources/{resourceId}
Table 20. 获取一个部署资源 - URL 参数
参数 必填 描述

deploymentId

String

请求的资源所在的部署的id。

resourceId

String

要获取的资源的id。请确保如果包含斜线符,需要对resourceId进行URL编码。例如,使用’diagrams%2Fmy-process.bpmn20.xml’代替’diagrams/Fmy-process.bpmn20.xml'。

Table 21. 获取一个部署资源 - 响应码
响应码 描述

200

代表已找到部署与资源,并已返回资源。

404

代表未找到请求的部署,或者该部署中没有给定id的资源。状态描述包含了额外信息。

成功响应体:

{
  "id": "diagrams/my-process.bpmn20.xml",
  "url": "http://localhost:8081/activiti-rest/service/repository/deployments/10/resources/diagrams%2Fmy-process.bpmn20.xml",
  "dataUrl": "http://localhost:8081/activiti-rest/service/repository/deployments/10/resourcedata/diagrams%2Fmy-process.bpmn20.xml",
  "mediaType": "text/xml",
  "type": "processDefinition"
}
  • mediaType: 包含了资源的媒体类型。使用(可插入的)MediaTypeResolver解析,默认包含有限数量的媒体类型映射。

  • type: 资源的类型,可用值为:

    • resource: 原始资源。

    • processDefinition: 包含一个或多个流程定义的资源。通过部署器挑选。

    • processImage: 代表流程定义的图形化输出的资源。

15.2.7. 获取一个部署资源的内容 Get a deployment resource content

GET repository/deployments/{deploymentId}/resourcedata/{resourceId}
Table 22. 获取一个部署资源的内容 - URL 参数
参数 必填 描述

deploymentId

String

请求的资源所在的部署的id。

resourceId

String

要获取的资源的id。请确保如果包含斜线符,需要对resourceId进行URL编码。例如,使用’diagrams%2Fmy-process.bpmn20.xml’代替’diagrams/Fmy-process.bpmn20.xml'。

Table 23. 获取一个部署资源的内容 - 响应码
响应码 描述

200

代表已找到部署与资源,并已返回资源。

404

代表未找到请求的部署,或者该部署中没有给定id的资源。状态描述包含了额外信息。

成功响应体:

响应体将包含所请求资源的二进制资源内容。响应的content-type与资源’mimeType’参数返回的类型相同。同时将设置content-disposition头,让浏览器可以下载文件而不是直接显示。

15.3. 流程定义 Process Definitions

15.3.1. 流程定义的列表 List of process definitions

GET repository/process-definitions
Table 24. 流程定义的列表 - URL 参数
参数 必填 描述

version

integer

只返回给定版本的流程定义。

name

String

只返回给定名字的流程定义。

nameLike

String

只返回名字like给定名字的流程定义。

key

String

只返回给定key的流程定义。

keyLike

String

只返回key like给定key的流程定义。

resourceName

String

只返回给定资源名的流程定义。

resourceNameLike

String

只返回资源名like给定资源名的流程定义。

category

String

只返回给定分类的流程定义

categoryLike

String

只返回分类名like给定名字的流程定义。

categoryNotEquals

String

只返回不是给定分类的流程定义。

deploymentId

String

只返回给定id的部署中的流程定义。

startableByUser

String

只返回给定用户可以启动的流程定义。

latest

Boolean

只返回流程定义的最新版本。只能与’key’及’keyLike’参数一起使用,同时使用任何其它参数都将导致400响应。

suspended

Boolean

如果值为true,则只返回暂停的流程定义。如果为值为false,则只返回活动的流程定义(未暂停的)。

sort

name(默认), id, key, category, deploymentId’与’version

用于排序的参数,与’order’一起使用。

可以在这个URL中使用通用分页与排序查询参数

Table 25. 流程定义的列表 - 响应码
响应码 描述

200

代表请求成功,并已返回流程定义。

400

代表某个参数格式错误,或者’latest’与’key', 'keyLike’以外的其他参数一起使用。状态描述中包含了额外信息。

成功响应体:

{
  "data": [
    {
      "id" : "oneTaskProcess:1:4",
      "url" : "http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4",
      "version" : 1,
      "key" : "oneTaskProcess",
      "category" : "Examples",
      "suspended" : false,
      "name" : "The One Task Process",
      "description" : "This is a process for testing purposes",
      "deploymentId" : "2",
      "deploymentUrl" : "http://localhost:8081/repository/deployments/2",
      "graphicalNotationDefined" : true,
      "resource" : "http://localhost:8182/repository/deployments/2/resources/testProcess.xml",
      "diagramResource" : "http://localhost:8182/repository/deployments/2/resources/testProcess.png",
      "startFormDefined" : false
    }
  ],
  "total": 1,
  "start": 0,
  "sort": "name",
  "order": "asc",
  "size": 1
}
  • graphicalNotationDefined: 代表流程定义中包含有图形信息(BPMN DI)。

  • resource: 包含实际部署的BPMN 2.0 XML。

  • diagramResource: 包含流程的图形化表示。如果没有可用流程图则为null。

15.3.2. 获取一个流程定义

GET repository/process-definitions/{processDefinitionId}
Table 26. 获取一个流程定义 - URL 参数
参数 必填 描述

processDefinitionId

String

要获取的流程定义的id。

Table 27. 获取一个流程定义 - 响应码
响应码 描述

200

代表已找到并已返回流程定义。

404

代表未找到请求的流程定义。

成功响应体:

{
  "id" : "oneTaskProcess:1:4",
  "url" : "http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4",
  "version" : 1,
  "key" : "oneTaskProcess",
  "category" : "Examples",
  "suspended" : false,
  "name" : "The One Task Process",
  "description" : "This is a process for testing purposes",
  "deploymentId" : "2",
  "deploymentUrl" : "http://localhost:8081/repository/deployments/2",
  "graphicalNotationDefined" : true,
  "resource" : "http://localhost:8182/repository/deployments/2/resources/testProcess.xml",
  "diagramResource" : "http://localhost:8182/repository/deployments/2/resources/testProcess.png",
  "startFormDefined" : false
}
  • graphicalNotationDefined: 代表流程定义中包含有图形信息(BPMN DI)。

  • resource: 包含实际部署的BPMN 2.0 XML。

  • diagramResource: 包含流程的图形化表示。如果没有可用流程图则为null。

15.3.3. 更新一个流程定义的分类 Update category for a process definition

PUT repository/process-definitions/{processDefinitionId}

JSON体:

{
  "category" : "updatedcategory"
}
Table 28. 更新一个流程定义的分类 - 响应码
响应码 描述

200

代表已修改流程的分类。

400

代表请求体中未定义分类。

404

代表未找到请求的流程定义。

成功响应体:参见repository/process-definitions/{processDefinitionId}的响应。

15.3.4. 获取一个流程定义资源的内容 Get a process definition resource content

GET repository/process-definitions/{processDefinitionId}/resourcedata
Table 29. 获取一个流程定义资源的内容 - URL 参数
参数 必填 描述

processDefinitionId

String

要获取资源的流程定义的id。

响应:

GET repository/deployment/{deploymentId}/resourcedata/{resourceId}完全一样的响应码/响应体。

15.3.5. 获取一个流程定义的BPMN模型 Get a process definition BPMN model

GET repository/process-definitions/{processDefinitionId}/model
Table 30. 获取一个流程定义的BPMN模型 - URL 参数
参数 必填 描述

processDefinitionId

String

要获取模型的流程定义的id。

Table 31. 获取一个流程定义的BPMN模型 - 响应码
响应码 描述

200

代表已找到流程定义,并已返回模型。

404

代表未找到请求的流程定义。

响应体:响应体是一个代表了org.activiti.bpmn.model.BpmnModel的JSON,包含所有流程定义模型。

{
   "processes":[
      {
         "id":"oneTaskProcess",
         "xmlRowNumber":7,
         "xmlColumnNumber":60,
         "extensionElements":{

         },
         "name":"The One Task Process",
         "executable":true,
         "documentation":"One task process description",

    ]
}

15.3.6. 暂停一个流程定义 Suspend a process definition

PUT repository/process-definitions/{processDefinitionId}

JSON体:

{
  "action" : "suspend",
  "includeProcessInstances" : "false",
  "date" : "2013-04-15T00:42:12Z"
}
Table 32. 暂停一个流程定义 - JSON体参数
参数 描述 必填

action

要进行的操作,activatesuspend

includeProcessInstances

是否同时暂停/激活该流程定义的运行中流程实例。如果省略,则流程实例保持原有状态。

date

要进行暂停/激活操作的日期(ISO-8601)。如果省略,则暂停/激活立刻生效。

Table 33. 暂停一个流程定义 - 响应码
响应码 描述

200

代表已暂停流程。

404

代表未找到请求的流程定义。

409

代表请求的流程定义之前已经暂停。

成功响应体:参见repository/process-definitions/{processDefinitionId}的响应

15.3.7. 激活一个流程定义 Activate a process definition

PUT repository/process-definitions/{processDefinitionId}

JSON体:

{
  "action" : "activate",
  "includeProcessInstances" : "true",
  "date" : "2013-04-15T00:42:12Z"
}

参见暂停流程定义的JSON体参数

Table 34. 激活一个流程定义 - 响应码
响应码 描述

200

代表已激活流程。

404

代表未找到请求的流程定义。

409

代表请求的流程定义之前已经激活。

成功响应体:参见repository/process-definitions/{processDefinitionId}的响应

15.3.8. 获取一个流程定义的所有候选启动者 Get all candidate starters for a process-definition

GET repository/process-definitions/{processDefinitionId}/identitylinks
Table 35. 获取一个流程定义的所有候选启动者 - URL 参数
参数 必填 描述

processDefinitionId

String

要获取身份关联的流程定义的id。

Table 36. 获取一个流程定义的所有候选启动者 - 响应码
响应码 描述

200

代表已找到流程定义,并已返回请求的身份关联。

404

代表未找到请求的流程定义。

成功响应体:

[
   {
      "url":"http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4/identitylinks/groups/admin",
      "user":null,
      "group":"admin",
      "type":"candidate"
   },
   {
      "url":"http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4/identitylinks/users/kermit",
      "user":"kermit",
      "group":null,
      "type":"candidate"
   }
]

15.3.9. 为一个流程定义添加一个候选启动者 Add a candidate starter to a process definition

POST repository/process-definitions/{processDefinitionId}/identitylinks
Table 37. 为一个流程定义添加一个候选启动者 - URL 参数
参数 必填 描述

processDefinitionId

String

流程定义的id。

请求体(用户):

{
  "user" : "kermit"
}

请求体(组):

{
  "group" : "sales"
}
Table 38. 为一个流程定义添加一个候选启动者 - 响应码
响应码 描述

201

代表已找到流程定义,并已添加身份关联。

404

代表未找到请求的流程定义。

成功响应体:

{
  "url":"http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4/identitylinks/users/kermit",
  "user":"kermit",
  "group":null,
  "type":"candidate"
}

15.3.10. 从一个流程定义中删除一个候选启动者 Delete a candidate starter from a process definition

DELETE repository/process-definitions/{processDefinitionId}/identitylinks/{family}/{identityId}
Table 39. 从一个流程定义中删除一个候选启动者 - URL 参数
参数 必填 描述

processDefinitionId

String

流程定义的id。

family

String

usersgroups,取决于身份关联的类型。

identityId

String

要从候选启动者中移除的用户或组。

Table 40. 从一个流程定义中删除一个候选启动者 - 响应码
响应码 描述

204

代表已找到流程定义,并已移除该身份关联。响应体设置为空。

404

代表未找到请求的流程定义,或者流程定义中并没有匹配url的身份关联。

成功响应体:

{
  "url":"http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4/identitylinks/users/kermit",
  "user":"kermit",
  "group":null,
  "type":"candidate"
}

15.3.11. 从一个流程定义中获取一个候选启动者 Get a candidate starter from a process definition

GET repository/process-definitions/{processDefinitionId}/identitylinks/{family}/{identityId}
Table 41. 从一个流程定义中获取一个候选启动者 - URL 参数
参数 必填 描述

processDefinitionId

String

流程定义的id。

family

String

usersgroups,取决于身份关联的类型。

identityId

String

作为候选启动者的用户或组。

Table 42. 从一个流程定义中获取一个候选启动者 - 响应码
响应码 描述

200

代表已找到流程定义,并已返回身份关联。

404

代表未找到请求的流程定义,或者流程定义中并没有匹配url的身份关联。

成功响应体:

{
  "url":"http://localhost:8182/repository/process-definitions/oneTaskProcess%3A1%3A4/identitylinks/users/kermit",
  "user":"kermit",
  "group":null,
  "type":"candidate"
}

15.4. 模型 Models

15.4.1. 获取模型的列表 Get a list of models

GET repository/models
Table 43. 获取模型的列表 - URL 查询参数
参数 必填 描述

id

String

只返回给定id的模型。

category

String

只返回给定分类的模型。

categoryLike

String

只返回分类like给定值的模型。使用%字符作为通配符。

categoryNotEquals

String

只返回不是给定分类的模型。

name

String

只返回给定名字的模型。

nameLike

String

只返回名字like给定值的模型。使用%字符作为通配符。

key

String

只返回给定key的模型。

deploymentId

String

只返回在给定部署中部署的模型。

version

Integer

只返回给定版本的模型。

latestVersion

Boolean

如果值为true,则只返回最新版本的模型。最好与key联合使用。如果值为false,忽略本参数,返回所有版本。

deployed

Boolean

如果值为true,只返回已部署的模型。如果值为false,只返回未部署的模型(deploymentId为null)。

tenantId

String

只返回给定tenantId的模型。

tenantIdLike

String

只返回tenantId like给定值的模型。

withoutTenantId

Boolean

如果值为true,则只返回没有设置tenantId的模型。如果值为false,则忽略withoutTenantId参数。

sort

id(默认), category, createTime, key, lastUpdateTime, name, version’或’tenantId

用于排序的参数,与’order’一起使用。

可以在这个URL中使用通用分页与排序查询参数

Table 44. 获取模型的列表 - 响应码
响应码 描述

200

代表查询成功,并已返回模型。

400

代表传递的参数格式错误。状态描述中包含了额外信息。

成功响应体:

{
   "data":[
      {
         "name":"Model name",
         "key":"Model key",
         "category":"Model category",
         "version":2,
         "metaInfo":"Model metainfo",
         "deploymentId":"7",
         "id":"10",
         "url":"http://localhost:8182/repository/models/10",
         "createTime":"2013-06-12T14:31:08.612+0000",
         "lastUpdateTime":"2013-06-12T14:31:08.612+0000",
         "deploymentUrl":"http://localhost:8182/repository/deployments/7",
         "tenantId":null
      },

      ...

   ],
   "total":2,
   "start":0,
   "sort":"id",
   "order":"asc",
   "size":2
}

15.4.2. 获取一个模型 Get a model

GET repository/models/{modelId}
Table 45. 获取一个模型 - URL 参数
参数 必填 描述

modelId

String

要获取的模型的id。

Table 46. 获取一个模型 - 响应码
响应码 描述

200

代表已找到并返回模型。

404

代表未找到请求的模型。

成功响应体:

{
   "id":"5",
   "url":"http://localhost:8182/repository/models/5",
   "name":"Model name",
   "key":"Model key",
   "category":"Model category",
   "version":2,
   "metaInfo":"Model metainfo",
   "deploymentId":"2",
   "deploymentUrl":"http://localhost:8182/repository/deployments/2",
   "createTime":"2013-06-12T12:31:19.861+0000",
   "lastUpdateTime":"2013-06-12T12:31:19.861+0000",
   "tenantId":null
}

15.4.3. 更新一个模型 Update a model

PUT repository/models/{modelId}

请求体:

{
   "name":"Model name",
   "key":"Model key",
   "category":"Model category",
   "version":2,
   "metaInfo":"Model metainfo",
   "deploymentId":"2",
   "tenantId":"updatedTenant"
}

所有的请求值都是可选的。例如,可以只在请求体的JSON对象中包含’name’属性,则只更新模型的名字,而不影响其它任何字段。若明确包含了一个属性,并设置为null,则模型值将更新为null。例如:{"metaInfo" : null}将清空模型的metaInfo。

Table 47. 更新一个模型 - 响应码
响应码 描述

200

代表已找到并更新了模型。

404

代表未找到请求的模型。

成功响应体:

{
   "id":"5",
   "url":"http://localhost:8182/repository/models/5",
   "name":"Model name",
   "key":"Model key",
   "category":"Model category",
   "version":2,
   "metaInfo":"Model metainfo",
   "deploymentId":"2",
   "deploymentUrl":"http://localhost:8182/repository/deployments/2",
   "createTime":"2013-06-12T12:31:19.861+0000",
   "lastUpdateTime":"2013-06-12T12:31:19.861+0000",
   "tenantId":"updatedTenant"
}

15.4.4. 创建一个模型 Create a model

POST repository/models

请求体:

{
   "name":"Model name",
   "key":"Model key",
   "category":"Model category",
   "version":1,
   "metaInfo":"Model metainfo",
   "deploymentId":"2",
   "tenantId":"tenant"
}

所有的请求值都是可选的。例如,可以只在请求体的JSON对象中包含’name’属性,则只设置模型的名字,其它所有字段都为null。

Table 48. 创建一个模型 - 响应码
响应码 描述

201

代表已创建模型。

成功响应体:

{
   "id":"5",
   "url":"http://localhost:8182/repository/models/5",
   "name":"Model name",
   "key":"Model key",
   "category":"Model category",
   "version":1,
   "metaInfo":"Model metainfo",
   "deploymentId":"2",
   "deploymentUrl":"http://localhost:8182/repository/deployments/2",
   "createTime":"2013-06-12T12:31:19.861+0000",
   "lastUpdateTime":"2013-06-12T12:31:19.861+0000",
   "tenantId":"tenant"
}

15.4.5. 删除一个模型 Delete a model

DELETE repository/models/{modelId}
Table 49. 删除一个模型 - URL 参数
参数 必填 描述

modelId

String

要删除的模型的id。

Table 50. 删除一个模型 - 响应码
响应码 描述

204

代表已找到并删除了模型。响应体设置为空。

404

代表未找到请求的模型。

15.4.6. 获取一个模型的编辑器源码 Get the editor source for a model

GET repository/models/{modelId}/source
Table 51. 获取一个模型的编辑器源码 - URL 参数
参数 必填 描述

modelId

String

模型的id。

Table 52. 获取一个模型的编辑器源码 - 响应码
响应码 描述

200

代表已找到模型,并已返回源码。

404

代表未找到请求的模型。

成功响应体:响应体包含了模型的原始编辑器源码。无论源码的content是什么,响应的content-type都设置为application/octet-stream

15.4.7. 设置一个模型的编辑器源码 Set the editor source for a model

PUT repository/models/{modelId}/source
Table 53. 设置一个模型的编辑器源码 - URL 参数
参数 必填 描述

modelId

String

模型的id。

请求体:

请求需要是multipart/form-data类型的。需要有唯一的file-part,包含源码的二进制值。

Table 54. 设置一个模型的编辑器源码 - 响应码
响应码 描述

200

代表已找到模型,并已更新源码。

404

代表未找到请求的模型。

成功响应体:响应体包含了模型的原始编辑器源码。无论源码的content是什么,响应的content-type都设置为application/octet-stream

15.4.8. 获取一个模型的附加编辑器源码 Get the extra editor source for a model

GET repository/models/{modelId}/source-extra
Table 55. 获取一个模型的附加编辑器源码 - URL 参数
参数 必填 描述

modelId

String

模型的id。

Table 56. 获取一个模型的附加编辑器源码 - 响应码
响应码 描述

200

代表已找到模型,并已返回源码。

404

代表未找到请求的模型。

成功响应体:响应体包含了模型的原始编辑器源码。无论源码的content是什么,响应的content-type都设置为application/octet-stream

15.4.9. 设置一个模型的附加编辑器源码 Set the extra editor source for a model

PUT repository/models/{modelId}/source-extra
Table 57. 设置一个模型的附加编辑器源码 - URL 参数
参数 必填 描述

modelId

String

模型的id。

请求体:

请求需要是multipart/form-data类型的。需要有唯一的file-part,包含源码的二进制值。

Table 58. 设置一个模型的附加编辑器源码 - 响应码
响应码 描述

200

代表已找到模型,并已更新附加源码。

404

代表未找到请求的模型。

成功响应体:响应体包含了模型的原始编辑器源码。无论源码的content是什么,响应的content-type都设置为application/octet-stream

15.5. 流程实例 Process Instances

15.5.1. 获取一个流程实例 Get a process instance

GET runtime/process-instances/{processInstanceId}
Table 59. 获取一个流程实例 - URL 参数
参数 必填 描述

processInstanceId

String

要获取的流程实例的id。

Table 60. 获取一个流程实例 - 响应码
响应码 描述

200

代表已找到并已返回流程实例。

404

代表未找到请求的流程实例。

成功响应体:

{
   "id":"7",
   "url":"http://localhost:8182/runtime/process-instances/7",
   "businessKey":"myBusinessKey",
   "suspended":false,
   "processDefinitionUrl":"http://localhost:8182/repository/process-definitions/processOne%3A1%3A4",
   "activityId":"processTask",
   "tenantId": null
}

15.5.2. 删除一个流程实例 Delete a process instance

DELETE runtime/process-instances/{processInstanceId}
Table 61. 删除一个流程实例 - URL 参数
参数 必填 描述

processInstanceId

String

要删除的流程实例的id。

Table 62. 删除一个流程实例 - 响应码
响应码 描述

204

代表已找到并删除了流程实例。响应体设置为空

404

代表未找到请求的流程实例。

15.5.3. 激活或暂停一个流程实例 Activate or suspend a process instance

PUT runtime/process-instances/{processInstanceId}
Table 63. 激活或暂停一个流程实例 - URL 参数
参数 必填 描述

processInstanceId

String

要激活/暂停的流程实例的id。

请求体(暂停):

{
   "action":"suspend"
}

请求体(激活):

{
   "action":"activate"
}
Table 64. 激活或暂停一个流程实例 - 响应码
响应码 描述

200

代表已找到流程实例,并执行了操作。

400

代表提供了非法的操作。

404

代表未找到请求的流程实例。

409

代表请求的流程实例操作无法执行,因为流程实例之前已经激活/暂停了。

15.5.4. 启动一个流程实例 Start a process instance

POST runtime/process-instances

请求体(通过流程定义id启动):

{
   "processDefinitionId":"oneTaskProcess:1:158",
   "businessKey":"myBusinessKey",
   "variables": [
      {
        "name":"myVar",
        "value":"This is a variable",
      }
   ]
}

请求体(通过流程定义key启动):

{
   "processDefinitionKey":"oneTaskProcess",
   "businessKey":"myBusinessKey",
   "tenantId": "tenant1",
   "variables": [
      {
        "name":"myVar",
        "value":"This is a variable",
      }
   ]
}

请求体(通过消息启动):

{
   "message":"newOrderMessage",
   "businessKey":"myBusinessKey",
   "tenantId": "tenant1",
   "variables": [
      {
        "name":"myVar",
        "value":"This is a variable",
      }
   ]
}

请求体中只能使用processDefinitionIdprocessDefinitionKeymessage中的一个。businessKeyvariablestenantId参数是可选的。如果省略了tenantId,则将使用默认租户。关于变量格式的更多信息可以在REST变量章节找到。请注意提供的变量范围将被忽略,流程变量总是local的。

Table 65. 启动一个流程实例 - 响应码
响应码 描述

201

代表已创建流程实例。

400

代表(通过id或key)未找到流程定义,或者发送给定消息并未启动流程,或者传递了不合法的变量。状态描述中包含了关于错误的额外信息。

成功响应体:

{
   "id":"7",
   "url":"http://localhost:8182/runtime/process-instances/7",
   "businessKey":"myBusinessKey",
   "suspended":false,
   "processDefinitionUrl":"http://localhost:8182/repository/process-definitions/processOne%3A1%3A4",
   "activityId":"processTask",
   "tenantId" : null
}

15.5.5. 流程实例的列表 List of process instances

GET runtime/process-instances
Table 66. 流程实例的列表 - URL 查询参数
参数 必填 描述

id

String

只返回给定id的流程实例。

processDefinitionKey

String

只返回给定流程定义key的流程实例。

processDefinitionId

String

只返回给定流程定义id的流程实例。

businessKey

String

只返回给定businessKey的流程实例。

involvedUser

String

只返回给定用户参与的流程实例。

suspended

Boolean

如果值为true,则只返回暂停的流程实例。如果值为false,则只返回未暂停(激活)的流程实例。

superProcessInstanceId

String

只返回给定父流程实例id的流程实例(使用调用活动的流程)。

subProcessInstanceId

String

只返回给定子流程实例id的流程实例(通过调用活动启动的流程)。

excludeSubprocesses

Boolean

只返回不是子流程的流程实例。

includeProcessVariables

Boolean

是否在结果中包含流程变量。

tenantId

String

只返回给定tenantId的流程实例。

tenantIdLike

String

只返回tenantId like给定值的流程实例。

withoutTenantId

Boolean

如果值为true,则只返回没有设置tenantId的流程实例。如果值为false,则忽略withoutTenantId参数。

sort

String

排序字段,需要是id(默认), processDefinitionId, tenantIdprocessDefinitionKey中的一个。

可以在这个URL中使用通用分页与排序查询参数

Table 67. 流程实例的列表 - 响应码
响应码 描述

200

代表请求成功,并已返回流程实例。

400

代表传递的参数格式错误。状态描述中包含了额外信息。

成功响应体:

{
   "data":[
      {
         "id":"7",
         "url":"http://localhost:8182/runtime/process-instances/7",
         "businessKey":"myBusinessKey",
         "suspended":false,
         "processDefinitionUrl":"http://localhost:8182/repository/process-definitions/processOne%3A1%3A4",
         "activityId":"processTask",
         "tenantId" : null
      }


   ],
   "total":2,
   "start":0,
   "sort":"id",
   "order":"asc",
   "size":2
}

15.5.6. 查询流程实例 Query process instances

POST query/process-instances

请求体:

{
  "processDefinitionKey":"oneTaskProcess",
  "variables":
  [
    {
        "name" : "myVariable",
        "value" : 1234,
        "operation" : "equals",
        "type" : "long"
    }
  ]
}

请求体可以包含所有在流程实例的列表URL查询中可用的过滤器。另外,也可以在查询中包含一个变量的数组,使用这里描述的格式。

可以在这个URL中使用通用分页与排序查询参数

Table 68. 查询流程实例 - 响应码
响应码 描述

200

代表请求成功,并已返回流程实例。

400

代表传递的参数格式错误。状态描述中包含了额外信息。

成功响应体:

{
   "data":[
      {
         "id":"7",
         "url":"http://localhost:8182/runtime/process-instances/7",
         "businessKey":"myBusinessKey",
         "suspended":false,
         "processDefinitionUrl":"http://localhost:8182/repository/process-definitions/processOne%3A1%3A4",
         "activityId":"processTask",
         "tenantId" : null
      }


   ],
   "total":2,
   "start":0,
   "sort":"id",
   "order":"asc",
   "size":2
}

15.5.7. 获取一个流程实例的流程图 Get diagram for a process instance

GET runtime/process-instances/{processInstanceId}/diagram
Table 69. 获取一个流程实例的流程图 - URL 参数
参数 必填 描述

processInstanceId

String

要获取流程图的流程实例的id。

Table 70. 获取一个流程实例的流程图 - 响应码
响应码 描述

200

代表已找到流程实例,并已返回流程图。

400

代表已找到请求的流程实例,但该流程未包含任何图形信息(BPMN:DI),因此不能创建流程图。

404

代表未找到请求的流程实例。

成功响应体:

{
   "id":"7",
   "url":"http://localhost:8182/runtime/process-instances/7",
   "businessKey":"myBusinessKey",
   "suspended":false,
   "processDefinitionUrl":"http://localhost:8182/repository/process-definitions/processOne%3A1%3A4",
   "activityId":"processTask"
}

15.5.8. 获取流程实例的参与人 Get involved people for process instance

GET runtime/process-instances/{processInstanceId}/identitylinks
Table 71. 获取流程实例的参与人 - URL 参数
参数 必填 描述

processInstanceId

String

要查询关联的流程实例的id。

Table 72. 获取流程实例的参与人 - 响应码
响应码 描述

200

代表已找到流程实例,并已返回关联。

404

代表未找到请求的流程实例。

成功响应体:

[
   {
      "url":"http://localhost:8182/runtime/process-instances/5/identitylinks/users/john/customType",
      "user":"john",
      "group":null,
      "type":"customType"
   },
   {
      "url":"http://localhost:8182/runtime/process-instances/5/identitylinks/users/paul/candidate",
      "user":"paul",
      "group":null,
      "type":"candidate"
   }
]

请注意group永远为null,因为只有用户才能参与流程实例。

15.5.9. 为一个流程实例添加一个参与用户 Add an involved user to a process instance

POST runtime/process-instances/{processInstanceId}/identitylinks
Table 73. 为一个流程实例添加一个参与用户 - URL 参数
参数 必填 描述

processInstanceId

String

要添加关联的流程实例。

请求体:

{
  "user":"kermit",
  "type":"participant"
}

usertype都是必填的。

Table 74. 为一个流程实例添加一个参与用户 - 响应码
响应码 描述

201

代表已找到流程实例,并已返回关联。

400

代表请求体中未包含user或type。

404

代表未找到请求的流程实例。

成功响应体:

{
   "url":"http://localhost:8182/runtime/process-instances/5/identitylinks/users/john/customType",
   "user":"john",
   "group":null,
   "type":"customType"
}

请注意group永远为null,因为只有用户才能参与流程实例。

15.5.10. 从一个流程实例中移除一个参与用户 Remove an involved user to from process instance

DELETE runtime/process-instances/{processInstanceId}/identitylinks/users/{userId}/{type}
Table 75. 从一个流程实例中移除一个参与用户 - URL 参数
参数 必填 描述

processInstanceId

String

流程实例的id。

userId

String

要删除关联的用户的id。

type

String

要删除的关联的类型。

Table 76. 从一个流程实例中移除一个参与用户 - 响应码
响应码 描述

204

代表已找到流程实例,并已删除关联。响应体设置为空。

404

代表未找到请求的流程实例,或者要删除的关联不存在。响应状态中包含了关于错误的额外信息。

成功响应体:

{
   "url":"http://localhost:8182/runtime/process-instances/5/identitylinks/users/john/customType",
   "user":"john",
   "group":null,
   "type":"customType"
}

请注意group永远为null,因为只有用户才能参与流程实例。

15.5.11. 一个流程实例的变量的列表 List of variables for a process instance

GET runtime/process-instances/{processInstanceId}/variables
Table 77. 一个流程实例的变量的列表 - URL 参数
参数 必填 描述

processInstanceId

String

要列表变量的流程实例的id。

Table 78. 一个流程实例的变量的列表 - 响应码
响应码 描述

200

代表已找到流程实例,并已返回变量。

404

代表未找到请求的流程实例。

成功响应体:

[
   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"local"
   },
   {
      "name":"byteArrayProcVar",
      "type":"binary",
      "value":null,
      "valueUrl":"http://localhost:8182/runtime/process-instances/5/variables/byteArrayProcVar/data",
      "scope":"local"
   }
]

如果变量是二进制变量或序列化值,则valueUrl指向获取原始值的URL。如果是一个简单变量,则在响应中显示值。请注意只会返回local范围的变量,因为流程实例变量没有global范围。

15.5.12. 获取一个流程实例的一个变量 Get a variable for a process instance

GET runtime/process-instances/{processInstanceId}/variables/{variableName}
Table 79. 获取一个流程实例的一个变量 - URL 参数
参数 必填 描述

processInstanceId

String

要获取变量的流程实例的id。

variableName

String

要获取的变量的名字。

Table 80. 获取一个流程实例的一个变量 - 响应码
响应码 描述

200

代表已找到流程实例及变量,并已返回变量。

400

代表请求体不完整,或者包含不合法值。状态描述中包含了关于错误的额外信息。

404

代表未找到请求的流程实例,或者流程实例中没有给定名字的变量。状态描述中包含了关于错误的额外信息。

成功响应体:

   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"local"
   }

如果变量是二进制变量或序列化值,则valueUrl指向获取原始值的URL。如果是一个简单变量,则在响应中显示值。请注意只会返回local范围的变量,因为流程实例变量没有global范围。

15.5.13. 为一个流程实例创建(或更新)变量 Create (or update) variables on a process instance

POST runtime/process-instances/{processInstanceId}/variables
PUT runtime/process-instances/{processInstanceId}/variables

当使用POST时,会创建所有传递的变量。如果流程实例中已经存在某个变量,则请求结果为错误(409 - 冲突)。当使用PUT时,会创建流程实例中不存在的变量;已存在变量的将会被覆盖,而没有错误。

Table 81. 为一个流程实例创建(或更新)变量 - URL 参数
参数 必填 描述

processInstanceId

String

要操作变量的流程实例的id。

请求体:

[
   {
      "name":"intProcVar",
      "type":"integer",
      "value":123
   },

   ...
]

请求体的数组中可以传递任意数量的变量。可以在REST变量章节找到关于变量格式的更多信息。请注意范围将被忽略,流程实例中只能设置local变量。

Table 82. 为一个流程实例创建(或更新)变量 - 响应码
响应码 描述

201

代表已找到流程实例,并已创建变量。

400

代表请求体不完整,或含有非法值。状态描述中包含了关于错误的额外信息。

404

代表未找到请求的流程实例。

409

代表已找到流程实例,但其已包含了给定名字的变量(只在使用POST方法时抛出)。改用更新方法。

成功响应体:

[
   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"local"
   },

   ...

]

15.5.14. 为一个流程实例更新一个变量 Update a single variable on a process instance

PUT runtime/process-instances/{processInstanceId}/variables/{variableName}
Table 83. 为一个流程实例更新一个变量 - URL 参数
参数 必填 描述

processInstanceId

String

要操作变量的流程实例的id。

variableName

String

要更新的变量的名字。

请求体:

 {
    "name":"intProcVar"
    "type":"integer"
    "value":123
 }

请求体的数组中可以传递任意数量的变量。可以在REST变量章节找到关于变量格式的更多信息。请注意范围将被忽略,流程实例中只能设置local变量。

Table 84. 为一个流程实例更新一个变量 - 响应码
响应码 描述

200

代表已找到流程实例与变量,并已更新变量。

404

代表未找到请求的流程实例,或者流程实例中没有给定名字的变量。状态描述中包含了关于错误的额外信息。

成功响应体:

{
  "name":"intProcVar",
  "type":"integer",
  "value":123,
  "scope":"local"
}

如果变量是二进制变量或序列化值,则valueUrl指向获取原始值的URL。如果是一个简单变量,则在响应中显示值。请注意只会返回local范围的变量,因为流程实例变量没有global范围。

15.5.15. 为一个流程实例创建一个新的二进制变量 Create a new binary variable on a process-instance

POST runtime/process-instances/{processInstanceId}/variables
Table 85. 为一个流程实例创建一个新的二进制变量 - URL 参数
参数 必填 描述

processInstanceId

String

要创建新变量的流程实例的id。

请求体:请求需要是multipart/form-data类型的。需要有唯一的file-part,包含变量的二进制值。另外,也可以使用下列额外的form-fields:

  • name: 变量需要的名字。

  • type: 创建的变量的类型。如果省略,则使用binary,并且将请求中的二进制数据保存为字节数组。

成功响应体:

{
  "name" : "binaryVariable",
  "scope" : "local",
  "type" : "binary",
  "value" : null,
  "valueUrl" : "http://.../runtime/process-instances/123/variables/binaryVariable/data"
}
Table 86. 为一个流程实例创建一个新的二进制变量 - 响应码
响应码 描述

201

代表已创建变量,并已返回结果。

400

代表缺少要创建的变量的名字。状态消息提供了额外信息。

404

代表未找到请求的流程实例。

409

代表流程实例中已经有给定名字的变量。改用PUT方法更新任务变量。

415

代表序列化数据中包含了一个运行Activiti引擎的JVM中不存在的类的对象,因此不能反序列化。

15.5.16. 为一个流程实例更新一个已有的二进制变量 Update an existing binary variable on a process-instance

PUT runtime/process-instances/{processInstanceId}/variables
Table 87. 为一个流程实例更新一个已有的二进制变量 - URL 参数
参数 必填 描述

processInstanceId

String

要更新变量的流程实例的id。

请求体:请求需要是multipart/form-data类型的。需要有唯一的file-part,包含变量的二进制值。另外,也可以使用下列额外的form-fields:

  • name: 变量需要的名字。

  • type: 创建的变量的类型。如果省略,则使用binary,并且将请求中的二进制数据保存为字节数组。

成功响应体:

{
  "name" : "binaryVariable",
  "scope" : "local",
  "type" : "binary",
  "value" : null,
  "valueUrl" : "http://.../runtime/process-instances/123/variables/binaryVariable/data"
}
Table 88. 为一个流程实例更新一个已有的二进制变量 - 响应码
响应码 描述

200

代表已更新变量,并已返回结果。

400

代表缺少要更新的变量的名字。状态消息提供了额外信息。

404

代表未找到请求的流程实例,或者流程实例中没有给定名字的变量。

415

代表序列化数据中包含了一个运行Activiti引擎的JVM中不存在的类的对象,因此不能反序列化。

15.6. 执行 Executions

15.6.1. 获取一个执行 Get an execution

GET runtime/executions/{executionId}
Table 89. 获取一个执行 - URL 参数
参数 必填 描述

executionId

String

要获取的执行的id。

Table 90. 获取一个执行 - 响应码
响应码 描述

200

代表已找到并返回执行。

404

代表未找到执行

成功响应体:

{
   "id":"5",
   "url":"http://localhost:8182/runtime/executions/5",
   "parentId":null,
   "parentUrl":null,
   "processInstanceId":"5",
   "processInstanceUrl":"http://localhost:8182/runtime/process-instances/5",
   "suspended":false,
   "activityId":null,
   "tenantId": null
}

15.6.2. 对一个执行进行操作 Execute an action on an execution

PUT runtime/executions/{executionId}
Table 91. 对一个执行进行操作 - URL 参数
参数 必填 描述

executionId

String

要进行操作的执行的id。

请求体(为一个执行发信号):

{
  "action":"signal"
}

请求体(为一个执行发信号接收事件):

{
  "action":"signalEventReceived",
  "signalName":"mySignal",
  "variables": [  ]
}

通知执行:已经接收到了一个信号事件。要求有一个signalName参数。可以传递可选的variables,将在进行操作前设置到执行中。

请求体(为一个执行发消息接收事件):

{
  "action":"messageEventReceived",
  "messageName":"myMessage",
  "variables": [  ]
}

通知执行:已经接收到了一个消息事件。要求有一个messageName参数。可以传递可选的variables,将在进行操作前设置到执行中。

Table 92. 对一个执行进行操作 - 响应码
响应码 描述

200

代表已找到执行,并进行了操作。

204

代表已找到执行,进行了操作,该操作导致执行结束。

400

代表请求了非法的操作,请求中缺少必要的参数,或者传递了非法的变量。状态描述中包含了关于错误的额外信息。

404

代表未找到执行

成功响应体(当执行并未因该操作结束时):

{
   "id":"5",
   "url":"http://localhost:8182/runtime/executions/5",
   "parentId":null,
   "parentUrl":null,
   "processInstanceId":"5",
   "processInstanceUrl":"http://localhost:8182/runtime/process-instances/5",
   "suspended":false,
   "activityId":null,
   "tenantId" : null
}

15.6.3. 获取一个执行中的激活活动 Get active activities in an execution

GET runtime/executions/{executionId}/activities

返回执行中与所有子执行中(以及它们的子执行,递归)激活的所有活动。

Table 93. 获取一个执行中的激活活动 - URL 参数
参数 必填 描述

executionId

String

要获取活动的执行的id。

Table 94. 获取一个执行中的激活活动 - 响应码
响应码 描述

200

代表已找到执行,并已返回活动。

404

代表未找到执行

成功响应体:

[
  "userTaskForManager",
  "receiveTask"
]

15.6.4. 执行的列表 List of executions

GET runtime/executions
Table 95. 执行的列表 - URL 查询参数
参数 必填 描述

id

String

只返回给定id的执行。

activityId

String

只返回给定activity id的执行。

processDefinitionKey

String

只返回给定流程定义key的执行。

processDefinitionId

String

只返回给定流程定义id的执行。

processInstanceId

String

只返回给定流程实例中的执行。

messageEventSubscriptionName

String

只返回订阅了给定名字的消息的执行。

signalEventSubscriptionName

String

只返回订阅了给定名字的信号的执行。

parentId

String

只返回给定执行的直接子执行。

tenantId

String

只返回给定tenantId的执行。

tenantIdLike

String

只返回tenantId like给定值的执行。

withoutTenantId

Boolean

如果值为true,则只返回未设置tenantId的执行。如果值为false,则忽略withoutTenantId参数。

sort

String

排序字段,需要为processInstanceId(默认), processDefinitionId, processDefinitionKeytenantId

可以在这个URL中使用通用分页与排序查询参数

Table 96. 执行的列表 - 响应码
响应码 描述

200

代表请求成功,并已返回执行。

400

代表传递的参数格式错误。状态描述中包含了额外信息。

成功响应体:

{
   "data":[
      {
         "id":"5",
         "url":"http://localhost:8182/runtime/executions/5",
         "parentId":null,
         "parentUrl":null,
         "processInstanceId":"5",
         "processInstanceUrl":"http://localhost:8182/runtime/process-instances/5",
         "suspended":false,
         "activityId":null,
         "tenantId":null
      },
      {
         "id":"7",
         "url":"http://localhost:8182/runtime/executions/7",
         "parentId":"5",
         "parentUrl":"http://localhost:8182/runtime/executions/5",
         "processInstanceId":"5",
         "processInstanceUrl":"http://localhost:8182/runtime/process-instances/5",
         "suspended":false,
         "activityId":"processTask",
         "tenantId":null
      }
   ],
   "total":2,
   "start":0,
   "sort":"processInstanceId",
   "order":"asc",
   "size":2
}

15.6.5. 查询执行 Query executions

POST query/executions

请求体:

{
  "processDefinitionKey":"oneTaskProcess",
  "variables":
  [
    {
        "name" : "myVariable",
        "value" : 1234,
        "operation" : "equals",
        "type" : "long"
    }
  ],
  "processInstanceVariables":
  [
    {
        "name" : "processVariable",
        "value" : "some string",
        "operation" : "equals",
        "type" : "string"
    }
  ]
}

请求体可以包含所有在列表执行URL查询中可用的过滤器。另外,也可以在查询中包含一个variablesprocessInstanceVariables的数组,使用这里描述的格式。

可以在这个URL中使用通用分页与排序查询参数

Table 97. 查询执行 - 响应码
响应码 描述

200

代表请求成功,并已返回执行。

400

代表传递的参数格式错误 . 状态描述中包含了额外信息。

成功响应体:

{
   "data":[
      {
         "id":"5",
         "url":"http://localhost:8182/runtime/executions/5",
         "parentId":null,
         "parentUrl":null,
         "processInstanceId":"5",
         "processInstanceUrl":"http://localhost:8182/runtime/process-instances/5",
         "suspended":false,
         "activityId":null,
         "tenantId":null
      },
      {
         "id":"7",
         "url":"http://localhost:8182/runtime/executions/7",
         "parentId":"5",
         "parentUrl":"http://localhost:8182/runtime/executions/5",
         "processInstanceId":"5",
         "processInstanceUrl":"http://localhost:8182/runtime/process-instances/5",
         "suspended":false,
         "activityId":"processTask",
         "tenantId":null
      }
   ],
   "total":2,
   "start":0,
   "sort":"processInstanceId",
   "order":"asc",
   "size":2
}

15.6.6. 一个执行中的变量的列表 List of variables for an execution

GET runtime/executions/{executionId}/variables?scope={scope}
Table 98. 一个执行中的变量的列表 - URL 参数
参数 必填 描述

executionId

String

要列表变量的执行的id。

scope

String

可以为localglobal。若省略,则同时返回本地与全局范围的变量。

Table 99. 一个执行中的变量的列表 - 响应码
响应码 描述

200

代表已找到执行,并已返回变量。

404

代表未找到请求的执行。

成功响应体:

[
   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"global"
   },
   {
      "name":"byteArrayProcVar",
      "type":"binary",
      "value":null,
      "valueUrl":"http://localhost:8182/runtime/process-instances/5/variables/byteArrayProcVar/data",
      "scope":"local"
   }


]

如果变量是二进制变量或序列化值,则valueUrl指向获取原始值的URL。如果是一个简单变量,则在响应中显示值。

15.6.7. 获取一个执行的一个变量 Get a variable for an execution

GET runtime/executions/{executionId}/variables/{variableName}?scope={scope}
Table 100. 获取一个执行的一个变量 - URL 参数
参数 必填 描述

executionId

String

要获取变量的执行的id

variableName

String

要获取的变量的名字。

scope

String

localglobal。若省略,则(存在时)返回本地变量。不存在本地变量时,则(存在时)返回全局变量。

Table 101. 获取一个执行的一个变量 - 响应码
响应码 描述

200

代表已找到执行与变量,并已返回变量。

400

代表请求体不完整,或者包含不合法值。状态描述中包含了关于错误的额外信息。

404

代表未找到请求的执行,或者在请求的范围内执行没有给定名字的变量(若省略了范围查询条件,则本地与全局范围中都没有该变量)。状态描述中包含了关于错误的额外信息。

成功响应体:

   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"local"
   }

如果变量是二进制变量或序列化值,则valueUrl指向获取原始值的URL。如果是一个简单变量,则在响应中显示值。

15.6.8. 为一个执行创建(或更新)变量 Create (or update) variables on an execution

POST runtime/executions/{executionId}/variables
PUT runtime/executions/{executionId}/variables

当使用POST时,会创建所有传递的变量。如果执行中已经存在某个变量,则请求结果为错误(409 - 冲突)。当使用PUT时,会创建执行中不存在的变量;已存在变量的将会被覆盖,而没有错误。

Table 102. 为一个执行创建(或更新)变量 - URL 参数
参数 必填 描述

executionId

String

要操作变量的执行的id。

请求体:

[
   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"local"
   }



]

请注意只能提供相同范围的变量。如果请求体数组中包含了不同范围的变量,则请求结果为错误(400 - 错误请求)。请求体的数组中可以传递任意数量的变量。可以在REST变量章节找到关于变量格式的更多信息。

Table 103. 为一个执行创建(或更新)变量 - 响应码
响应码 描述

201

代表已找到执行,并已创建变量。

400

代表请求体不完整,或者包含不合法值。状态描述中包含了关于错误的额外信息。

404

代表未找到请求的执行。

409

代表已找到执行,但其已包含了给定名字的变量(只在使用POST方法时抛出)。改用更新方法。

成功响应体:

[
   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"local"
   }



]

15.6.9. 为一个执行更新一个变量 Update a variable on an execution

PUT runtime/executions/{executionId}/variables/{variableName}
Table 104. 为一个执行更新一个变量 - URL 参数
参数 必填 描述

executionId

String

要更新变量的执行的id。

variableName

String

要更新的变量的名字。

请求体:

 {
    "name":"intProcVar"
    "type":"integer"
    "value":123,
    "scope":"global"
 }

可以在REST变量章节找到关于变量格式的更多信息。

Table 105. 为一个执行更新一个变量 - 响应码
响应码 描述

200

代表已找到执行与变量,并已更新变量。

404

代表未找到请求的执行,或者执行没有给定名字的变量。状态描述中包含了关于错误的额外信息。

成功响应体:

   {
      "name":"intProcVar",
      "type":"integer",
      "value":123,
      "scope":"global"
   }

如果变量是二进制变量或序列化值,则valueUrl指向获取原始值的URL。如果是一个简单变量,则在响应中显示值。

15.6.10. 为一个执行创建一个新的二进制变量 Create a new binary variable on an execution

POST runtime/executions/{executionId}/variables
Table 106. 为一个执行创建一个新的二进制变量 - URL 参数
参数 必填 描述

executionId

String

要创建新变量的执行的id。

请求体:请求需要是multipart/form-data类型的。需要有唯一的file-part,包含变量的二进制值。另外,也可以使用下列额外的form-fields:

  • name: 变量需要的名字。

  • type: 创建的变量的类型。如果省略,则使用binary,并且将请求中的二进制数据保存为字节数组。

  • scope: 创建变量的范围。如果省略,则使用local

成功响应体:

{
  "name" : "binaryVariable",
  "scope" : "local",
  "type" : "binary",
  "value" : null,
  "valueUrl" : "http://.../runtime/executions/123/variables/binaryVariable/data"
}
Table 107. 为一个执行创建一个新的二进制变量 - 响应码
响应码 描述

201

代表已创建变量,并已返回结果。

400

代表缺少要创建的变量的名字。状态消息提供了额外信息。

404

代表未找到请求的执行。

409

代表执行已包含了给定名字的变量(只在使用POST方法时抛出)。改用PUT方法更新变量。

415

代表序列化数据中包含了一个运行Activiti引擎的JVM中不存在的类的对象,因此不能反序列化。

15.6.11. 为一个执行更新一个已有二进制变量 Update an existing binary variable on a process-instance

PUT runtime/executions/{executionId}/variables/{variableName}
Table 108. 为一个执行更新一个已有二进制变量 - URL 参数
参数 必填 描述

executionId

String

要更新变量的执行的id。

variableName

String

要更新的变量的名字。

请求体:请求需要是multipart/form-data类型的。需要有唯一的file-part,包含变量的二进制值。另外,也可以使用下列额外的form-fields:

  • name: 变量需要的名字。

  • type: 创建的变量的类型。如果省略,则使用binary,并且将请求中的二进制数据保存为字节数组。

  • scope: 创建变量的范围。如果省略,则使用local

成功响应体:

{
  "name" : "binaryVariable",
  "scope" : "local",
  "type" : "binary",
  "value" : null,
  "valueUrl" : "http://.../runtime/executions/123/variables/binaryVariable/data"
}
Table 109. 为一个执行更新一个已有二进制变量 - 响应码
响应码 描述

200

代表已更新变量,并已返回结果。

400

代表缺少要更新的变量的名字。状态消息提供了额外信息。

404

代表未找到请求的执行,或者执行中没有给定名字的变量。

415

代表序列化数据中包含了一个运行Activiti引擎的JVM中不存在的类的对象,因此不能反序列化。

15.7. 任务 Tasks

15.7.1. 获取一个任务 Get a task

GET runtime/tasks/{taskId}
Table 110. 获取一个任务 - URL 参数
参数 必填 描述

taskId

String

要获取的任务的id。

Table 111. 获取一个任务 - 响应码
响应码 描述

200

代表已找到并返回了任务。

404

代表未找到请求的任务。

成功响应体:

{
  "assignee" : "kermit",
  "createTime" : "2013-04-17T10:17:43.902+0000",
  "delegationState" : "pending",
  "description" : "Task description",
  "dueDate" : "2013-04-17T10:17:43.902+0000",
  "executionUrl" : "http://localhost:8182/runtime/executions/5",
  "executionId" : "5",
  "id" : "8",
  "name" : "My task",
  "owner" : "owner",
  "parentTaskUrl" : "http://localhost:8182/runtime/tasks/9",
  "parentTaskId" : "9",
  "priority" : 50,
  "processDefinitionUrl" : "http://localhost:8182/repository/process-definitions/oneTaskPr
ocess%3A1%3A4",
  "processDefinitionId" : "oneTaskProcess%3A1%3A4",
  "processInstanceUrl" : "http://localhost:8182/runtime/process-instances/5",
  "processInstanceId" : "5",
  "suspended" : false,
  "formKey" : null,
  "category": "examples",
  "taskDefinitionKey" : "theTask",
  "url" : "http://localhost:8182/runtime/tasks/8",
  "tenantId" : null
}
  • delegationState: 任务的代理状态,可以为null"pending""resolved"

15.7.2. 任务的列表 List of tasks

GET runtime/tasks
Table 112. 任务的列表 - URL 查询参数
参数 必填 描述

name

String

只返回给定名字的任务。

nameLike

String

只返回名字like给定名字的任务。

description

String

只返回给定描述的任务。

priority

Integer

只返回给定优先级的任务。

minimumPriority

Integer

只返回优先级高于给定值的任务。

maximumPriority

Integer

只返回优先级低于给定值的任务。

assignee

String

只返回指派至给定用户的任务。

assigneeLike

String

只返回办理人like给定值的任务。

owner

String

只返回属主为给定用户的任务。

ownerLike

String

只返回属主like给定值的任务。

unassigned

Boolean

只返回未指派的任务。如果传递false,则忽略本参数。

delegationState

String

只返回给定代理状态的任务。可用值为pendingresolved

candidateUser

String

只返回可以被给定用户申领的任务。包括用户为候选人,以及用户所在组为候选组的任务。

candidateGroup

String

只返回可以被给定组中的用户申领的任务。

candidateGroups

String

只返回可以被给定组中的用户申领的任务。逗号分隔值。

involvedUser

String

只返回给定用户参与的任务。

taskDefinitionKey

String

只返回给定任务定义id的任务。

taskDefinitionKeyLike

String

只返回任务定义id like给定值的任务。

processInstanceId

String

只返回给定id流程实例中的任务。

processInstanceBusinessKey

String

只返回给定businessKey流程实例中的任务。

processInstanceBusinessKeyLike

String

只返回businessKey like给定值的流程实例中的任务。

processDefinitionId

String

只返回给定id的流程定义的流程实例中的任务。

processDefinitionKey

String

只返回给定key的流程定义的流程实例中的任务。

processDefinitionKeyLike

String

只返回key like给定值的流程定义的流程实例中的任务。

processDefinitionName

String

只返回给定名字的流程定义的流程实例中的任务。

processDefinitionNameLike

String

只返回名字like给定值的流程定义的流程实例中的任务。

executionId

String

只返回给定id的执行中的任务。

createdOn

ISO Date

只返回给定日期创建的任务。

createdBefore

ISO Date

只返回给定日期之前创建的任务。

createdAfter

ISO Date

只返回给定日期之后创建的任务。

dueOn

ISO Date

只返回给定日期到期的任务。

dueBefore

ISO Date

只返回给定日期前到期的任务。

dueAfter

ISO Date

只返回给定日期后到期的任务。

withoutDueDate

boolean

只返回没有到期日期的任务。如果值为false则忽略本参数。

excludeSubTasks

Boolean

只返回不是另一个任务的子任务的任务。

active

Boolean

如果值为true,则只返回未暂停(要么所在流程未暂停,要么根本不在流程中)的任务。如果值为false,则只返回已暂停流程实例中的任务。

includeTaskLocalVariables

Boolean

是否在结果中包含本地变量。

includeProcessVariables

Boolean

是否在结果中包含流程变量。

tenantId

String

只返回给定tenantId的任务。

tenantIdLike

String

只返回tenantId like给定值的任务。

withoutTenantId

Boolean

如果值为true,则只返回未设置tenantId的任务。如果值为false,则忽略withoutTenantId参数。

candidateOrAssigned

String

选择已被申领,或已指派给用户,或等待用户(候选用户或组)申领的任务。

category

string

选择给定分类的任务。请注意这是任务的分类,而不是流程定义的分类(即BPMN Xml内的namespace)。

可以在这个URL中使用通用分页与排序查询参数

Table 113. 任务的列表 - 响应码
响应码 描述

200

代表请求成功,并已返回任务。

400

代表传递的参数格式错误,或’delegationState’使用了不合法的值(不是’pending’与’resolved')。状态描述中包含了额外信息。

成功响应体:

{
  "data": [
    {
      "assignee" : "kermit",
      "createTime" : "2013-04-17T10:17:43.902+0000",
      "delegationState" : "pending",
      "description" : "Task description",
      "dueDate" : "2013-04-17T10:17:43.902+0000",
      "executionUrl" : "http://localhost:8182/runtime/executions/5",
      "executionId" : "5",
      "id" : "8",
      "name" : "My task",
      "owner" : "owner",
      "parentTaskUrl" : "http://localhost:8182/runtime/tasks/9",
      "parentTaskId" : "9",
      "priority" : 50,
      "processDefinitionUrl" : "http://localhost:8182/repository/process-definitions/oneTa
skProcess%3A1%3A4",
      "processDefinitionId" : "oneTaskProcess%3A1%3A4",
      "processInstanceUrl" : "http://localhost:8182/runtime/process-instances/5",
      "processInstanceId" : "5",
      "suspended" : false,
      "taskDefinitionKey" : "theTask",
      "url" : "http://localhost:8182/runtime/tasks/8",
      "formKey" : null,
      "category": "examples",
      "tenantId" : null
    }
  ],
  "total": 1,
  "start": 0,
  "sort": "name",
  "order": "asc",
  "size": 1
}

15.7.3. 查询任务 Query for tasks

POST query/tasks

请求体:

{
  "name" : "My task",
  "description" : "The task description",

  ...

  "taskVariables" : [
    {
      "name" : "myVariable",
      "value" : 1234,
      "operation" : "equals",
      "type" : "long"
    }
  ],

    "processInstanceVariables" : [
      {
         ...
      }
    ]
  ]
}

支持的JSON参数字段与获取任务集合的参数一模一样(除了candidateGroupIn,只能在POST任务查询REST服务中使用),但是使用JSON体参数而不是URL参数,可以进行更高级的查询,也可以避免请求URI太长的错误。另外,查询可以通过任务与流程变量进行过滤。taskVariablesprocessInstanceVariables都是JSON数组,包含这里描述的格式的对象。

Table 114. 查询任务 - 响应码
响应码 描述

200

代表请求成功,并已返回任务。

400

代表传递的参数格式错误,或’delegationState’使用了不合法的值(不是’pending’与’resolved')。状态描述中包含了额外信息。

成功响应体:

{
  "data": [
    {
      "assignee" : "kermit",
      "createTime" : "2013-04-17T10:17:43.902+0000",
      "delegationState" : "pending",
      "description" : "Task description",
      "dueDate" : "2013-04-17T10:17:43.902+0000",
      "executionUrl" : "http://localhost:8182/runtime/executions/5",
      "executionId" : "5",
      "id" : "8",
      "name" : "My task",
      "owner" : "owner",
      "parentTaskUrl" : "http://localhost:8182/runtime/tasks/9",
      "parentTaskId" : "9",
      "priority" : 50,
      "processDefinitionUrl" : "http://localhost:8182/repository/process-definitions/oneTa
skProcess%3A1%3A4",
      "processDefinitionId" : "oneTaskProcess%3A1%3A4",
      "processInstanceUrl" : "http://localhost:8182/runtime/process-instances/5",
      "processInstanceId" : "5",
      "suspended" : false,
      "taskDefinitionKey" : "theTask",
      "url" : "http://localhost:8182/runtime/tasks/8",
      "formKey" : null,
      "category": "examples",
      "tenantId" : null
    }
  ],
  "total": 1,
  "start": 0,
  "sort": "name",
  "order": "asc",
  "size": 1
}

15.7.4. 更新一个任务 Update a task

PUT runtime/tasks/{taskId}

JSON体:

{
  "assignee" : "assignee",
  "delegationState" : "resolved",
  "description" : "New task description",
  "dueDate" : "2013-04-17T13:06:02.438+02:00",
  "name" : "New task name",
  "owner" : "owner",
  "parentTaskId" : "3",
  "priority" : 20
}

所有的请求值都是可选的。例如,可以只在请求体的JSON对象中包含’assignee’属性,则只更新任务的办理人,而不影响其它任何字段。若明确包含了一个属性,并设置为null,则任务值将更新为null。例如:{"dueDate" : null}将清空任务的到期日期。

Table 115. 更新一个任务 - 响应码
响应码 描述

200

代表任务已被更新。

404

代表未找到请求的任务。

409

代表请求的任务已经被并发同时更新。

成功响应体:参见runtime/tasks/{taskId}的响应。

15.7.5. 任务操作 Task actions

POST runtime/tasks/{taskId}

完成一个任务 - JSON 体:

{
  "action" : "complete",
  "variables" : []
}

完成任务。可以使用variables参数传递可选的变量数组。可以在REST变量章节找到关于变量格式的更多信息。请注意提供的变量范围将被忽略,变量将设置在父范围中,除非一个本地范围中存在该变量。这种情况下将会覆盖。与TaskService.completeTask(taskId, variables)调用的行为相同。

申领一个任务 - JSON 体:

{
  "action" : "claim",
  "assignee" : "userWhoClaims"
}

使用给定办理人申领任务。如果办理人为null,则取消任务的指派,可以重新申领。

代理一个任务 - JSON 体:

{
  "action" : "delegate",
  "assignee" : "userToDelegateTo"
}

将任务代理给给定办理人。办理人为必填。

解决一个任务 - JSON 体:

{
  "action" : "resolve"
}

解决任务代理。任务指派回任务的属主(若有)。

Table 116. 任务操作 - 响应码
响应码 描述

200

代表已执行操作。

400

代表请求体包含非法值,或者当操作需要时没有提供办理人。

404

代表未找到请求的任务。

409

代表操作由于冲突无法执行。任务已被并发同时更新,或者在进行'claim'操作时,任务已被其他用户申领。

成功响应体:参见runtime/tasks/{taskId}的响应。

15.7.6. 删除一个任务 Delete a task

DELETE runtime/tasks/{taskId}?cascadeHistory={cascadeHistory}&deleteReason={deleteReason}
Table 117. 删除一个任务 - URL 参数
参数 必填 描述

taskId

String

要删除的任务的id。

cascadeHistory

Boolean

是否在删除任务时,删除历史任务实例(如果可用)。如果未设置,则默认值为false。

deleteReason

String

任务删除的原因。如果cascadeHistory值为true,则忽略本参数。

Table 118. 删除一个任务 - 响应码
响应码 描述

204

代表已找到并删除任务。响应体设置为空。

403

代表由于请求的任务是工作流的一部分,无法删除。

404

代表未找到请求的任务。

15.7.7. 获取一个任务的所有变量 Get all variables for a task

GET runtime/tasks/{taskId}/variables?scope={scope}
Table 119. 获取一个任务的所有变量 - URL 参数
参数 必填 描述

taskId

String

要获取变量的任务的id。

scope

String

要返回的变量的范围。若值为'local,则只返回任务本地变量。若值为global',则只返回任务的父执行树中的变量。若省略该参数,则本地与全局变量都会返回。

Table 120. 获取一个任务的所有变量 - 响应码
响应码 描述

200

代表已找到任务,并已返回请求的变量。

404

代表未找到请求的任务。

成功响应体:

[
  {
    "name" : "doubleTaskVar",
    "scope" : "local",
    "type" : "double",
    "value" : 99.99
  },
  {
    "name" : "stringProcVar",
    "scope" : "global",
    "type" : "string",
    "value" : "This is a ProcVariable"
  }



]

变量作为一个JSON数组返回。在通用REST变量章节中可以找到全部响应体的描述。

15.7.8. 从一个任务中获取一个变量 Get a variable from a task

GET runtime/tasks/{taskId}/variables/{variableName}?scope={scope}
Table 121. 从一个任务中获取一个变量 - URL 参数
参数 必填 描述

taskId

String

要获取变量的任务的id。

variableName

String

要获取的变量的名字。

scope

String

要返回的变量的范围。若值为'local,则只返回任务本地变量。若值为global',则只返回任务的父执行树中的变量。若省略该参数,如果存在本地变量就会返回,否则返回全局变量。

Table 122. 从一个任务中获取一个变量 - 响应码
响应码 描述

200

代表已找到任务,并已返回请求的变量。

404

代表未找到请求的任务,或者任务(在给定范围中)没有给定名字的变量。状态消息提供了额外信息。

成功响应体:

{
  "name" : "myTaskVariable",
  "scope" : "local",
  "type" : "string",
  "value" : "Hello my friend"
}

在通用REST变量章节中可以找到全部响应体的描述。

15.7.9. 获取一个变量的二进制数据 Get the binary data for a variable

GET runtime/tasks/{taskId}/variables/{variableName}/data?scope={scope}
Table 123. 获取一个变量的二进制数据 - URL 参数
参数 必填 描述

taskId

String

要获取变量的任务的id。

variableName

String

要获取数据的变量的名字。只能使用binaryserializable类型的变量。如果使用其它任何类型的变量,将返回404

scope

String

要返回的变量的范围。若值为'local,则只返回任务本地变量。若值为global',则只返回任务的父执行树中的变量。若省略该参数,如果存在本地变量就会返回,否则返回全局变量。

Table 124. 获取一个变量的二进制数据 - 响应码
响应码 描述

200

代表已找到任务,并已返回请求的变量。

404

代表未找到请求的任务,或者任务(在给定范围中)没有给定名字的变量,或者变量没有可用的二进制流。状态消息提供了额外信息。

成功响应体:响应体包含了变量的二进制值。如果变量是binary类型,则不论变量的content或者请求的accept-type头是什么,响应的content-type都将设置为application/octet-stream。如果变量是serializable类型,则content-type会使用application/x-java-serialized-object

15.7.10. 为一个任务创建(或更新)变量 Create (or update) variables on a task

POST runtime/tasks/{taskId}/variables
PUT runtime/tasks/{taskId}/variables

使用POST时,将创建所有传递的变量。如果其中有已经在任务中存在的变量,则请求结果为错误(409 - 冲突)。而在使用PUT时,会为任务创建不存在的变量,而已经存在的变量将被更新,不会报错。

Table 125. 为一个任务创建(或更新)变量 - URL 参数
参数 必填 描述

taskId

String

要创建新变量的任务的id。

创建简单(非二进制)变量的请求体:

[
  {
    "name" : "myTaskVariable",
    "scope" : "local",
    "type" : "string",
    "value" : "Hello my friend"
  },
  {

  }
]

请求体需要是一个数组,包含有一个或多个代表要创建的变量的JSON对象

  • name: 变量需要的名字。

  • scope: 创建变量的范围。如果省略,则使用local

  • type: 创建的变量的类型。如果省略,则转换为原始JSON值的类型(string、boolean、integer或double)。

  • value: 变量值。

可以在REST变量章节找到关于变量格式的更多信息。

成功响应体:

[
  {
    "name" : "myTaskVariable",
    "scope" : "local",
    "type" : "string",
    "value" : "Hello my friend"
  },
  {

  }
]
Table 126. 为一个任务创建(或更新)变量 - 响应码
响应码 描述

201

代表已创建变量,并已返回结果。

400

代表缺少要创建的变量名,或者尝试为独立任务(未关联至流程)创建global变量,或者请求中包含了空的变量数组,或者请求中没有包含变量数组。状态消息提供了额外信息。

404

代表未找到请求的任务。

409

代表任务中已有给定名字的变量(仅在使用POST方法时抛出)。改用PUT方法更新任务变量。

15.7.11. 为一个任务创建一个新的二进制变量 Create a new binary variable on a task

POST runtime/tasks/{taskId}/variables
Table 127. 为一个任务创建一个新的二进制变量 - URL 参数
参数 必填 描述

taskId

String

要创建新变量的任务的id。

请求体:请求需要是multipart/form-data类型的。需要有唯一的file-part,包含变量的二进制值。另外,也可以使用下列额外的form-fields:

  • name: 变量需要的名字。

  • scope: 创建变量的范围。如果省略,则使用local

  • type: 创建的变量的类型。如果省略,则使用binary,并且将请求中的二进制数据保存为字节数组。

成功响应体:

{
  "name" : "binaryVariable",
  "scope" : "local",
  "type" : "binary",
  "value" : null,
  "valueUrl" : "http://.../runtime/tasks/123/variables/binaryVariable/data"
}
Table 128. 为一个任务创建一个新的二进制变量 - 响应码
响应码 描述

201

代表已创建变量,并已返回结果。

400

代表缺少要创建的变量名,或者尝试为独立任务(未关联至流程)创建global变量。状态消息提供了额外信息。

404

代表未找到请求的任务。

409

代表任务中已有给定名字的变量。改用PUT方法更新任务变量。

415

代表序列化数据中包含了一个运行Activiti引擎的JVM中不存在的类的对象,因此不能反序列化。

15.7.12. 为一个任务更新一个已有变量 Update an existing variable on a task

PUT runtime/tasks/{taskId}/variables/{variableName}
Table 129. 为一个任务更新一个已有变量 - URL 参数
参数 必填 描述

taskId

String

要更新变量的任务的id。

variableName

String

要更新的变量的名字。

更新简单(非二进制)变量的请求体:

{
  "name" : "myTaskVariable",
  "scope" : "local",
  "type" : "string",
  "value" : "Hello my friend"
}

请求体需要是一个数组,包含有一个或多个代表要创建的变量JSON对象

  • name: 变量需要的名字。

  • scope: 创建变量的范围。如果省略,则使用local

  • type: 创建的变量的类型。如果省略,则转换为原始JSON值的类型(string、boolean、integer或double)。

  • value: 变量值。

可以在REST变量章节找到关于变量格式的更多信息。

成功响应体:

{
  "name" : "myTaskVariable",
  "scope" : "local",
  "type" : "string",
  "value" : "Hello my friend"
}
Table 130. 为一个任务更新一个已有变量 - 响应码
响应码 描述

200

代表已更新变量,并已返回结果。

400

代表缺少要创建的变量名,或者尝试为独立任务(未关联至流程)创建global变量。状态消息提供了额外信息。

404

代表未找到请求的任务,或者任务在给定范围中没有给定名字的变量。状态消息提供了额外信息。

15.7.13. 为一个任务更新一个二进制变量 Updating a binary variable on a task

PUT runtime/tasks/{taskId}/variables/{variableName}
Table 131. 为一个任务更新一个二进制变量 - URL 参数
参数 必填 描述

taskId

String

要更新变量的任务的id。

variableName

String

要更新的变量的名字。

请求体:请求需要是multipart/form-data类型的。需要有唯一的file-part,包含变量的二进制值。另外,也可以使用下列额外的form-fields:

  • name: 变量需要的名字。

  • scope: 创建变量的范围。如果省略,则使用local

  • type: 创建的变量的类型。如果省略,则使用binary,并且将请求中的二进制数据保存为字节数组。

成功响应体:

{
  "name" : "binaryVariable",
  "scope" : "local",
  "type" : "binary",
  "value" : null,
  "valueUrl" : "http://.../runtime/tasks/123/variables/binaryVariable/data"
}
Table 132. 为一个任务更新一个二进制变量 - 响应码
响应码 描述

200

代表已更新变量,并已返回结果。

400

代表缺少要创建的变量名,或者尝试为独立任务(未关联至流程)创建global变量。状态消息提供了额外信息。

404

代表未找到请求的任务,或者任务在给定范围中没有给定名字的变量。

415

代表序列化数据中包含了一个运行Activiti引擎的JVM中不存在的类的对象,因此不能反序列化。

15.7.14. 为一个任务删除一个变量 Delete a variable on a task

DELETE runtime/tasks/{taskId}/variables/{variableName}?scope={scope}
Table 133. 为一个任务删除一个变量 - URL 参数
参数 必填 描述

taskId

String

要删除的变量所在任务的id。

variableName

String

要删除的变量的名字。

scope

String

要删除的变量所在的范围。可以为localglobal。若省略,则使用local

Table 134. 为一个任务删除一个变量 - 响应码
响应码 描述

204

代表已找到并删除任务变量。响应体设置为空。

404

代表未找到请求的任务,或者任务在给定范围中没有给定名字的变量。状态消息提供了额外信息。

15.7.15. 为一个任务删除所有本地变量 Delete all local variables on a task

DELETE runtime/tasks/{taskId}/variables
Table 135. 为一个任务删除所有本地变量 - URL 参数
参数 必填 描述

taskId

String

要删除的变量所在任务的id。

Table 136. 为一个任务删除所有本地变量 - 响应码
响应码 描述

204

代表已删除所有本地任务变量。响应体设置为空。

404

代表未找到请求的任务。

GET runtime/tasks/{taskId}/identitylinks
Table 137. 获取一个任务的所有身份关联 - URL 参数
参数 必填 描述

taskId

String

要获取身份关联的任务的id。

Table 138. 获取一个任务的所有身份关联 - 响应码
响应码 描述

200

代表已找到任务,并已返回请求的身份关联。

404

代表未找到请求的任务。

成功响应体:

[
  {
    "user" : "kermit",
    "group" : null,
    "type" : "candidate",
    "url" : "http://localhost:8081/activiti-rest/service/runtime/tasks/100/identitylinks/users/kermit/candidate"
  },
  {
    "user" : null,
    "group" : "sales",
    "type" : "candidate",
    "url" : "http://localhost:8081/activiti-rest/service/runtime/tasks/100/identitylinks/groups/sales/candidate"
  },

  ...
]
GET runtime/tasks/{taskId}/identitylinks/users
GET runtime/tasks/{taskId}/identitylinks/groups

只返回目标为用户或组的身份关联。响应体及状态码与获取一个任务的完整身份关联列表时完全相同。

GET runtime/tasks/{taskId}/identitylinks/{family}/{identityId}/{type}
Table 139. 获取一个任务的一个身份关联 - URL 参数
参数 必填 描述

taskId

String

任务的id。

family

String

可以为groupsusers,取决于身份目标的类型。

identityId

String

身份的id。

type

String

身份关联的类型。

Table 140. 获取一个任务的一个身份关联 - 响应码
响应码 描述

200

代表已找到任务及身份关联,并已返回。

404

代表未找到请求的任务,或者任务中没有请求的身份关联。状态消息提供了额外信息。

成功响应体:

{
  "user" : null,
  "group" : "sales",
  "type" : "candidate",
  "url" : "http://localhost:8081/activiti-rest/service/runtime/tasks/100/identitylinks/groups/sales/candidate"
}
POST runtime/tasks/{taskId}/identitylinks
Table 141. 为一个任务创建一个身份关联 - URL 参数
参数 必填 描述

taskId

String

任务的id。

响应体(用户):

{
  "user" : "kermit",
  "type" : "candidate",
}

响应体(组):

{
  "group" : "sales",
  "type" : "candidate",
}
Table 142. 为一个任务创建一个身份关联 - 响应码
响应码 描述

201

代表已找到任务,并已创建身份关联。

404

代表未找到任务,或任务中没有请求的身份关联。状态消息提供了额外信息。

成功响应体:

{
  "user" : null,
  "group" : "sales",
  "type" : "candidate",
  "url" : "http://localhost:8081/activiti-rest/service/runtime/tasks/100/identitylinks/groups/sales/candidate"
}
DELETE runtime/tasks/{taskId}/identitylinks/{family}/{identityId}/{type}
Table 143. 为一个任务删除一个身份关联 - URL 参数
参数 必填 描述

taskId

String

任务的id。

family

String

可以为groupsusers,取决于身份目标的类型。

identityId

String

身份的id。

type

String

身份关联的类型。

Table 144. 为一个任务删除一个身份关联 - 响应码
响应码 描述

204

代表已找到任务与身份关联,并已删除身份关联。响应体设置为空。

404

代表未找到任务,或任务中没有请求的身份关联。状态消息提供了额外信息。

15.7.21. 为一个任务创建一个新备注 Create a new comment on a task

POST runtime/tasks/{taskId}/comments
Table 145. 为一个任务创建一个新备注 - URL 参数
参数 必填 描述

taskId

String

要创建备注的任务的id。

请求体:

{
  "message" : "This is a comment on the task.",
  "saveProcessInstanceId" : true
}

参数saveProcessInstanceId是可选的,如果为true,则在备注中保存任务的流程实例id。

成功响应体:

{
  "id" : "123",
  "taskUrl" : "http://localhost:8081/activiti-rest/service/runtime/tasks/101/comments/123",
  "processInstanceUrl" : "http://localhost:8081/activiti-rest/service/history/historic-process-instances/100/comments/123",
  "message" : "This is a comment on the task.",
  "author" : "kermit",
  "time" : "2014-07-13T13:13:52.232+08:00",
  "taskId" : "101",
  "processInstanceId" : "100"
}
Table 146. 为一个任务创建一个新备注 - 响应码
响应码 描述

201

代表已创建备注,并已返回结果。

400

代表请求中缺少备注。

404

代表未找到请求的任务。

15.7.22. 获取一个任务的所有备注 Get all comments on a task

GET runtime/tasks/{taskId}/comments
Table 147. 获取一个任务的所有备注 - URL 参数
参数 必填 描述

taskId

String

要获取备注的任务的id。

成功响应体:

[
  {
    "id" : "123",
    "taskUrl" : "http://localhost:8081/activiti-rest/service/runtime/tasks/101/comments/123",
    "processInstanceUrl" : "http://localhost:8081/activiti-rest/service/history/historic-process-instances/100/comments/123",
    "message" : "This is a comment on the task.",
    "author" : "kermit",
    "time" : "2014-07-13T13:13:52.232+08:00",
    "taskId" : "101",
    "processInstanceId" : "100"
  },
  {
    "id" : "456",
    "taskUrl" : "http://localhost:8081/activiti-rest/service/runtime/tasks/101/comments/456",
    "processInstanceUrl" : "http://localhost:8081/activiti-rest/service/history/historic-process-instances/100/comments/456",
    "message" : "This is another comment on the task.",
    "author" : "gonzo",
    "time" : "2014-07-13T13:13:52.232+08:00",
    "taskId" : "101",
    "processInstanceId" : "100"
  }
]
Table 148. 获取一个任务的所有备注 - 响应码
响应码 描述

200

代表已找到任务,并已返回备注。

404

代表未找到请求的任务。

15.7.23. 获取一个任务的一个备注 Get a comment on a task

GET runtime/tasks/{taskId}/comments/{commentId}
Table 149. 获取一个任务的一个备注 - URL 参数
参数 必填 描述

taskId

String

要获取备注的任务的id。

commentId

String

备注的id。

成功响应体:

{
  "id" : "123",
  "taskUrl" : "http://localhost:8081/activiti-rest/service/runtime/tasks/101/comments/123",
  "processInstanceUrl" : "http://localhost:8081/activiti-rest/service/history/historic-process-instances/100/comments/123",
  "message" : "This is a comment on the task.",
  "author" : "kermit",
  "time" : "2014-07-13T13:13:52.232+08:00",
  "taskId" : "101",
  "processInstanceId" : "100"
}
Table 150. 获取一个任务的一个备注 - 响应码
响应码 描述

200

代表已找到任务与备注,并已返回备注。

404

代表未找到请求的任务,或任务没有给定ID的备注。

15.7.24. 为一个任务删除一个备注 Delete a comment on a task

DELETE runtime/tasks/{taskId}/comments/{commentId}
Table 151. 为一个任务删除一个备注 - URL 参数
参数 必填 描述

taskId

String

要删除备注的任务的id。

commentId

String

备注的id。

Table 152. 为一个任务删除一个备注 - 响应码
响应码 描述

204

代表已找到任务与备注,并已删除备注。响应体设置为空。

404

代表未找到请求的任务,或任务没有给定id的备注。

15.7.25. 获取一个任务的所有事件 Get all events for a task

GET runtime/tasks/{taskId}/events
Table 153. 获取一个任务的所有事件 - URL 参数
参数 必填 描述

taskId

String

要获取事件的任务的id。

成功响应体:

[
  {
    "action" : "AddUserLink",
    "id" : "4",
    "message" : [ "gonzo", "contributor" ],
    "taskUrl" : "http://localhost:8182/runtime/tasks/2",
    "time" : "2013-05-17T11:50:50.000+0000",
    "url" : "http://localhost:8182/runtime/tasks/2/events/4",
    "userId" : null
  }

]
Table 154. 获取一个任务的所有事件 - 响应码
响应码 描述

200

代表已找到任务,并已返回事件。

404

代表未找到请求的任务。

15.7.26. 获取一个任务的一个事件 Get an event on a task

GET runtime/tasks/{taskId}/events/{eventId}
Table 155. 获取一个任务的一个事件 - URL 参数
参数 必填 描述

taskId

String

要获取事件的任务的id。

eventId

String

事件的id。

成功响应体:

{
  "action" : "AddUserLink",
  "id" : "4",
  "message" : [ "gonzo", "contributor" ],
  "taskUrl" : "http://localhost:8182/runtime/tasks/2",
  "time" : "2013-05-17T11:50:50.000+0000",
  "url" : "http://localhost:8182/runtime/tasks/2/events/4",
  "userId" : null
}
Table 156. 获取一个任务的一个事件 - 响应码
响应码 描述

200

代表已找到任务与事件,并已返回事件。

404

代表未找到请求的任务,或者任务中没有给定ID的事件。

POST runtime/tasks/{taskId}/attachments
Table 157. 为一个任务创建一个新的附件,带有一个指向外部资源的链接 - URL 参数
参数 必填 描述

taskId

String

要创建附件的任务的id。

请求体:

{
  "name":"Simple attachment",
  "description":"Simple attachment description",
  "type":"simpleType",
  "externalUrl":"http://activiti.org"
}

要创建一个新的附件,只有附件名是必填的。

成功响应体:

{
  "id":"3",
  "url":"http://localhost:8182/runtime/tasks/2/attachments/3",
  "name":"Simple attachment",
  "description":"Simple attachment description",
  "type":"simpleType",
  "taskUrl":"http://localhost:8182/runtime/tasks/2",
  "processInstanceUrl":null,
  "externalUrl":"http://activiti.org",
  "contentUrl":null
}
Table 158. 为一个任务创建一个