1. 简介
1.1. 协议
Activiti使用 the Apache V2 license 协议开源。 Activiti Modeler(Web设计器)使用了另一个开源协议 the LGPL 2.1 license 。
1.3. 源码
Activiti的发布包里包含了大部分源码,这些源码是以jar压缩文件提供的。Activiti的源码可以通过以下链接获得: https://github.com/Activiti/Activiti
1.4. 必要的软件
1.4.1. JDK 6+
Activiti需要JDK 6或以上版本。访问 Oracle Java SE downloads 点击“下载JDK”按钮。这个页面上有安装文档。安装完成后,执行 java -version
校验安装是否成功。能看到JDK的版本信息就说明安装成功了。
1.4.2. Eclipse Indigo 和 Juno
到 the Eclipse 下载页面选择对应的Eclipse版本下载。把下载的文件解压, 然后执行+eclipse+目录下的eclipse文件。手册后续有专门一章 installing our eclipse designer plugin。
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 runtime与Apache 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 runtime与Apache 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 |
流程引擎的用户操作台。使用这个工具来启动新流程、分配任务、查看与接收任务等。这个工具同时可以管理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。你也可以单独禁用或启用其中的某些设置。
1
2
3
4
5 # 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
类:
1 ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine()
这样会从classpath寻找activiti.cfg.xml
,并用这个文件中的配置构造引擎。下面的代码展示了一个配置的例子。后续章节会对配置参数进行详细介绍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 <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行)。
1
2
3
4
5 ProcessEngineConfiguration.createProcessEngineConfigurationFromResourceDefault();
ProcessEngineConfiguration.createProcessEngineConfigurationFromResource(String resource);
ProcessEngineConfiguration.createProcessEngineConfigurationFromResource(String resource, String beanName);
ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(InputStream inputStream);
ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(InputStream inputStream, String beanName);
也可以不使用配置文件,基于默认创建配置(参考不同的支持类获得更多信息)。
1
2 ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();
ProcessEngineConfiguration.createStandaloneInMemProcessEngineConfiguration();
所有的ProcessEngineConfiguration.createXXX()
方法都返回ProcessEngineConfiguration
,并可以继续按需调整。调用buildProcessEngine()
后,生成一个ProcessEngine
:
1
2
3
4
5
6 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。
1 <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秒)。
数据库配置示例:
1
2
3
4 <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):
1
2
3
4
5
6
7
8
9
10
11
12 <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:
1
2
3 <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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <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文件,包含下列配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <?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,按照下列修 改文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <?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配置文件:
1
2
3
4
5
6
7
8
9 <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标准的ManagedJobExecutor
与ManagedAsyncJobExecutor
来管理线程。要启用这个功能,需要在配置中如下加入线程工厂:
1
2
3
4
5
6
7
8
9 <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
随流程引擎启动时,设置:
1 <property name="jobExecutorActivate" value="false" />
3.11. 启用异步执行器 Async executor activation
AsyncExecutor
是管理线程池的组件,这个线程池用于触发定时器与异步任务。
默认情况下,由于历史原因,当使用JobExecutor
时,AsyncExecutor
不生效。然而我们建议使用新的AsyncExecutor
代替JobExecutor
,通过定义两个参数实现
1
2 <property name="asyncExecutorEnabled" value="true" />
<property name="asyncExecutorActivate" value="true" />
asyncExecutorEnabled参数用于启用异步执行器,代替老的作业执行器。 第二个参数asyncExecutorActivate命令Activiti引擎在启动时启动异步执行器线程池。
3.12. 配置邮件服务器 Mail server configuration
配置邮件服务器是可选的。Activiti支持在业务流程中发送电子邮件。发送电子邮件需要配置有效的SMTP邮件服务器。查看电子邮件任务了解配置选项。
3.13. 配置历史 History configuration
1 <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
鉴于流程定义信息不会改变,为了避免每次使用流程定义时都读取数据库,所有的流程定义都会(在解析后)被缓存。默认情况下,这个缓存没有限制。要限制流程定义缓存,加上如下的参数
1 <property name="processDefinitionCacheLimit" value="10" />
设置这个参数,会将默认的hashmap替换为LRU缓存,以进行限制。当然,参数的“最佳”取值,取决于总的流程定义数量,以及实际使用的流程定义数量。
你也可以注入自己的缓存实现。它必须是一个实现了org.activiti.engine.impl.persistence.deploy.DeploymentCache
接口的bean:
1
2
3 <property name="processDefinitionCache">
<bean class="org.activiti.MyCache" />
</property>
配置规则缓存(rules cache)可以使用类似的名为knowledgeBaseCacheLimit
与knowledgeBaseCache
的参数。只有在流程中使用规则任务(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)的依赖,请注意你还需要加上版本:
1
2
3
4 <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的示例(省略了版本):
1
2
3
4 <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中进行如下简单的布局定义,就可以让日志记录器显示上述信息:
1 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
, processInstanceId
与processDefinitionId
。部分事件含有关于发生事件的上下文信息。关于事件包含的附加信息,请参阅所有支持的事件类型。
3.18.1. 事件监听器实现 Event listener implementation
对事件监听器的唯一要求,是要实现org.activiti.engine.delegate.event.ActivitiEventListener
接口。下面是一个监听器实现的例子,它将接收的所有事件打印至标准输出,并对作业执行相关的事件特别处理。:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 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。下面的代码片段在配置中增加了一个事件监听器,无论任何类型的事件分发时,都会得到通知:
1
2
3
4
5
6
7
8 <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
实例的列表。下面的代码片段在配置中增加了一个事件监听器,它会在作业执行成功或失败时得到通知:
1
2
3
4
5
6
7
8
9
10
11
12 <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
)为引擎增加或删除额外的事件监听器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 /**
* 新增一个监听器,分发器会在所有事件分发时通知。
* @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作为监听器。
1
2
3
4
5
6
7
8
9 <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>
实体相关的事件也可以在流程定义中增加监听器,只有在特定实体类型的事件发生时得到通知。下面的代码片段展示了如何设置。可以使用实体的所有(第一个例子)事件,或只使用实体的特定类型(第二个例子)事件。
1
2
3
4
5
6
7
8
9 <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事件,会导致错误。下面的代码片段展示了如何在流程实例中抛出信号,向外部流程(全局)抛出信号,在流程实例中抛出消息事件,以及
在流程实例中抛出错误事件。这里不使用class
或delegateExpression
,而要使用throwEvent
属性,以及一个附加属性,用于指定需要抛出的事件类型。
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<activiti:eventListener throwEvent="signal" signalName="My signal" events="TASK_ASSIGNED" />
</extensionElements>
</process>
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<activiti:eventListener throwEvent="globalSignal" signalName="My signal" events="TASK_ASSIGNED" />
</extensionElements>
</process>
1
2
3
4
5 <process id="testEventListeners">
<extensionElements>
<activiti:eventListener throwEvent="message" messageName="My message" events="TASK_ASSIGNED" />
</extensionElements>
</process>
1
2
3
4
5 <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.MessageThrowingEventListener
与org.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
值,会在流程定义部署时抛出异常(导致部署失败)。如果class
或delegateExecution
指定了不合法的值(不存在的类,不存在的bean引用,或者代理类没有实现监听器接口),在流程启动(或该流程定义的第一个有效事件分发给这个监听器)时,会抛出异常。请确保引用的类在classpath中,并且保证表达式能够解析为有效的实例。
3.18.5. 通过API分发事件 Dispatching events through API
我们通过API提供事件分发机制,可以向任何在引擎中注册的监听器分发自定义事件。建议(但不强制)只分发类型为CUSTOM
的ActivitiEvents
。可以使用RuntimeService
分发事件:
1
2
3
4
5
6
7
8 /**
* 将给定事件分发给所有注册监听器。
* @param event 要分发的事件。
*
* @throws ActivitiException 当分发事件发生异常,或者{@link ActivitiEventDispatcher}被禁用。
* @throws ActivitiIllegalArgumentException 当给定事件不可分发
*/
void dispatchEvent(ActivitiEvent event);
3.18.6. 支持的事件类型 Supported event types
下表列出引擎中的所有事件类型。每种类型对应org.activiti.engine.delegate.event.ActivitiEventType
中的一个枚举值。
Event name | Description | Event classes |
---|---|---|
ENGINE_CREATED |
本监听器附着的流程引擎已经创建,并可以响应API调用。 |
|
ENGINE_CLOSED |
本监听器附着的流程引擎已经关闭,不能再对该引擎的进行API调用。 |
|
ENTITY_CREATED |
新的实体已经创建。该实体包含在本事件里。 |
|
ENTITY_INITIALIZED |
新的实体已经创建并完全初始化。如果任何子实体作为该实体的一部分被创建,本事件会在子实体创建/初始化后触发,与 |
|
ENTITY_UPDATED |
实体已经更新。该实体包含在本事件里。 |
|
ENTITY_DELETED |
实体已经删除。该实体包含在本事件里。 |
|
ENTITY_SUSPENDED |
实体已经挂起。该实体包含在本事件里。会为ProcessDefinitions(流程定义), ProcessInstances(流程实例)与Tasks(任务)分发本事件。 |
|
ENTITY_ACTIVATED |
实体已被激活。该实体包含在本事件里。会为ProcessDefinitions, ProcessInstances与Tasks分发本事件。 |
|
JOB_EXECUTION_SUCCESS |
作业已经成功执行。该作业包含在本事件里。 |
|
JOB_EXECUTION_FAILURE |
作业执行失败。该作业与异常包含在本事件里。 |
|
JOB_RETRIES_DECREMENTED |
作业重试次数已经由于执行失败而减少。该作业包含在本事件里。 |
|
TIMER_FIRED |
定时器已经被触发。 |
|
JOB_CANCELED |
作业已经被取消。该作业包含在本事件里。作业会由于API调用取消,任务完成导致关联的边界定时器取消,也会由于新流程定义的部署而取消。 |
|
ACTIVITY_STARTED |
节点开始执行 |
|
ACTIVITY_COMPLETED |
节点成功完成 |
|
ACTIVITY_CANCELLED |
节点将要取消。节点的取消有三个原因(MessageEventSubscriptionEntity, SignalEventSubscriptionEntity, TimerEntity)。 |
|
ACTIVITY_SIGNALED |
节点收到了一个信号 |
|
ACTIVITY_MESSAGE_RECEIVED |
节点收到了一个消息。事件在节点接收消息前分发。消息接收后,会为该节点分发 |
|
ACTIVITY_ERROR_RECEIVED |
节点收到了错误事件。在节点实际处理错误前分发。该事件的 |
|
UNCAUGHT_BPMN_ERROR |
抛出了未捕获的BPMN错误。流程没有该错误的处理器。该事件的 |
|
ACTIVITY_COMPENSATE |
节点将要被补偿。该事件包含将要执行补偿的节点id。 |
|
VARIABLE_CREATED |
创建了流程变量。本事件包含变量名、取值与关联的执行和任务(若有)。 |
|
VARIABLE_UPDATED |
更新了已有变量。本事件包含变量名、取值与关联的执行和任务(若有)。 |
|
VARIABLE_DELETED |
删除了已有变量。本事件包含变量名、最后取值与关联的执行和任务(若有)。 |
|
TASK_ASSIGNED |
任务分派给了用户。该任务包含在本事件里。 |
|
TASK_CREATED |
任务已经创建。本事件在 |
|
TASK_COMPLETED |
任务已经结束。本事件在 |
|
PROCESS_COMPLETED |
流程完成。在最后一个节点的 |
|
PROCESS_CANCELLED |
流程已经被取消。在流程实例从运行时删除前分发。流程实例使用API调用 |
|
MEMBERSHIP_CREATED |
用户加入了一个组。本事件包含了相关的用户和组的id。 |
|
MEMBERSHIP_DELETED |
用户从一个组中移出。本事件包含了相关的用户和组的id。 |
|
MEMBERSHIPS_DELETED |
组的所有用户将被移出。本事件在用户移出前抛出,因此关联关系仍然可以访问。因为性能原因,不会再为每个被移出的用户抛出 |
|
引擎中所有的 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与服务对象都是线程安全的,因此可以在整个服务器中保存一份引用。
1
2
3
4
5
6
7
8
9 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.xml
与activiti-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
中摘录:
1
2
3
4
5
6 /**
* 当任务成功执行时调用。
* @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()
方法。
这段小教程的最终目标是生成一个业务流程,模拟公司中简单的请假流程:
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了解这种结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 <?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文件解析为可执行的东西,并为部署中包含的每个流程定义创建新的数据库记录。这样,引擎重启后,仍能获取已部署的流程:
1
2
3
4
5
6
7 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)需要用到它们。流程变量的使用很普遍,因为它们为流程定义的流程实例赋予 了意义。流程变量使每个流程实例与其他实例不同。
1
2
3
4
5
6
7
8
9
10 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
流程启动时,第一步是一个用户任务。这个步骤必须由系统用户操作。一般会提供“待办任务”列出所有需要该用户处理的任务。下面的代码片段展示如何进行这种列表的查询:
1
2
3
4
5
6 // 获取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(完成)
这个任务。下面的代码片段展示了如何操作:
1
2
3
4
5
6 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
暂停流程定义:
1
2
3
4
5
6 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逻辑),也可以明确指定排序。下面是示例代码:
1
2
3
4
5 List<Task> tasks = taskService.createTaskQuery()
.taskAssignee("kermit")
.processVariableValueEquals("orderId", "0815")
.orderByDueDate().asc()
.list();
有时你需要更强力的查询,例如使用OR操作符查询,或者使用查询API不能满足查询条件要求。我们为这种需求提供了原生查询,可以自己写SQL查 询。返回类型由使用的查询对象决定,数据也会映射到正确的对象中,如Task, ProcessInstance, Execution….查询会在数据库中进行,因此你需要使用数据库中定义的表名与列名。这需要了解内部数据结构,因此建议小心使用原生查询。数据库表 名可以通过API读取,这样可以将依赖关系减到最小。
1
2
3
4
5
6
7
8
9 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中:
1 ProcessInstance startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables);
也可以在流程执行中加入变量。例如(RuntimeService):
1
2
3
4 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中有类似的方法。这意味着任务与执行一样,可以持有局部变量,其生存期为任务持续的时间。
1
2
3
4
5
6 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)等等。在这些结构中,提供了当前的execution或task对象,可用于变量的设置、读取。简单示例如下:
1
2
3
4
5
6 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版本起,引入了可以更精细控制的方法。这个方法有一个可选的参数,告诉引擎是否需要在幕后将所有变量读取并缓存:
1
2
3 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风格的测试看起来类似:
1
2
3
4
5
6
7
8
9
10
11
12
13 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
的用法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 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里可以通过在代码左侧条上双击实现:
如果我们在debug模式(在测试类中右键,选择“Run as”,然后选择“JUnit test”)下运行单元测试,测试进程会在断点处暂停,这样我们就可以在右上窗口中查看测试中的变量。
要检查Activiti的数据,打开Display窗口(如果没有找到这个窗口,打开 Window→Show View→Other,然后选择Display),并键入(可以使用代码补全)org.h2.tools.Server.createWebServer("-web").start()
选中刚键入的行并右键点击。然后选择’Display'(或者用快捷方式执行)
现在打开浏览器并访问http://localhost:8082,填入内存数据库的JDBC URL(默认为jdbc:h2:mem:activiti
),然后点击connect按钮。
现在你可以看到Activiti的数据,可以用来理解你的单元测试执行流程的方式是什么,以及为什么这样。
4.9. Web应用中的流程引擎 The process engine in a web application
ProcessEngine
是线程安全的类,可以很容易地在多个线程间共享。在web应用中,这意味着可以在容器启动时创建引擎,并在容器关闭时关闭引擎。
下面的代码片段展示了如何在纯Servlet环境中,简单的通过ServletContextListener
初始化与销毁流程引擎。
1
2
3
4
5
6
7
8
9
10
11 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中有多个这样的资源文件,请确保它们都使用不同的名字。需要使用流程引擎时,可以获取通过
1 ProcessEngines.getDefaultProcessEngine()
或者
1 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为:
1
2
3
4
5
6
7 <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就需要未代理的数据源)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 <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与配置:
1
2
3
4
5
6
7
8
9
10
11 <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应用上下文:
1
2 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(
"org/activiti/examples/spring/SpringTransactionIntegrationTest-context.xml");
或者在单元测试中:
1 @ContextConfiguration("classpath:org/activiti/spring/test/transaction/SpringTransactionIntegrationTest-context.xml")
现在就可以获取服务bean,并反射调用(invoke)它们的方法。ProcessEngineFactoryBean会为服务加上额外的拦截器 (interceptor),为Activiti服务方法设置Propagation.REQUIRED事务语义(transaction semantics)。因此,我们可以像这样使用repositoryService部署流程:
1
2
3
4
5
6
7 RepositoryService repositoryService =
(RepositoryService) applicationContext.getBean("repositoryService");
String deploymentId = repositoryService
.createDeployment()
.addClasspathResource("org/activiti/spring/test/hello.bpmn20.xml")
.deploy()
.getId();
还有另一种方法也可以使用。在这个例子中,userBean.hello()方法被Spring事务包围,Activiti服务方法的调用会加入这个事务。
1
2 UserBean userBean = (UserBean) applicationContext.getBean("userBean");
userBean.hello();
UserBean看起来像这样。请记着在上面的Spring bean配置中,我们已经将repositoryService注入了userBean。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 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都将可以使用。
1
2
3
4
5
6
7
8
9
10 <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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <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
为:
1
2
3
4
5
6 public class Printer {
public void printMessage() {
System.out.println("hello world");
}
}
Spring bean配置(上面已经展示过)为:
1
2
3
4
5
6 <beans>
...
<bean id="printer" class="org.activiti.examples.spring.Printer" />
</beans>
5.4. 自动部署资源 Automatic resource deployment
集成Spring还提供了部署资源的特殊方式。在流程引擎配置中,可以指定一组资源。当流程引擎被创建时,这些资源都会被扫描并部署。有过滤器用于 阻止重复部署。只有当资源确实发生变化时,才会重新部署至Activiti数据库。在Spring容器经常重启(例如测试时)的时候,这很有用。
这里有个例子:
1
2
3
4
5
6
7
8
9 <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
的例子:
1
2
3
4
5
6 <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的单元测试,对业务流程进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 @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注入)。
1
2
3 <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或更低则不需要。需要添加的依赖为:
1
2
3
4
5 <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中:
1
2
3
4
5 <dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>${activiti.version}</version>
</dependency>
就这么简单。这个依赖会自动向classpath添加正确的Activiti与Spring依赖。现在你可以编写Spring Boot应用了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 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数据库依赖:
1
2
3
4
5 <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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <?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,在应用启动时执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 @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数据库:
1
2
3
4
5
6
7
8
9 @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连接池:
1
2
3
4
5
6
7
8
9
10 <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中添加下列依赖:
1
2
3
4
5 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
创建一个新的Spring服务类,并创建两个方法:一个用于启动流程,另一个用于获得给定任务办理人的任务列表。在这里我们简单地包装了Activiti调用,但很明显在实际使用场景中会比这复杂得多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 @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)。在这里我们简单地委派给了上面定义的服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 @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支持,增加下列依赖:
1
2
3
4
5 <dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-jpa</artifactId>
<version>${activiti.version}</version>
</dependency>
这会加入Spring的配置,以及JPA用的bean。默认使用Hibernate作为JPA提供者。
创建一个简单的实体类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 @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
添加下列类:
1
2
3
4
5 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使用它打桩数据库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 @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现在为:
1
2
3
4
5
6
7
8
9
10 @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指派为任务办理人:
1 <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文件部署业务存档,可以这样做:
1
2
3
4
5
6
7 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:
会弹出窗口,让你选择电脑中的文件,或者(如果你的浏览器支持)可以直接拖放文件到指定区域。
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
指定版本。
对于业务存档中的每个流程定义,下列步骤都会执行,以初始化key
,version
,name
与id
参数:
-
XML文件中的流程定义
id
属性作为流程定义的key
参数。 -
XML文件中的流程定义
name
属性作为流程定义的name
参数。如果未给定name
属性,会使用id作为name。 -
当每个key的流程第一次部署时,指定版本为1。对其后所有使用相同key的流程定义,部署时版本会在该key当前已部署的最高版本号基础上加1。key参数用于区分流程定义。
-
id参数设置为{processDefinitionKey}:{processDefinitionVersion}:{generated-id},其中
generated-id
是一个唯一数字,用以保证在集群环境下,流程定义缓存中,流程id的唯一性。
以下面的流程为例
1
2
3 <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,表中会增加第三行。
1
2
3 <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文件中只有一个流程定义,就没有问题。
用编程方式部署的例子:
1
2
3
4
5 repositoryService.createDeployment()
.name("expense-process.bar")
.addClasspathResource("org/activiti/expenseProcess.bpmn20.xml")
.addClasspathResource("org/activiti/expenseProcess.png")
.deploy();
图片资源可用下面的API获取:
1
2
3
4
5
6
7 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引擎会生成流程图。
可以用与部署时提供图片完全相同的方法获取图片资源。
如果由于某种原因,不需要或不希望在部署时生成流程图,可以在流程引擎配置中设置isCreateDiagramOnDeploy
参数:
1 <property name="createDiagramOnDeploy" value="false" />
这样就不会生成流程图了。
6.6. 类别 Category
部署与流程定义都可以定义类别。流程定义的类别使用BPMN文件中的<definitions … targetNamespace="yourCategory" …/>
设置。
部署的类别可用API如此设定:
1
2
3
4
5 repositoryService
.createDeployment()
.category("yourCategory")
...
.deploy();
7. BPMN 2.0介绍 BPMN 2.0 Introduction
7.1. BPMN是什么? What is BPMN?
查看我们关于BPMN 2.0的FAQ条目。
7.2. 定义流程 Defining a process
这个介绍的写作,基于使用Eclipse IDE创建与编辑文件。但其实只有很少的部分使用了Eclipse的特性。可以使用你喜欢的任何其他工具创建包含BPMN 2.0的XML文件。 |
创建一个新的XML文件(在任意项目上右击,选择New→Other→XML-XML File)并命名。确保该文件名以.bpmn20.xml或.bpmn结尾,因为只有这样,引擎才会在部署时选择这个文件。
BPMN 2.0概要(schema)的根元素(root element)是definitions
元素。在这个元素中,可以定义多个流程定义(然而我们建议在每个文件中,只有一个流程定义。这样可以简化已部署流程的管理)。下面显示的是一个空流程定义。请注意definitions
元素最少需要包含xmlns
与targetNamespace
声明。targetNamespace
可以为空,用于对流程定义进行分类。
1
2
3
4
5
6
7
8
9
10 <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分类配置,也可以使用在线概要。
1
2
3 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
来启动这个流程定义的新流程实例。这个方法总会使用流程定义的最后部署版本。
1 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess");
-
请注意这与调用
startProcessInstanceById
方法不同。startProcessInstanceById
方法的参数为Activiti引擎在部署时生成的字符串id,可以通过调用processDefinition.getId()
方法获取。生成id的格式为key:version,长度限制为64字符。如果有ActivitiException
显示生成id过长,请限制流程key参数(即这个id字段)的文字长度。 -
name: 可选属性,映射为
ProcessDefinition
的name参数。引擎自己不会使用这个参数,可以用于例如,在用户界面上显示更用户友好的名字。
[[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注记像是这样:
我们看到的是一个空启动事件 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结构的详细章节):
-
(空)开始事件 (none) start event是流程的入口点(entry point)。
-
用户任务 User Tasks的声明表示了流程中的人工任务。请注意第一个任务分配给accountancy组,而第二个任务分配给management组。查看用户任务分配章节 the section on user task assignment了解关于用户与组如何分配用户任务的更多信息。
-
流程在到达空结束事件 none end event时结束。
-
各元素间通过顺序流 sequence flows链接。顺序流用
source
与target
定义顺序流的流向(direction)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 <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)。
1
2
3 Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("FinancialReportProcess.bpmn20.xml")
.deploy();
现在可以使用在流程定义中定义的id
(参见XML文件中的流程元素)启动新流程实例。请注意这个id
在Activiti术语中被称作key。
1 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的工具中可视化展示流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 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
现在可以通过添加下列逻辑,获取这个任务:
1 List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit").list();
请注意传递给这个操作的用户需要是accountancy组的成员,因为在流程定义中是这么声明的:
1
2
3
4
5 <potentialOwner>
<resourceAssignmentExpression>
<formalExpression>accountancy</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
也可以使用任务查询API,用组名查得相同结果。可以在代码中添加下列逻辑:
1
2 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 (开始流程)'链接后,可以启动我们的业务流程。
前面已经解释过,流程会执行到第一个用户任务。因为使用Fozzie登录,就可以看到在启动流程实例后,他有一个新的候选任务(candidate task)。选择Tasks页面来查看这个新任务。请注意即使流程是由其他人启动的,accountancy组中的每一个人仍然都能看到这个候选任务。
7.3.8. 申领任务 Claiming the task
会计师(accountancy组的成员)现在需要申领任务。申领任务后,这个用户会成为任务的执行人 (assignee),这个任务也会从accountancy组的其他成员的任务列表中消失。申领任务通过编程方式如下实现:
1 taskService.claim(task.getId(), "fozzie");
这个任务现在在申领任务者的个人任务列表中。
1 List<Task> tasks = taskService.createTaskQuery().taskAssignee("fozzie").list();
在Activiti Explorer UI中,点击claim按钮会执行相同操作。这个任务会转移到登录用户的个人任务列表中。也可以看到任务执行人变更为当前登录用户。
7.3.9. 完成任务 Completing the task
会计师(accountancy组的成员)现在需要开始撰写金融报告了。一旦报告完成,他就可以完成任务。这意味着这个任务的所有工作都已经完成。
1 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可以验证这一点,流程执行的存储表中找不到记录。
也可以通过编程方式,使用historyService
验证流程已经结束
1
2
3
4 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启动了一些流程实例。代码中总是获取任务列表而不是一个任务,因此总能执行):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 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)。定时器事件有默认的业务日历,但也可以为每个定时器事件定义,定义业务日历。
1
2
3 <timerEventDefinition activiti:businessCalendarName="custom">
...
</timerEventDefinition>
其中businessCalendarName指向流程引擎配置中的业务日历。如果省略业务日历定义,就使用默认业务日历。
定时器定义必须且只能使用下列的一种元素:
-
timeDate。这个方式指定了ISO 8601格式的固定时间。 在这个时间点,会触发触发器。例如:
1
2
3 <timerEventDefinition>
<timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
-
timeDuration。要定义在触发前,定时器需要等待多长时间,可以用timeDuration作为timerEventDefinition的子元素来指定。使用ISO 8601格式(BPMN 2.0规范要求)。例如(等待10天):
1
2
3 <timerEventDefinition>
<timeDuration>P10D</timeDuration>
</timerEventDefinition>
-
timeCycle。指定重复周期,可用于周期性启动流程,或者为超期用户任务多次发送提醒。这个元素可以使用两种格式。第一种是按照ISO 8601标准定义的循环时间周期。例如(三次重复间隔,每次间隔为10小时):
1
2
3 <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}
1
2
3 <timerEventDefinition>
<timeCycle>R3/PT10H/${EndDate}</timeCycle>
</timerEventDefinition>
如果同时使用了两种指定方式,则系统会使用属性方式定义的endDate。
目前只有BoundaryTimerEvents与CatchTimerEvent支持EndDate功能。
另外,也可以使用cron表达式指定定时周期。下面的例子展示了一个整点启动,每5分钟触发的触发器:
0 0/5 * * * ?
请参考这个教程了解如何使用cron表达式。
请注意: 与普通的Unix cron不同,第一个符号代表的是秒。
重复时间周期更适用于使用相对时间,也就是从某个特定时间点开始计算(比如用户任务开始的时间)。而cron表达式可以使用绝对时间,因此绝对适合用于定时启动事件 timer start events。
可以在定时事件定义中使用表达式,也就是使用流程变量调整定时器定义。这个流程变量必须是包含合适时间格式的字符串,ISO 8601(或者对于循环类型,cron)。
1
2
3
4
5 <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异常使用它们自己的方式处理。
1
2
3 <endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
8.2.4. 信号事件定义 Signal Event Definitions
信号事件,是引用具名信号的事件。信号是全局范围(广播)的事件,并会被传递给所有激活的处理器(等待中的流程实例/捕获信号事件 catching signal events)。
信号事件定义使用signalEventDefinition
元素声明。其signalRef
属性引用一个signal
元素,该signal
元素需要声明为definitions
根元素的子元素。下面摘录一个流程,使用中间事件(intermediate event)抛出与捕获信号事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <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
中的方法可以用于编程方式抛出信号:
1
2 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
可以查询订阅了某一信号事件的所有执行:
1
2
3 List<Execution> executions = runtimeService.createExecutionQuery()
.signalEventSubscriptionName("alert")
.list();
可以使用signalEventReceived(String signalName, String executionId)
方法为这些执行传递这个信号。
信号事件范围 Signal event scope
默认情况下,信号事件在流程引擎全局广播。这意味着你可以在一个流程实例中抛出一个信号事件,而不同流程定义的不同流程实例都会响应这个事件。
然而,有时也会希望只在同一个流程实例中响应信号事件。例如在流程实例中使用异步机制,而两个或多个活动彼此互斥的时候。
要限制信号事件的范围(scope),在信号事件定义中添加(非BPMN 2.0标准!)scope属性:
1 <signal id="alertSignal" name="alert" activiti:scope="processInstance"/>
这个属性的默认值为"global(全局)"。
信号事件示例 Signal Event example(s)
下面是一个关于两个不同的流程通过信号通信的例子。第一个流程在保险政策更新或变更时启动。在变更由人工审核之后,会抛出信号事件,指出政策已经发生了变更:
这个事件可以被所有感兴趣的流程实例捕获。下面是一个订阅这个事件的流程的例子。
请注意:要理解信号事件会广播给所有激活的处理器,这很重要。这意味着在上面的例子中,所有捕获这个信号的流程实例,都会接收这个信号。在这个例子中这就是我们期望的。然而,有的情况下,不希望使用广播方式。考虑下面的流程:
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)引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 <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服务提供的方法中的一种:
1
2
3 ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey, Map<String, Object> processVariables);
这些方法可以使用引用的消息启动流程实例。
如果需要由已有的流程实例接收消息,需要首先将消息与特定的流程实例关联(查看后续章节),然后触发等待中的执行,让其继续。runtime服务提供了下列方法,根据消息事件的订阅,触发执行:
1
2 void messageEventReceived(String messageName, String executionId);
void messageEventReceived(String messageName, String executionId, HashMap<String, Object> processVariables);
查询消息事件订阅 Querying for Message Event subscriptions
-
对于消息启动事件,消息事件的订阅与特定的流程定义相关。这种类型的消息订阅,可以使用
ProcessDefinitionQuery
查询:
1
2
3 ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.messageEventSubscription("newCallCenterBooking")
.singleResult();
因为对于一个消息,只能有一个流程定义订阅,因此这个查询总是返回0或1个结果。如果流程定义更新了,只有该流程定义的最新版本会订阅这个消息事件。
-
对于消息捕获中间事件(intermediate catch message event),消息事件的订阅与特定的执行相关。这种类型的消息订阅,可以使用
ExecutionQuery
查询:
1
2
3
4 Execution execution = runtimeService.createExecutionQuery()
.messageEventSubscriptionName("paymentReceived")
.variableValueEquals("orderId", message.getOrderId())
.singleResult();
这种查询通常有关联查询,并且通常需要了解流程(在这个例子里,对于给定的orderId,至多只有一个流程实例)。
消息事件示例 Message Event example(s)
下面是一个流程的例子,可以使用两种不同的消息启动:
在流程需要通过不同的方式响应不同的启动事件,但是后续使用统一的方式处理时,这就很有用。
8.2.6. 启动事件 Start Events
启动事件指明了流程的起点。启动事件的类型(流程在消息到达时启动,在指定的时间间隔后启动,等等),定义了流程如何启动,并显示为启动事件中的小图标。在XML中,类型由子元素声明来定义。
启动事件“随时捕获”:概念上,事件(随时)等候,直到特定的触发器被触发。
在启动事件中,可以使用下列Activiti专用参数:
-
initiator: 指明保存认证用户(authenticated user)id用的变量名。在流程启动时,该id会使用这个变量名被保存。例如:
1 <startEvent id="request" activiti:initiator="initiator" />
认证用户必须通过IdentityService.setAuthenticatedUserId(String)
方法,在try-finally块中设置,像这样:
1
2
3
4
5
6 try {
identityService.setAuthenticatedUserId("bono");
runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
identityService.setAuthenticatedUserId(null);
}
这段代码在集成在Activiti Explorer应用中。因此可以与表单一起使用。
8.2.7. 空启动事件 None Start Event
描述 Description
“空”启动事件,技术上指的是没有特别指定启动流程实例的触发器。这意味着引擎无法预知何时启动流程实例。空启动事件用于流程实例通过调用下列startProcessInstanceByXXX API方法启动的情况。
1 ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();
请注意:子流程(subprocess)总是有空启动事件。
图示 Graphical notation
空启动事件用空心圆圈表示,中间没有图标(也就是说没有触发器)。
XML表示 XML representation
空启动事件的XML表示格式,就是普通的启动事件声明,而没有任何子元素(其他种类的启动事件都有子元素,用于声明其类型)。
1 <startEvent id="start" name="my start event" />
空启动事件的自定义扩展 Custom extensions for the none start event
formKey: 引用表单模板,用户需要在启动新流程实例时填写该表单。可以在表单章节找到更多信息。例如:
1 <startEvent id="request" activiti:formKey="org/activiti/examples/taskforms/request.form" />
8.2.8. 定时器启动事件 Timer Start Event
描述 Description
定时器启动事件,用于在指定时间创建流程实例。在流程只需要启动一次,或者流程需要在特定的时间间隔重复启动时,都可以使用。
请注意:子流程不能有定时器启动事件。
请注意:定时器启动事件,在流程部署的同时就开始计时。不需要调用startProcessInstanceByXXX,尽管也不禁止使用启动流程的方法。调用startProcessInstanceByXXX时也会启动流程。
请注意:当部署带有定时器启动事件的流程的新版本时,上一版本的定时器作业会被移除。这是因为通常并不希望旧版本的流程仍然自动启动新的流程实例。
图示 Graphical notation
定时器启动事件,用其中有一个钟表图标的圆圈来表示。
XML表示 XML representation
定时器启动事件的XML表示格式,是普通的启动事件声明,加上定时器定义子元素。请参考定时器定义了解详细配置方法。
示例:流程会启动4次,间隔5分钟,从2011年3月11日,12:13开始
1
2
3
4
5 <startEvent id="theStart">
<timerEventDefinition>
<timeCycle>R4/2011-03-11T12:13/PT5M</timeCycle>
</timerEventDefinition>
</startEvent>
示例:流程会在选定的时间启动一次
1
2
3
4
5 <startEvent id="theStart">
<timerEventDefinition>
<timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
</startEvent>
8.2.9. 消息启动事件 Message Start Event
描述 Description
消息启动事件,使用具名消息启动流程实例。它让我们可以使用消息名,有效地在一组可选的启动事件中选择正确的启动事件。
当部署具有一个或多个消息启动事件的流程定义时,会考虑下列因素:
-
消息启动事件的名字,在给定流程定义中,必须是唯一的。一个流程定义不得包含多个同名的消息启动事件。如果流程定义中有两个或多个消息启动事件引用 同一个消息,也即两个或多个消息启动事件引用了具有相同消息名字的消息,则Activiti在部署这个流程定义时,会抛出异常。
-
消息启动事件的名字,在所有已部署的流程定义中,必须是唯一的。如果流程定义中,一个或多个消息启动事件,引用了已经部署的另一流程定义中消息启动事件的消息名,则Activiti在部署这个流程定义时,会抛出异常。
-
流程版本:在部署流程定义的新版本时,会取消上一版本的消息订阅。即使新版本中并没有这个消息事件,仍然如此(取消上版本的消息订阅)。
当启动流程实例时,可以使用下列RuntimeService
中的方法,触发消息启动事件:
1
2
3 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
元素被messageEventDefinition
的messageRef
属性引用。当启动流程实例时,请考虑下列因素:
-
只有顶层流程(top-level process)才支持消息启动事件。嵌入式子流程不支持消息启动事件。
-
如果一个流程定义中有多个消息启动事件,
runtimeService.startProcessInstanceByMessage(…)
允许选择合适的启动事件。 -
如果一个流程定义中有多个消息启动事件,与一个空启动事件,则
runtimeService.startProcessInstanceByKey(…)
与runtimeService.startProcessInstanceById(…)
会使用空启动事件启动流程实例。 -
如果一个流程定义中有多个消息启动事件,而没有空启动事件,则
runtimeService.startProcessInstanceByKey(…)
与runtimeService.startProcessInstanceById(…)
会抛出异常。 -
如果一个流程定义中只有一个消息启动事件,则
runtimeService.startProcessInstanceByKey(…)
与runtimeService.startProcessInstanceById(…)
会使用这个消息启动事件启动新流程实例。 -
如果流程由调用活动(call activity)启动,则消息启动事件只有在下列情况下才被支持
-
除了消息启动事件,流程还有唯一的空启动事件
-
或者流程只有唯一的消息启动事件,而没有其他启动事件。
-
图示 Graphical notation
消息启动事件,用其中有一个消息事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。
XML表示 XML representation
消息启动事件的XML表示格式,为普通启动事件声明,加上messageEventDefinition子元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <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
元素被signalEventDefinition
的signalRef
属性所引用。
图示 Graphical notation
信号启动事件,用其中有一个信号事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。
XML表示 XML representation
信号启动事件的XML表示格式,为普通启动事件声明,加上signalEventDefinition子元素:
1
2
3
4
5
6
7
8
9
10
11 <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
图示 Graphical notation
错误启动事件,用其中有一个错误事件标志的圆圈表示。这个标志并未填充,用以表示捕获(接收)行为。
XML表示 XML representation
错误启动事件的XML表示格式,为普通启动事件声明,加上errorEventDefinition子元素:
1
2
3 <startEvent id="messageStart" >
<errorEventDefinition errorRef="someError" />
</startEvent>
8.2.12. 结束事件 End Events
结束事件标志着(子)流程的(分支的)结束。结束事件总是抛出(型)事件。这意味着当流程执行到达结束事件时,会抛出一个结果。结果的类型由事件内部的黑色图标描绘。在XML表示中,类型由子元素声明给出。
8.2.13. 空结束事件 None End Event
描述 Description
“空”结束事件,意味着当到达这个事件时,抛出的结果没有特别指定。因此,引擎除了结束当前执行分支之外,不会多做任何事情。
图示 Graphical notation
空结束事件,用其中没有图标(没有结果类型)的粗圆圈表示。
XML表示 XML representation
空事件的XML表示格式,为普通结束事件声明,没有任何子元素(其它种类的结束事件都有子元素,用于声明其类型)。
1 <endEvent id="end" name="my end event" />
8.2.14. 错误结束事件 Error End Event
描述 Description
当流程执行到达错误结束事件时,结束执行的当前分支,并抛出错误。这个错误可以使用匹配的错误边界中间事件 intermediate boundary error event 捕获。如果找不到匹配的错误边界事件,将会抛出异常。
图示 Graphical notation
错误结束事件事件,用内部有一个错误图标的标准结束事件(粗圆圈)表示。错误图标是全黑的,代表抛出的含义。
XML表示 XML representation
错误结束事件,表示为结束事件,加上errorEventDefinition子元素:
1
2
3 <endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
errorRef属性可以引用在流程外定义的error元素:
1
2
3
4 <error id="myError" errorCode="123" />
...
<process id="myProcess">
...
error的errorCode用于查找匹配的错误捕获边界事件。如果errorRef不匹配任何已定义的error,则该errorRef会用做errorCode的快捷方式。这个快捷方式是Activiti特有的。下面的代码片段在功能上是相同的。
1
2
3
4
5
6
7
8 <error id="myError" errorCode="error123" />
...
<process id="myProcess">
...
<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
...
与下面的功能相同
1
2
3 <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
终止结束事件,用内部有一个全黑圆的标准结束事件(粗圆圈)表示。
XML表示 XML representation
终止结束事件,表示为结束事件,加上terminateEventDefinition子元素。
请注意terminateAll属性是可选的(默认为false)。
1
2
3 <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
取消结束事件,用内部有一个取消图标的标准结束事件(粗圆圈)表示。取消图标是全黑的,代表抛出的含义。
XML表示 XML representation
取消结束事件,表示为结束事件,加上cancelEventDefinition子元素。
1
2
3 <endEvent id="myCancelEndEvent">
<cancelEventDefinition />
</endEvent>
8.2.17. 边界事件 Boundary Events
边界事件是捕获(型)事件,依附在活动(activity)上(边界事件永远不会抛出)。这意味着当活动运行时,事件在监听特定类型的触发器。当事件捕获时,活动会被终止,并沿该事件的出口顺序流继续。
所有的边界事件都用相同的方式定义:
1
2
3 <boundaryEvent id="myBoundaryEvent" attachedToRef="theActivity">
<XXXEventDefinition/>
</boundaryEvent>
边界事件由下列(元素)定义:
-
唯一标识符(流程范围)
-
通过attachedToRef属性定义的,对该事件所依附的活动的引用。请注意边界事件,与其所依附的活动,定义在相同级别(也就是说,边界事件并不包含在活动内部)。
-
定义了边界事件的类型的,XXXEventDefinition形式的XML子元素(例如TimerEventDefinition,ErrorEventDefinition,等等)。查阅特定边界事件类型,以了解更多细节。
8.2.18. 定时器边界事件 Timer Boundary Event
描述 Description
定时器边界事件的行为像是跑表与闹钟。当执行到达边界事件所依附的活动时,启动定时器。当定时器触发时(例如在特定事件间隔后),活动会被中断,沿着边界事件继续执行。
图示 Graphical Notation
定时器边界事件,用内部有一个定时器图标的标准边界事件(圆圈)表示。
XML表示 XML Representation
定时器边界事件与一般边界事件一样定义。其中类型子元素为timerEventDefinition元素。
1
2
3
4
5 <boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>PT4H</timeDuration>
</timerEventDefinition>
</boundaryEvent>
请参考定时器事件定义了解定时器配置的细节。
上面的例子在图示中,圆圈画为虚线:
其典型使用场景,是发送额外的升级邮件,但不中断正常的流程流向。
在BPMN 2.0中,中断与非中断定时器事件是不同的。默认为中断。非中断意味着最初的活动不会被中断,而会保留。并会创建额外的执行,用于处理事件的出口转移(outgoing transition)。在XML表示中,cancelActivity属性设置为false。
1 <boundaryEvent id="escalationTimer" cancelActivity="false" attachedToRef="firstLineSupport"/>
请注意:定时器边界事件只有在作业或异步执行器启用时才能触发(也就是说,需要在activiti.cfg.xml
中,将jobExecutorActivate或者asyncExecutorActivate设置为true
。因为默认情况下,作业与异步执行器都是禁用的。)
边界事件的已知问题 Known issue with boundary events
所有类型的边界事件,都有一个关于并发的已知问题。不能在边界事件上附加多个出口顺序流(查看问题ACT-47)。这个问题的解决方案,是使用一条出口顺序流,指向并行网关。
8.2.19. 错误边界事件 Error Boundary Event
描述 Description
在活动边界上的错误捕获中间(事件),或简称错误边界事件,捕获其依附的活动范围内抛出的错误。
在嵌入式子流程或者调用活动上定义错误边界事件最有意义,因为子流程会为其中的所有活动创建范围。错误由错误结束事件抛出。这样的错误会逐层向其上级父范围传播,直到找到一个错误边界事件的范围,该范围定义了匹配的错误事件定义。
当错误事件被捕获时,边界事件定义所在的活动会被销毁,同时销毁其中所有的当前执行(例如,并行活动,嵌套子流程,等等)。流程执行沿着边界事件的出口顺序流继续。
图示 Graphical notation
错误边界事件,用内部有一个错误图标的标准中间事件(两层圆圈)表示。错误图标是白色的,代表捕获的含义。
XML表示 Xml representation
错误边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="catchError" attachedToRef="mySubProcess">
<errorEventDefinition errorRef="myError"/>
</boundaryEvent>
在边界事件中,errorRef引用一个流程元素外定义的错误:
1
2
3
4 <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 (提供更多信息)'用户任务。
这个流程作为演示配置的示例提供。可以在org.activiti.examples.bpmn.event.error包中找到流程XML与单元测试。
8.2.20. 信号边界事件 Signal Boundary Event
描述 Description
依附在活动边界上的信号捕获中间(事件),或简称信号边界事件,捕获与其信号定义具有相同信号名的信号。
请注意:与其他事件例如错误边界事件不同的是,信号边界事件不只是捕获其所依附范围抛出的信号。信号边界事件为全局范围(广播)的,意味着信号可以从任何地方抛出,甚至是不同的流程实例。
请注意:与其他事件如错误事件不同,信号在被捕获后不会被消耗。如果有两个激活的信号边界事件,捕获相同的信号事件,则两个边界事件都会被触发,哪怕它们不在同一个流程实例里。
图示 Graphical notation
信号边界事件,用内部有一个信号图标的标准中间事件(两层圆圈)表示。信号图标是白色的,代表捕获的含义。
XML表示 XML representation
信号边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<signalEventDefinition signalRef="alertSignal"/>
</boundaryEvent>
示例 Example
查看信号事件定义章节内容。
8.2.21. 消息边界事件 Message Boundary Event
描述 Description
在活动边界上的消息捕获中间(事件),或简称消息边界事件,捕获与其消息定义具有相同消息名的消息。
图示 Graphical notation
消息边界事件,用内部有一个消息图标的标准中间事件(两层圆圈)表示。信号图标是白色的,代表捕获的含义。
请注意消息边界事件既可以是中断型的(右手边),也可以是非中断型的(左手边)。
XML表示 XML representation
消息边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<messageEventDefinition messageRef="newCustomerMessage"/>
</boundaryEvent>
示例 Example
查看消息事件定义章节内容。
8.2.22. 取消边界事件 Cancel Boundary Event
描述 Description
依附在事务子流程边界上的取消捕获中间(事件),或简称取消边界事件,在事务取消时触发。当 取消边界事件触发时,首先会中断当前范围的所有活动执行。接下来,启动事务范围内所有有效的的补偿边界事件(compensation boundary event)。补偿会同步执行,也就是说在离开事务前,边界事件会等待补偿完成。当补偿完成时,使用取消边界事件的出口顺序流,离开事务子流程。
请注意:一个事务子流程只允许一个取消边界事件。
请注意:如果事务子流程中有嵌套的子流程,只有成功完成的子流程才会触发补偿。
请注意:如果取消边界事件放置在具有多实例特性的事务子流程上,如果一个实例触发了取消,则边界事件将取消所有实例。
图示 Graphical notation
取消边界事件,用内部有一个取消图标的标准中间事件(两层圆圈)表示。取消图标是白色的(未填充),代表捕获的含义。
XML表示 XML representation
取消边界事件与标准边界事件一样定义:
1
2
3 <boundaryEvent id="boundary" attachedToRef="transaction" >
<cancelEventDefinition />
</boundaryEvent>
因为取消边界事件总是中断型的,因此不需要cancelActivity
属性。
8.2.23. 补偿边界事件 Compensation Boundary Event
描述 Description
依附在活动边界上的补偿捕获中间(事件),或简称补偿边界事件,可以为活动附加补偿处理器。
补偿边界事件必须通过直接关联的方式,引用单个的补偿处理器。
补偿边界事件与其它边界事件的活动策略不同。其它边界事件,例如信号边界事件,当其依附的活动启动时激活;当离开该活动时,会被解除,并取消相应的事件订阅。而补偿边界事件不是这样。补偿边界事件在其依附的活动成功完成时激活,同时创建补偿事件的相应订阅。当补偿事件被触发,或者相应的流程实例结束时,才会移除订阅。请考虑下列因素:
-
当补偿被触发时,补偿边界事件关联的补偿处理器会被调用,次数与其依附的活动成功完成的次数相同。
-
如果补偿边界事件依附在具有多实例特性的活动上,则会为每一个实例创建补偿事件订阅。
-
如果补偿边界事件依附在位于循环内部的活动上,则每次该活动执行时,都会创建一个补偿事件订阅。
-
如果流程实例结束,则取消补偿事件的订阅。
请注意:嵌入式子流程不支持补偿边界事件。
图示 Graphical notation
补偿边界事件,用内部有一个补偿图标的标准中间事件(两层圆圈)表示。补偿图标是白色的(未填充),代表捕获的含义。另外,补偿边界事件使用单向连接关联补偿处理器,如下图所示:
XML表示 XML representation
补偿边界事件与标准边界事件一样定义:
1
2
3
4
5
6
7 <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
所有的捕获中间事件都使用相同方式定义:
1
2
3 <intermediateCatchEvent id="myIntermediateCatchEvent" >
<XXXEventDefinition/>
</intermediateCatchEvent>
捕获中间事件由下列(元素)定义
-
唯一标识符(流程范围)
-
定义了捕获中间事件类型的,XXXEventDefinition形式的XML子元素(例如TimerEventDefinition等)。查阅特定中间捕获事件类型,以了解更多细节。
8.2.25. 定时器捕获中间事件 Timer Intermediate Catching Event
描述 Description
定时器捕获中间事件的行为像是跑表。当执行到达捕获事件活动(catching event activity)时,启动定时器;当定时器触发时(例如在一段时间间隔后),沿定时器中间事件的出口顺序流继续执行。
图示 Graphical Notation
定时器中间事件,用内部有定时器图标的中间捕获事件表示。
8.2.26. 信号捕获中间事件 Signal Intermediate Catching Event
描述 Description
信号捕获中间事件,捕获与其引用的信号定义具有相同信号名称的信号。
请注意:与其他事件如错误事件不同,信号在被捕获后不会被消耗。如果有两个激活的信号中间事件,捕获相同的信号事件,则两个中间事件都会被触发,哪怕它们不在同一个流程实例里。
图示 Graphical notation
信号捕获中间事件,用内部有信号图标的标准中间事件(两层圆圈)表示。信号图标是白色的(未填充),代表捕获的含义。
XML表示 XML representation
信号中间事件与捕获中间事件一样定义。指定类型的子元素为signalEventDefinition元素。
1
2
3 <intermediateCatchEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateCatchEvent>
示例 Example
查看信号事件定义章节。
8.2.27. 消息捕获中间事件 Message Intermediate Catching Event
描述 Description
消息捕获中间事件,捕获特定名字的消息。
图示 Graphical notation
消息捕获中间事件,用内部有消息图标的标准中间事件(两层圆圈)表示。消息图标是白色的(未填充),代表捕获的含义。
XML表示 XML representation
消息中间事件与捕获中间事件一样定义。指定类型的子元素为messageEventDefinition元素。
1
2
3 <intermediateCatchEvent id="message">
<messageEventDefinition signalRef="newCustomerMessage" />
</intermediateCatchEvent>
示例 Example
查看消息事件定义章节。
8.2.28. 抛出中间事件 Intermediate Throwing Event
所有的抛出中间事件都使用相同方式定义:
1
2
3 <intermediateThrowEvent id="myIntermediateThrowEvent" >
<XXXEventDefinition/>
</intermediateThrowEvent>
抛出中间事件由下列(元素)定义
-
唯一标识符(流程范围)
-
定义了抛出中间事件类型的,XXXEventDefinition形式的XML子元素(例如signalEventDefinition等)。查阅特定中间抛出事件类型,以了解更多细节。
8.2.29. 空抛出中间事件 Intermediate Throwing None Event
下面的流程图展示了空中间事件的简单例子,其用于指示流程已经到达了某种状态。
基本上添加一个执行监听器 execution listener后,空中间事件就可以成为很好的监视某些KPI(Key Performance Indicators 关键绩效指标)的钩子。
1
2
3
4
5 <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
消息抛出中间事件,用内部有信号图标的标准中间事件(两层圆圈)表示。信号图标是黑色的(已填充),代表抛出的含义。
XML表示 XML representation
信号中间事件与抛出中间事件一样定义。指定类型的子元素为signalEventDefinition元素。
1
2
3 <intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateThrowEvent>
异步信号事件像这样定义:
1
2
3 <intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" activiti:async="true" />
</intermediateThrowEvent>
示例 Example
查看信号事件定义章节。
8.2.31. 补偿抛出中间事件 Compensation Intermediate Throwing Event
描述 Description
补偿抛出中间事件,可用于触发补偿。
触发补偿:补偿既可以为设计的活动触发,也可以为补偿事件所在的范围触发。补偿由活动所关联的补偿处理器执行。
-
抛出补偿时,活动关联的补偿处理器执行的次数,与活动成功完成的次数相同。
-
如果为当前范围抛出了补偿,则当前范围中所有的活动都会被补偿,包括并行分支上的活动。
-
补偿分层触发:如果将要被补偿的活动是一个子流程,则该子流程中所有的活动都会触发补偿。如果该子流程有嵌套的活动,则会递归地抛出补偿。然而,补偿不会传播至流程的上层:如果子流程中触发了补偿,该补偿不会传播至子流程范围外的活动。BPMN规范指出,补偿为“与子流程在相同级别”的活动触发。
-
在Activiti中,补偿按照执行的相反顺序运行。这意味着最后完成的活动会第一个补偿,等等。
-
补偿抛出中间事件,可用于补偿已经成功完成的事务子流程。
请注意:如果抛出补偿的范围中有一个子流程,而该子流程包含有关联了补偿处理器的活动,则当抛出补偿时,只有当 该子流程成功完成的情况,补偿才会传播至该子流程。如果子流程内嵌套的部分活动已经完成,并附加了补偿处理器,则如果包含这些活动的子流程还没有完成,这 些补偿处理器不会执行。参考下面的例子:
在这个流程中,有两个并行的执行。一个执行嵌入子流程,另一个执行“charge credit card(信用卡付款)”活动。假定两个执行都已开始,且第一个并行执行正等待用户完成“review bookings(检查预定)”任务。第二个执行进行了“charge credit card(信用卡付款)”活动的操作,抛出了一个错误,导致“cancel reservations(取消预订)”事件触发补偿。这时并行子流程还未完成,意味着补偿不会传播至该子流程,因此不会执行“cancel hotel reservation(取消酒店预订)”补偿处理器。而如果“cancel reservations(取消预订)”运行前,这个用户任务(因此该嵌入式子流程也)已经完成,则补偿会传播至该嵌入式子流程。
流程变量:当补偿嵌入式子流程时,用于执行补偿处理器的执行,可以以变量在子流程完成时所处的状态,访问子流程的局部流程变量。围了实现这一点,会为范围执行(为执行子流程所创建的执行)所关联的流程变量,进行快照。意味着:
-
子流程范围内创建的并行执行所添加的变量,补偿执行器无法访问。
-
上层的执行关联的流程变量(例如流程实例的执行关联的流程变量),不在该快照中:补偿处理器(本就)可以以其在抛出补偿时所处的状态,访问这些流程变量。
-
只会为嵌入式子流程,而不会为其他活动,进行变量快照。
目前的限制:
-
目前不支持
waitForCompletion="false"
。当补偿抛出中间事件触发补偿时,只有在补偿成功完成时,才会离开该事件。 -
补偿现在由并行执行来运行。该并行执行按照补偿活动完成的逆序启动。Activiti的后续版本可能会添加选项,使补偿可以按(活动完成的)顺序运行。
-
补偿不会传播至调用活动(call activity)生成的子流程。
图示 Graphical notation
补偿抛出中间事件,用内部有补偿图标的标准中间事件(两层圆圈)表示。补偿图标是黑色的(已填充),代表抛出的含义。
Xml representation
补偿中间事件与抛出中间事件一样定义。指定类型的子元素为compensateEventDefinition元素。
1
2
3 <intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition />
</intermediateThrowEvent>
另外,activityRef
可选项可用于为指定的范围/活动触发补偿:
1
2
3 <intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition activityRef="bookHotel" />
</intermediateThrowEvent>
8.3. 顺序流 Sequence Flow
8.3.1. 描述 Description
顺序流是流程中两个元素间的连接器。当流程执行中,一个元素被访问后,会沿着所有的出口顺序流继续。这意味着BPMN 2.0的默认性质是并行的:两个出口顺序流,会创建两个独立的,并行的执行路径。
8.3.2. 图示 Graphical notation
顺序流,用从源元素指向目标元素的箭头表示。箭头总是指向目标元素。
8.3.3. XML表示 XML representation
顺序流需要有流程唯一的id,以及对存在的源与目标元素的引用。
1 <sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
8.3.4. 条件顺序流 Conditional sequence flow
描述 Description
在顺序流上可以定义条件。当离开BPMN 2.0活动时,默认行为是计算其出口顺序流上的条件。当条件计算为true时,选择该出口顺序流。如果该方法选择了多条顺序流,则会生成多个执行,流程会以并行方式继续。
请注意:上面的介绍对BPMN 2.0活动(与事件)有效,但不适用于网关(gateway)。不同类型的网关,会用不同的方式处理带有条件的顺序流。
图示 Graphical notation
条件顺序流,用起点带有小菱形的一般顺序流表示。条件表达式挨着顺序流显示。
XML表示 XML representation
条件顺序流的XML表示格式,为含有conditionExpression(条件表达式)子元素的普通顺序流。请注意目前只支持tFormalExpressions。省略xsi:type=""定义会默认为唯一支持的表达式类型。
1
2
3
4
5 <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引用流程变量的数据。
1
2
3 <conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
-
这个例子调用了一个解析为boolean值的方法。
1
2
3 <conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.isStandardOrder()}]]>
</conditionExpression>
Activiti发行版中包含了下列示例流程,展示值表达式与方法表达式的使用(参见org.activiti.examples.bpmn.expression)。
8.3.5. 默认顺序流 Default sequence flow
描述 Description
所有的BPMN 2.0任务与网关,都可以使用默认顺序流。这种顺序流只有当没有其他顺序流可以选择时,才会被选择为活动的出口顺序流。默认顺序流上的条件会被忽略。
图示 Graphical notation
默认顺序流,用起点带有“斜线”标记的一般顺序流表示。
XML表示 XML representation
活动的默认顺序流,由该活动的default属性定义。下面的XML片段展示了一个排他网关(exclusive gateway),带有默认顺序流flow 2。只有当conditionA与conditionB都计算为false时,默认顺序流才会被选择为网关的出口顺序流。
1
2
3
4
5
6
7
8 <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 标志)。网关可以消耗与生成标志。
网关用其中带有图标的菱形表示。该图标显示了网关的类型。
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的菱形标志。
XML表示 XML representation
排他网关的XML表示格式很直接:一行定义网关的XML,而条件表达式定义在出口顺序流上。查看条件顺序流章节了解这种表达式的可用选项。
以下面的模型为例:
用XML表示如下:
1
2
3
4
5
6
7
8
9
10
11
12
13 <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)的含义。
XML表示 XML representation
定义并行网关需要一行XML:
1 <parallelGateway id="myParallelGateway" />
实际行为(分支,合并或两者皆有),由连接到该并行网关的顺序流定义。
例如,上面的模型表现为下面的XML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <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" />
在上面的例子中,当流程启动后,会创建两个任务:
1
2
3
4
5
6
7
8
9
10
11
12
13 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中是合法的:
8.4.3. 包容网关 Inclusive Gateway
描述 Description
包容网关可被视作排他网关与并行网关的组合。与排他网关一样,可以在出口顺序流上定义条件,包容网关会计算它们。然而主要的区别是,包容网关与并行网关一样,可以选择多于一条(出口)顺序流。
包容网关的功能,基于其入口与出口顺序流:
-
分支:所有出口顺序流的条件都会被计算,对于条件计算为true的顺序流,流程会并行地沿其继续,为每一条顺序流创建一个并行执行。
-
合并:所有到达包容网关的并行执行,都会在网关处等待,直到每一条具有流程标志的入口顺序流,都有一个执行到达。这是与并行网关的重要区别。换句话说,包容网关只会等待将会被执行的入口顺序流。在合并后,流程穿过合并并行网关继续。
请注意,如果包容网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有具有流程标志的入口顺序流,然后为条件计算为true的出口顺序流,分裂为多条并行执行路径。
图示 Graphical Notation
包容网关,用内部带有’圆圈’图标的网关(菱形)表示。
XML表示 XML representation
定义包容网关需要一行XML:
1 <inclusiveGateway id="myInclusiveGateway" />
实际行为(分支,合并或两者皆有),由连接到该包容网关的顺序流定义。
例如,上面的模型表现为下面的XML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 <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(传递订单)一个任务会被创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 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
基于事件的网关,用内部带有特殊图标的网关(菱形)表示。
XML表示 XML representation
用于定义基于事件的网关的XML元素为eventBasedGateway
。
示例 Example(s)
下面的流程,是带有基于事件的网关的流程的例子。当执行到达基于事件的网关时,流程执行被暂停。并且,流程实例订阅alert信号事件,并创建一个 10分钟后触发的定时器。这使得流程引擎等待10分钟,并等待信号事件。如果信号在10分钟内触发,则定时器会被取消,执行沿着信号继续。如果信号未被触 发,执行会在定时器到时后继续,并取消信号订阅。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 <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
用户任务,用左上角有一个小用户图标的标准任务(圆角矩形)表示。
XML表示 XML representation
用户任务在XML中如下定义。id是必须属性,name是可选属性。
1 <userTask id="theTask" name="Important task" />
一个用户任务也可以有一个描述(description)。事实上任何BPMN 2.0元素都可以有一个描述。描述使用附加的documentation元素定义。
1
2
3
4 <userTask id="theTask" name="Schedule meeting" >
<documentation>
Schedule an engineering meeting for next week with the new hire.
</documentation>
描述文本可以从任务中,使用标准Java方式获取:
1 task.getDescription()
到期日期 Due Date
每个任务都有一个字段,标志该任务的到期日期。可以使用查询API,查询在给定日期前或后到期的任务。
有一个Activiti的扩展,可以在任务定义中指定表达式,以在任务创建时,设定初始到期日期。该表达式必须解析为java.util.Date
,java.util.String (ISO8601格式)
,ISO8601时间长度(例如PT50M),或者null
。例如,可以使用在流程里前一个表单中输入的日期,或者由前一个服务任务计算出的日期。如果使用的是时间长度,则到期日期基于当前时间加上给定长度计算。例如当dueDate使用“PT30M”时,任务在从现在起30分钟后到期。
1 <userTask id="theTask" name="Important task" activiti:dueDate="${dateVariable}"/>
任务的到期日期,也可以使用TaskService
,或者在TaskListener
中使用传递的DelegateTask
修改。
用户指派 User assignment
一个用户任务可以直接指派给一个用户。可以通过定义humanPerformer子元素实现。这个humanPerformer定义需要resourceAssignmentExpression来实际定义用户。目前,只支持formalExpressions。
1
2
3
4
5
6
7
8
9
10
11 <process >
...
<userTask id='theTask' name='important task' >
<humanPerformer>
<resourceAssignmentExpression>
<formalExpression>kermit</formalExpression>
</resourceAssignmentExpression>
</humanPerformer>
</userTask>
只有一个用户可被指定为任务的humanPerformer。在Activiti术语中,这个用户被称作办理人(assignee)。拥有办理人的任务,在其他人的任务列表中不可见,而可以在该办理人的个人任务列表中看到。
特定用户办理的任务,可以通过TaskService如下获取:
1 List<Task> tasks = taskService.createTaskQuery().taskAssignee("kermit").list();
任务也可以放在用户的候选任务列表中。在这个情况下,需要使用potentialOwner(潜在用户)结构。用法与humanPerformer结构类似。请注意需要为表达式中的每一个元素指定其为用户还是组(引擎无法自行判断)。
1
2
3
4
5
6
7
8
9
10
11 <process >
...
<userTask id='theTask' name='important task' >
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>user(kermit), group(management)</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>
定义了potential owner结构的任务,可用如下方法获取(或类似于指派用户任务,使用TaskQuery查询):
1 List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit");
将获取所有kermit作为候选用户的任务,也就是说,表达式含有user(kermit)的任务,也将获取所有指派给kermit为其成员的组的任务(例如group(management),如果kermit是这个组的成员,并且使用Activiti身份组件)。组会在运行时解析,并可通过IdentityService(身份服务)管理。
如果并未指定给定字符串是用户还是组,引擎默认其为组。因此下列代码与声明了group(accountancy)一样。
1 <formalExpression>accountancy</formalExpression>
用于任务指派的Activiti扩展 Activiti extensions for task assignment
很明显,当指派关系不复杂时,这种用户与组的指派方式十分笨重。为避免这种复杂性,可以在用户任务上使用自定义扩展。
-
assignee(办理人)属性:这个自定义扩展用于直接将一个用户任务指派至一个给定用户。
1 <userTask id="theTask" name="my task" activiti:assignee="kermit" />
与使用上面定义的humanPerformer结构完全相同。
-
candidateUsers(候选用户)属性:这个自定义扩展用于为一个任务指定候选用户。
1 <userTask id="theTask" name="my task" activiti:candidateUsers="kermit, gonzo" />
与使用上面定义的potentialOwner结构完全相同。请注意不需要像在potential owner中一样,使用user(kermit)的声明,因为这个属性只能用于用户。
-
candidateGroups(候选组)attribute:这个自定义扩展用于为一个任务指定候选组。
1 <userTask id="theTask" name="my task" activiti:candidateGroups="management, accountancy" />
与使用上面定义的potentialOwner结构完全相同。请注意不需要像在potential owner中一样,使用group(management)的声明,因为这个属性只能用于组。
-
candidateUsers与candidateGroups可以定义在同一个用户任务上。
请注意:尽管Activiti提供了身份管理组件,通过IdentityService暴露,但并不会检查给定的用户是否在身份组件中存在。这样Activiti在嵌入应用时,可以与已有的身份管理解决方案集成。
自定义身份联系类型(试验特性) Custom identity link types (Experimental)
在用户指派中定义过,BPMN标准支持单个指派用户即hunamPerformer,或者一组用户构成potentialOwners潜在用户池。另外,Activiti为用户任务定义了扩展属性元素,代表任务的办理人或者候选用户。
Activiti支持的身份联系类型有:
1
2
3
4
5
6
7
8 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的身份管理实现并不适用于生产环境,而需要在支持的认证概要下扩展。
如果需要添加额外的联系类型,可按照下列语法,使用自定义资源作为扩展元素:
1
2
3
4
5
6
7
8
9 <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类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 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类,以支持自定义身份联系类型:
1
2
3
4
5
6
7
8
9 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)上使用任务监听器,代理自定义指派逻辑:
1
2
3
4
5 <userTask id="task1" name="My task" >
<extensionElements>
<activiti:taskListener event="create" class="org.activiti.MyAssignmentHandler" />
</extensionElements>
</userTask>
传递至TaskListener
实现的DelegateTask
,可用于设置办理人与候选用户/组:
1
2
3
4
5
6
7
8
9
10
11
12
13 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参数是一个流程变量。
1 <userTask id="task" name="My Task" activiti:assignee="${ldapService.findManagerForEmployee(emp)}"/>
也可以用于候选用户与组:
1 <userTask id="task" name="My Task" activiti:candidateUsers="${ldapService.findAllSales()}"/>
请注意调用方法的返回类型必须是String
或Collection<String>
(候选用户与组):
1
2
3
4
5
6
7
8
9
10
11 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任务(圆角矩形)表示。
XML表示 XML representation
脚本任务通过指定script与scriptFormat定义。
1
2
3
4
5
6
7
8 <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的一部分。因此,现在必须添加如下依赖:
1
2
3
4
5 <dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.x.x<version>
</dependency>
脚本中的变量 Variables in scripts
到达脚本引擎的执行可以访问的所有流程变量,都可以在脚本中使用。在这个例子里,脚本变量'inputArray'实际上就是一个流程变量(integer数组)。
1
2
3
4
5
6 <script>
sum = 0
for ( i in inputArray ) {
sum += i
}
</script>
也可以简单地调用execution.setVariable("variableName", variableValue),在脚本中设置流程变量。默认情况下,变量不会自动储存(请注意,在Activiti 5.12以前是这样的!)。可以将scriptTask
的autoStoreVariables
参数设置为true
,以自动保存任何在脚本中定义的变量(例如上例中的sum)。然而,最佳实践不是这么做,而是直接调用execution.setVariable(),因为在JDK近期的一些版本中,某些脚本语言不能自动保存变量。查看这个链接了解更多信息。
1 <scriptTask id="script" scriptFormat="JavaScript" activiti:autoStoreVariables="false">
这个参数的默认值为false
,意味着这个参数将在脚本任务定义中忽略,所有声明的变量将只在脚本执行期间有效。
在脚本中设置变量的例子:
1
2
3
4 <script>
def scriptVar = "test123"
execution.setVariable("myVar", scriptVar)
</script>
请注意:下列名字被保留,不能用于变量名:out,out:print,lang:import,context,elcontext。
脚本结果 Script results
脚本任务的返回值,可以通过为脚本任务定义的'activiti:resultVariable'属性设置流程变量名,指定为已经存在的,或者新的流程变量。指定的已有值的流程变量,会被脚本执行的结果值覆盖。当不指定结果变量名时,脚本结果值将被忽略。
1
2
3 <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
服务任务,用左上角有一个小齿轮图标的圆角矩形表示。
XML表示 XML representation
有四种方法声明如何调用Java逻辑:
-
指定实现了JavaDelegate或ActivityBehavior的类
-
对解析为代理对象的表达式求值
-
调用方法表达式
-
对值表达式求值
要指定流程执行时调用的类,需要使用activiti:class属性提供全限定类名(fully qualified classname)。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
activiti:class="org.activiti.MyJavaDelegate" />
查看实现章节,了解关于如何使用这种类的更多信息。
也可以使用解析为对象的表达式。该对象必须遵循的规则,与使用activiti:class
创建的对象规则相同(查看更多)。
1 <serviceTask id="serviceTask" activiti:delegateExpression="${delegateExpressionBean}" />
这里,delegateExpressionBean
是一个实现了JavaDelegate
接口的bean,在Spring容器中定义。
要指定需要计算的UEL方法表达式,使用activiti:expression属性。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
activiti:expression="#{printer.printMessage()}" />
将在名为printer
的对象上调用printMessage
方法(不带参数)。
也可以为表达式中使用的方法传递变量。
1
2
3 <serviceTask id="javaService"
name="My Java Service Task"
activiti:expression="#{printer.printMessage(execution, myVar)}" />
将在名为printer
的对象上调用printMessage
方法。传递的第一个参数为DelegateExecution
,名为execution
,在表达式上下文中默认可用。传递的第二个参数,是当前执行中,名为myVar
变量的值。
可以使用activiti:expression属性指定需要计算的UEL值表达式。
1
2
3 <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)访问流程实例信息,如流程变量等。
1
2
3
4
5
6
7
8
9 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元素。
1
2
3
4
5
6
7 <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
:
1
2
3
4
5
6
7
8
9 public class ToUpperCaseFieldInjected implements JavaDelegate {
private Expression text;
public void execute(DelegateExecution execution) {
execution.setVariable("var", ((String)text.getValue(execution)).toUpperCase());
}
}
另外,对于较长文本(例如邮件内容),可以使用'activiti:string'子元素:
1
2
3
4
5
6
7
8
9
10
11 <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
中找到
1
2
3
4
5
6
7
8
9
10
11
12 <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>
1
2
3
4
5
6
7
8
9
10
11
12
13 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太过冗长,可以将表达式设置为属性,而不是子元素。
1
2 <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)中运行。当在不同的任务和/或流程定义中使用这个表达式时,表达式总会解析为相同的实例。这种情况下,使用字段注入不是线程安全的。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13 <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:
1
2
3
4
5
6 <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容器都会返回一个新实例。
在以下流程定义中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 <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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 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中,有一点区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 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版本中,(由于架构缺陷)不能在ExecutionListener或TaskListener中使用DelegateHelper。要保证监听器的线程安全,仍需使用表达式,或确保每次解析代理表达式时,都创建新实例。
-
在Activiti 6.x版本中,在ExecutionListener或TaskListener中可以使用DelegateHelper。例如在6.x版本中,下列代码可以使用DelegateHelper:
1
2
3
4
5
6
7 <extensionElements>
<activiti:executionListener
delegateExpression="${testExecutionListener}" event="start">
<activiti:field name="input" expression="${startValue}" />
<activiti:field name="resultVar" stringValue="processStartValue" />
</activiti:executionListener>
</extensionElements>
其中testExecutionListener解析为ExecutionListener接口的一个实现的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 @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'属性设置流程变量名,指定为已经存在的,或者新的流程变量。指定的已有值的流程变量,会被服务执行的结果值覆盖。当不指定结果变量名时,服务执行的结果值将被忽略。
1
2
3 <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。引擎会捕获这个异常,并将其转发至合适的错误处理器,例如异常边界事件,或者错误事件子程序。
1
2
3
4
5
6
7
8
9
10
11 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异常映射至业务异常(错误)。单一映射是最简单的格式:
1
2
3
4
5
6 <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
属性,映射特定异常的所有子异常。
1
2
3
4
5
6 <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异常:
1
2
3
4
5 <serviceTask id="servicetask1" name="Service Task" activiti:class="...">
<extensionElements>
<activiti:mapException errorCode="myErrorCode1"/>
</extensionElements>
</serviceTask>
映射会按照顺序检查,从上至下,使用第一个匹配的映射,除了默认映射。默认映射将只在所有映射都不能成功匹配时使用。只有第一个没有类的映射会当做默认映射处理。默认映射忽略includeChildExceptions
。
异常顺序流 Exception Sequence Flow
也可以选择在发生异常时,将流程执行路由至另一条路径。下面的例子展示了如何做。
1
2
3
4
5
6
7 <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" />
在这里,这个服务任务具有两条出口顺序流,分别称为exception
与no-exception
。这些顺序流id会在发生异常时,用于控制流程流向:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 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接口使用这些服务:
1
2
3
4
5
6
7
8 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接口获得的。
1
2
3
4
5
6
7
8
9
10
11 @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服务任务显示地一样。
XML表示 XML representation
要使用Web服务,需要导入其操作,以及复杂的类型。通过使用指向Web服务的WSDL的导入标签(import tag),可以自动完成这些:
1
2
3 <import importType="http://schemas.xmlsoap.org/wsdl/"
location="http://localhost:63081/counter?wsdl"
namespace="http://webservice.activiti.org/" />
上面的声明告知Activiti导入定义,但并不创建条目定义(item definition)与消息。假设我们需要调用一个名为’prettyPrint’的方法,我们需要为请求与回复消息,创建相应的消息与条目定义:
1
2
3
4
5 <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”操作:
1
2
3
4
5
6
7 <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服务操作。
1
2
3
4 <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例子中,根据之前声明的条目定义,定义输入与输出:
1
2
3
4
5
6
7
8
9
10 <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字段:
1
2
3
4
5
6
7
8
9
10
11
12 <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’字 段。
1
2
3
4
5
6
7
8 <dataInputAssociation>
<sourceRef>PrefixVariable</sourceRef>
<targetRef>prefix</targetRef>
</dataInputAssociation>
<dataInputAssociation>
<sourceRef>SuffixVariable</sourceRef>
<targetRef>suffix</targetRef>
</dataInputAssociation>
Web服务任务数据输出关联 Web Service Task data output associations
有两种指定数据输出关联的方式:
-
使用表达式
-
使用简化方法
要使用表达式指定数据输出关联,需要定义目标变量与源表达式。这种方法很直接,与数据输入关联类似:
1
2
3
4 <dataOutputAssociation>
<targetRef>dataOutputOfProcess</targetRef>
<transformation>${dataOutputOfServiceTask.prettyPrint}</transformation>
</dataOutputAssociation>
另一方面,也可以使用简化方法。'sourceRef’是条目定义的参数,而’targetRef’元素是一个Activiti变量名。这种方法很直接,与数据输入关联类似:
1
2
3
4 <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
业务规则任务,显示为带有表格图标的圆角矩形。
XML表示 XML representation
要执行一条或多条,与流程定义在同一个BAR文件中部署的业务规则,需要定义输入与结果变量。输入变量可以用流程变量的列表定义,使用逗号分隔。输 出变量只能有一个变量名,将执行业务规则数处对象存储至流程变量。请注意结果变量会包含对象的list。如果没有指定结果变量名,会使用默认的 org.activiti.engine.rules.OUTPUT。
下面的业务规则任务,执行与流程定义一起部署的所有业务规则:
1
2
3
4
5
6
7
8
9
10
11
12
13 <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文件中的一组规则。要做到这一点,需要指定规则名字的列表,用逗号分隔。
1
2 <businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
activiti:rules="rule1, rule2" />
这个例子中只会执行rule1与rule2。
也可以定义需要从执行中排除的规则列表。 execution.
1
2 <businessRuleTask id="businessRuleTask" activiti:ruleVariablesInput="${order}"
activiti:rules="rule1, rule2" exclude="true" />
这个例子中与流程定义一起部署在同一个BAR文件中的所有规则都会被执行,除了rule1与rule2.
前面提到过,还可以自行处理BusinessRuleTask的实现:
1 <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)。默认为 |
mailServerPort |
是,如果不使用默认端口 |
邮件服务器的SMTP端口。默认值为25 |
mailServerDefaultFrom |
否 |
若用户没有提供地址,默认使用的邮件发件人地址。默认为activiti@activiti.org |
mailServerUsername |
若服务器需要 |
部分邮件服务器发信时需要进行认证。默认为空。 |
mailServerPassword |
若服务器需要 |
部分邮件服务器发信时需要进行认证。默认为空。 |
mailServerUseSSL |
若服务器需要 |
部分邮件服务器要求ssl通信。默认设置为false。 |
mailServerUseTLS |
若服务器需要 |
部分邮件服务器要求TLS通信(例如gmail)。默认设置为false。 |
定义邮件任务 Defining an Email Task
邮件任务实现为特殊的服务任务,通过将服务任务的type定义为'mail'设置。
1 <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代码片段展示了使用邮件任务的示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 <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>
产生如下结果:
8.5.7. Mule任务 Mule Task
Mule任务可以向Mule发送消息,增强Activiti的集成特性。请注意Mule任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。因此,在Activiti中,Mule任务实现为一种特殊的服务任务。
定义Mule任务 Defining an Mule Task
Mule任务实现为特殊的服务任务,通过将服务任务的type定义为'mule'设置。
1 <serviceTask id="sendMule" activiti:type="mule">
Mule任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
endpointUrl |
是 |
希望调用的Mule终端(endpoint)。 |
language |
是 |
计算payloadExpression字段所用的语言。 |
payloadExpression |
是 |
消息的载荷表达式 |
resultVariable |
否 |
存储调用结果的变量名。 |
使用示例 Example usage
下面的XML代码片段展示了使用Mule任务的示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <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'设置。
1 <serviceTask id="sendCamel" activiti:type="camel">
流程定义本身只需要在服务任务上定义Camel类型。集成逻辑都通过Camel容器代理。默认情况下Activiti引擎在Spring容器中查找 camelContext bean。camelContext bean定义了由Camel容器装载的Camel路由。在下面的例子中,路由通过给定的Java包装载,但也可以自行在Spring配置中直接定义路由。
1
2
3
4
5 <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任务定义中像这样覆盖:
1
2
3
4
5 <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上下文。下面的代码用做这个目的:
1
2
3
4
5 <camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<packageScan>
<package>org.activiti.camel.examples.simpleCamelCall</package>
</packageScan>
</camelContext>
1
2
3
4
5
6
7 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。下面需要像这样定义工作流:
1
2
3
4
5
6
7
8
9 <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服务。这是我们的调用代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 @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传回的结果。流程像是这样:
1
2
3
4
5
6
7
8
9 <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行为:
1
2
3
4
5 <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路由总结为第二个例子:
1
2
3
4 @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中重载已有行为的例子:
1 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,启用这个功能。
1 <serviceTask id="serviceAsyncPing" activiti:type="camel" activiti:async="true"/>
设置这个特性后,Camel路由会由Activiti作业执行器异步启动。如果定义了Camel路由队列,Activiti流程会继续执行 Camel服务任务之后的活动。Camel路由会与流程执行完全异步地执行。如果需要在流程定义的某处等待Camel服务任务的响应,可以使用接收任务 (receive task)。
1 <receiveTask id="receiveAsyncPing" name="Wait State" />
流程实例会等待,直到接收到信号,例如来自Camel。在Camel中,可以通过向合适的Activiti终端发送消息,来为流程实例发送信号。
1 from("activiti:asyncPingProcess:serviceAsyncPing").to("activiti:asyncPingProcess:receiveAsyncPing");
(译者注:原文如此。可能为缺失了的 to 终端的定义:)
-
“activiti”字符串常量
-
流程名
-
接收任务名
从Camel路由实例化工作流 Instantiate workflow from Camel route
上面的所有例子,都是先启动Activiti工作流,然后在工作流中启动Camel路由。也可以反过来。可以在已经启动的Camel路由中实例化工作流。与为接收任务发送消息很类似,除了最后一部分。这是一个简单的路由:
1 from("direct:start").to("activiti:camelProcess");
可以看到url有两部分,第一部分是“activiti”字符串常量,第二个名字是流程的名字。很明显流程需要已经部署,并且可以通过引擎配置启动。
也可以在Camel头中,将流程起动人设置为某个已认证用户id。要这么做,首先需要在流程定义中指定启动人变量:
1 <startEvent id="start" activiti:initiator="initiator" />
然后在Camel头中的CamelProcessInitiatorHeader指定用户id。Camel路由会如下定义:
1
2
3 from("direct:startWithInitiatorHeader")
.setHeader("CamelProcessInitiatorHeader", constant("kermit"))
.to("activiti:InitiatorCamelCallProcess?processInitiatorHeaderName=CamelProcessInitiatorHeader");
8.5.9. 手动任务 Manual Task
描述 Description
手动任务定义了在BPMN引擎外部的任务。用于建模引擎不需要了解的某项工作,或者其他系统或用户界面。对于引擎来说,手动任务将按直接穿过活动处理,在流程执行到达时,自动继续流程。
图示 Graphical Notation
手动任务,表现为左上角带有“手型”图标的圆角矩形。
XML表示 XML representation
1 <manualTask id="myManualTask" name="Call client for more information" />
8.5.10. Java接收任务 Java Receive Task
描述 Description
接收任务,是等待特定消息到达的简单任务。目前,我们只为这个任务实现了Java语义。当流程执行到达接收任务时,流程状态将提交至持久化存储。这意味着流程将保持等待状态,直到引擎接收到特定的消息,并将触发流程通过接收任务。
图示 Graphical notation
接收任务,表现为右上角带有消息图标的任务(圆角矩形)。消息图标是白色的(黑色消息图标代表发送的含义)。
XML表示 XML representation
1 <receiveTask id="waitState" name="wait" />
要使流程实例从当前的等待状态,如接收任务中继续,需要使用到达接收任务的执行id,调用runtimeService.signal(executionId)。下面的代码片段展示了如何操作:
1
2
3
4
5
6
7
8 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'设置。
1 <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。
1
2
3
4
5
6
7
8
9
10 <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.ExecutionListener
,org.activiti.engine.impl.pvm.delegate.TaskListener
与org.activiti.engine.impl.pvm.el.Expression
已被废弃。从现在起,应该使用org.activiti.engine.delegate.ExecutionListener
,org.activiti.engine.delegate.TaskListener
与org.activiti.engine.delegate.Expression
。在新的公开可用的API中,对ExecutionListenerExecution.getEventSource()
的访问已被移除。除了编译器的废弃警告,现有代码可以正常运行。但是请考虑切换至新的公开API接口(包名中不带有.impl.)。
执行监听器可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式。可以被捕获的事件有:
-
流程实例的start(启动)和end(结束)。
-
take(进行)转移(transition)。
-
活动的start和end。
-
网关的start和end。
-
中间事件的start和end。
-
启动事件的end,和结束事件的start。
下面的流程定义包含了三个执行监听器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 <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)
方法。
1
2
3
4
5
6
7 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
,并将在事件触发时计算/调用。
1 <activiti:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />
与其他表达式一样,可以使用与解析execution变量。因为execution实现对象有一个暴露事件名的参数,因此可以使用execution.eventName
向你的方法传递事件名。
执行监听器也支持使用delegateExpression
,与服务任务类似。
1 <activiti:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />
在Activiti 5.12中,我们也引入了新的执行监听器类型,org.activiti.engine.impl.bpmn.listener.ScriptExecutionListener。这个脚本执行监听器,可以为一个执行监听器事件执行一段脚本逻辑。
1
2
3
4
5
6
7
8
9
10
11
12 <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
属性配置的执行监听器时,可以使用字段注入。与服务任务字段注入使用完全相同的机制,可以在那里看到字段注入提供的各种可能用法。
下面的代码片段展示了简单的示例流程,有一个使用了字段注入的执行监听器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <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>
1
2
3
4
5
6
7
8
9
10 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
'流程变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 @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命名空间下。
1
2
3
4
5 <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
接口。
1
2
3
4
5
6
7 public class MyTaskCreateListener implements TaskListener {
public void notify(DelegateTask delegateTask) {
// Custom logic goes here
}
}
也可以使用字段注入,为代理类传递流程变量或执行。请注意代理类的实例在流程部署时创建(与Activiti中其它的代理类一样),这意味着该实例会在所有流程实例执行中共享。
-
expression:(不能与class属性一起使用):指定在事件发生时要执行的表达式。可以为被调用的对象传递
DelegateTask
对象与事件名(使用task.eventName
)作为参数。
1 <activiti:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
-
delegateExpression:可以指定一个能够解析为
TaskListener
接口实现类对象的表达式。与服务任务类似。
1 <activiti:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
-
在Activiti 5.12中,我们也引入了新的执行监听器类型,org.activiti.engine.impl.bpmn.listener.ScriptTaskListener。这个脚本任务监听器,可以为一个任务监听器事件执行一段脚本逻辑。
1
2
3
4
5
6
7
8
9
10
11
12 <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
如果一个活动是多实例,将通过在该活动底部的三条短线表示。三条竖线代表实例会并行执行,而三条横线代表顺序执行。
XML表示 Xml representation
要将活动变成多实例,该活动的XML元素必须有multiInstanceLoopCharacteristics
子元素
1
2
3 <multiInstanceLoopCharacteristics isSequential="false|true">
...
</multiInstanceLoopCharacteristics>
isSequential属性代表了活动的实例为顺序还是并行执行。
实例的数量在进入活动时,计算一次。有不同方法可以配置数量。一个方法是通过loopCardinality子元素,直接指定数字。
1
2
3 <multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>5</loopCardinality>
</multiInstanceLoopCharacteristics>
也可以使用解析为正整数的表达式:
1
2
3 <multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>
另一个定义实例数量的方法,是使用loopDataInputRef
子元素,指定一个集合流程变量的名字。对集合中的每一个条目,都会创建一个实例。可以使用inputDataItem
子元素,将集合中的该条目设置给实例。在下面的XML示例中展示:
1
2
3
4
5
6 <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
的流程变量,含有集合中的一个值,并在这个例子中被用于指派用户任务。
loopDataInputRef
与inputDataItem
的缺点是 1)名字很难记 2)由于BPMN 2.0概要的限制,不能使用表达式。Activiti通过在multiInstanceCharacteristics
上提供collection与elementVariable属性解决了这些问题:
1
2
3
4
5 <userTask id="miTasks" name="My Task" activiti:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
activiti:collection="${myService.resolveUsersForTask()}" activiti:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时计算。当表达式计算为true时,销毁所有剩余的实例,并且结束多实例活动,继续流程。这个表达式必须通过completionCondition子元素定义。
1
2
3
4
5
6 <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
多实例是普通活动,因此可以在其边界定义边界事件。对于中断边界事件,当捕获事件时,活动中的所有实例都会被销毁。以下面的多实例子流程为例:
当定时器触发时,子流程的所有实例都会被销毁,无论有多少实例,或者哪个内部活动还未完成。
多实例与执行监听器 Multi instance and execution listeners
(Activiti 5.18及以上版本可用)
有一个关于执行监听器与多实例一起使用的警告。以下面的BPMN 2.0 XML代码片段为例,其定义在multiInstanceLoopCharacteristics XML元素的相同级别:
1
2
3
4 <extensionElements>
<activiti:executionListener event="start" class="org.activiti.MyStartListener"/>
<activiti:executionListener event="end" class="org.activiti.MyEndListener"/>
</extensionElements>
对于普通的BPMN活动,会在活动开始于结束时调用一次监听器。
然而,当该活动为多实例时,行为有区别:
-
当进入多实例活动时,在任何内部活动执行前,抛出启动事件。这时loopCounter变量还未设置(为null)。
-
每个实际执行的活动,抛出一个启动事件。这时loopCounter变量已经设置。
对结束事件类似:
-
当离开实际活动时,抛出一个结束事件。这时loopCounter变量已经设置。
-
当多实例活动整体完成时,抛出一个结束事件。这时loopCounter变量未设置。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13 <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执行监听器,loopCounter与assignee变量均未设置(也就是说为null)。
-
每一个活动实例抛出一个启动事件。调用三次start执行监听器,loopCounter与assignee变量均已设置(也就是说不为null)。
-
因此启动执行监听器总共被调用四次。
请注意当multiInstanceLoopCharacteristics不是定义在子元素上,也是如此。例如上面的简单用户任务的例子,也合理适用这一点。
8.5.15. 补偿处理器 Compensation Handlers
描述 Description
如果一个活动要用于补偿另一个活动的影响,可以声明为补偿处理器。补偿处理器不在普通流程中,只在抛出补偿事件时才会执行。
补偿处理器不得有入口或出口顺序流。
补偿处理器必须通过单向连接,关联一个补偿边界事件。
图示 Graphical notation
如果一个活动是补偿处理器,则会在下部中间显示补偿事件图标。 下面摘录的流程图展示了一个带有补偿边界事件的服务任务,并关联至一个补偿处理器。请注意补偿处理器图标显示在"cancel hotel reservation(取消酒店预订)"服务任务的下部中间。
XML表示 XML representation
要将一个活动声明为补偿处理器,需要将isForCompensation属性设置为true:
1
2 <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
子流程表示为标准活动,即圆角矩形。若折叠了子流程,则只显示其名字与一个加号,提供了流程的高层概览:
若展开了子流程,则子流程的所有步骤都在子流程边界内显示:
使用子流程的一个主要原因,是为特定事件定义范围。下面的流程模型展示了这种用法:investigate software/investigate hardware(调查硬件/调查软件)两个任务需要并行执行,且需要在给定时限内,在Level 2 support(二级支持)响应前完成。在这里,定时器的范围(即需要按时完成的活动)通过子流程限制。
XML表示 XML representation
子流程通过subprocess元素定义。子流程中的所有活动、网关、事件等,都需要附在这个元素内。
1
2
3
4
5
6
7
8
9 <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只支持错误启动事件与消息启动事件触发事件子流程。
XML表示 XML representation
事件子流程的XML表示格式,与嵌入式子流程相同。但需要将triggeredByEvent
属性设置为true
:
1
2
3 <subProcess id="eventSubProcess" triggeredByEvent="true">
...
</subProcess>
示例 Example
下面是使用错误启动事件触发事件子流程的例子。该事件子流程位与“流程级别”,即流程实例的范围:
事件子流程在XML是这样的:
1
2
3
4
5
6
7 <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>
前面已经指出,事件子流程也可以添加到嵌入式子流程内。若添加到嵌入式子流程内,将可替代边界事件的功能。考虑下面两个流程图,嵌入式子流程都抛出错误事件,该错误事件都被捕获,并由用户任务处理。
对比:
两种情况下都执行相同的任务。然而,两种模型选择有如下不同:
-
嵌入式(事件)子流程使用其宿主范围的执行来执行。这意味着嵌入式(事件)子流程可以访问其范围的局部变量。当使用边界事件时,创建用于执行嵌入式子流程的执行,将被边界事件的出口顺序流删除。这意味着嵌入式子流程创建的变量将不再可用。
-
使用事件子流程时,事件完全由其所在的子流程处理。当使用边界事件时,事件由其父流程处理。
这两个区别可以帮助你判断,使用边界事件还是嵌入式(事件)子流程,哪个更适合解决特定的流程建模/实现问题。
8.6.3. 事务子流程 Transaction subprocess
描述 Description
事务子流程是一种嵌入式子流程,可用于将多个活动组织在一个事务里。事务是工作的逻辑单元,可以组织一组独立活动,使得它们可以一起成功或失败。
事务的可能结果:事务有三种不同的结果:
-
若未被取消,或被意外终止,则事务成功。若事务子流程成功,将使用出口顺序流离开。若流程后面抛出了补偿事件,成功的事务可以被补偿。请注意:与“普通”嵌入式子流程一样,可以使用补偿抛出中间事件,在事务成功完成后补偿。
-
若执行到达取消结束事件时,事务被取消。在这种情况下,所有执行都将被终止并移除。只会保留一个执行,设置为取消边界事件,并将触发补偿。在补偿完成后,事务子流程通过取消边界事件的出口顺序流离开。
-
若由于抛出了错误结束事件,且未被事务子流程所在的范围捕获,则事务会被意外终止(错误被事件子流程的边界捕获也一样)。在这种情况下,不会进行补偿。
下面的流程图展示了三种不同的结果:
与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事务时,与实施“普通”流程与子流程,需要遵守相同的规则。因此要有效地保证一致性,需要将乐观锁、事务执行模型纳入考虑 范围,以实现流程。
XML表示 XML representation
事务子流程,在XML中通过transaction
标签表示:
1
2
3 <transaction id="myTransaction" >
...
</transaction>
示例 Example
下面是一个事务子流程的例子:
8.6.4. 调用活动(子流程) Call activity (subprocess)
描述 Description
BPMN 2.0区分一般的子流程,通常也称作嵌入式子流程,与调用活动,尽管它们看起来很像。从概念上说,两者都在流程执行到达该活动时,调用一个子流程。
区别在于,调用活动引用一个流程定义外部的流程,而subprocess嵌入在原有流程定义内。调用活动的主要使用场景,是它有一个可重复使用的流程定义,可以在多个其他流程定义中调用。
当流程执行到达call activity时,会创建一个新的执行,作为到达调用活动的执行的子执行。这个子执行之后用于执行子流程,潜在地创建了类似普通流程的并行子执行。父执行将等待子流程完成,之后沿原流程继续执行。
XML表现 XML representation
调用活动是一个普通活动,需要有通过其key引用流程定义的calledElement。在实际使用中,这通常意味着在calledElement中使用流程的id。
1 <callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
请注意子流程的流程定义在运行时解析。这意味着如果需要的话,子流程可以与调用流程分别部署。
传递变量 Passing variables
可以向子流程传递流程变量,反之亦然。数据将在子流程启动时复制到子流程,并在其结束时复制回主流程。
1
2
3
4
5
6 <callActivity id="callSubProcess" calledElement="checkCreditProcess" >
<extensionElements>
<activiti:in source="someVariableInMainProcess" target="nameOfVariableInSubProcess" />
<activiti:out source="someVariableInSubProcess" target="nameOfVariableInMainProcess" />
</extensionElements>
</callActivity>
使用Activiti扩展,作为BPMN标准元素dataInputAssociation与dataOutputAssociation的扩展。它们需要按照BPMN 2.0标准的方式声明流程变量。
也可以在这里使用表达式:
1
2
3
4
5
6 <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(检查信用额度步骤)建模为调用活动。
流程像是下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13 <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" />
子流程像是下面这样:
与子流程的流程定义相比没什么特别。也可以不通过其他流程调用而使用。
8.7. 事务与并发 Transactions and Concurrency
8.7.1. 异步延续 Asynchronous Continuations
Activiti以事务方式执行流程,并可按照你的需求配置。让我们从Activiti一般如何为事务划分范围开始介绍。如果Activiti被触 发(也就是说,启动流程,完成任务,为执行发送信号),Activiti将沿流程继续,直到到达每个执行路径的等待状态。更具体地说,它以深度优先方式搜 索流程图,并在每个执行分支都到达等待状态时返回。等待状态是“之后”再执行的任务,意味着Activiti将当前执行持久化,并等待再次触发。触发可以 来自外部来源,例如用户任务或消息接受任务,也可以来自Activiti自身,例如定时器事件。以下面的图片说明:
这是一个BPMN流程的片段,有一个用户任务,一个服务任务,与一个定时器事件。完成用户任务与验证地址(validate address)在同一个工作单元内,因此需要原子性地(atomically)成功或失败。这意味着如果服务任务抛出了异常,我们会想要回滚当前事务, 以便执行返回到用户任务,而用户任务仍然存在于数据库中。这也是Activit的默认行为。在(1)中,应用或客户端线程完成任务。在相同的线程 中,Activiti执行服务并继续,直到到达等待状态,在这个例子中,是定时器事件(2)。然后将控制权返回至调用者(3),同时提交事务(如果事务由 Activiti开启)。
在有的情况下,这不是我们想要的。有时我们需要在流程中,自定义地控制事务边界,以便为工作的逻辑单元划分范围。这就需要使用异步延续。考虑下面的流程(片段):
这次我们完成用户任务,生成发票,并将发票发送给客户。这次发票的生成不再是同一个工作单元的一部分,因此我们不希望当发票生成失败时,回滚用户任 务。因此我们希望Activiti做的,是完成用户任务(1),提交事务,并将控制权返回给调用程序。然后我们希望在后台线程中,异步地生成发票。这个后 台线程就是Activiti作业执行器(事实上是一个线程池),它周期性地将作业保存至数据库。因此在幕后,当到达"generate invoice(生成发票)"任务时,会为Activiti创建“消息”作业,以继续流程,并将其持久化到数据库中。这个作业之后会被作业执行器选中并执 行。我们也会为本地的作业执行器进行提示,告知其有新作业到来,以提升性能。
要使用这个特性,可以使用activiti:async="true"扩展。因此,服务任务会像是这样:
1 <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
元素配置。这有一个简单的例子:
1
2
3
4
5 <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?
考虑下面的流程定义:
我们有一个并行网关,之后是三个服务任务,都使用异步延续执行。其结果是,数据库中添加了三个作业。当作业储存在数据库后,就可以使用 JobExecutor处理。JobExecutor获取作业,并将其代理至工作线程的线程池,由它们实际执行作业。这意味着通过使用异步延续,可以将工 作分发至线程池(在集群场景下,甚至是在集群中跨越多个线程池)。通常这都是好事。然而,也有固有问题:一致性。考虑服务任务后的并行合并。当服务任务的 执行完成时,到达并行合并,并需要决定等待其他执行,还是需要继续向前。这意味着,对于每一个到达并行合并的分支,都需要选择继续执行,还是需要等待其他 分支上的一个或多个其他执行。
为什么这是问题呢?这是因为服务任务配置为使用一步延续,有可能所有相应的作业都同时被作业执行器处理,并代理至不同的工作线程。结果是服务执行的 事务,与到达并行合并的3个独立执行所在的事务,会发生重叠。如果这样,每一个独立事务都“看”不到,其他事物并发地到达了同样的并行合并,并因此判断需 要等待其他事务。然而,如果每个事务都判断需要等待其他事务,在并行合并后不会有继续流程的事务,而流程实例也就会永远保持这个状态。
Activiti如何解决这个问题呢?Activiti使用乐观锁,基于数据进行判断,而数据可能不是当前值(因为其他事务可能在我们提交前修改了 这个数据,我们确保会在每个事务中都增加同一个数据库记录行的版本号)。这样,无论哪个事务第一个提交,都将成功,而其他的会抛出乐观锁异常并失败。这解 决了上面流程中讨论的问题:如果多个执行并发到达并行合并,它们都判断需要等待,增加其父执行(流程实例)的版本号,并尝试提交。无论哪个执行第一个提 交,都可以成功提交,而其他的将会抛出乐观锁异常并失败。因为这些执行由作业触发,Activiti会在等待给定时间后,重试执行相同的作业,期望这一次 通过这个同步的网关。
这是好的解决方案么?我们已经看到,乐观锁使Activiti能够避免不一致。它确保了我们不会“在合并网关卡住”,意味着:要么所有的执行都通过 网关,要么数据库中的作业能确保可以重试通过它。然而,尽管这是一个持久化与一致性角度的完美解决方案,仍然不一定总是更高层次的理想行为:
-
Activiti只会为同一个作业,重试一个固定的最大次数(默认配置为'3’次)。在这之后,作业仍然保存在数据库中,但不会再重试。这意味着需要手动操作来触发作业。
-
如果一个作业有非事务性的副作用,将不会由于事务失败而回滚。例如,如果"book concert tickets(预定音乐会门票)"服务与Activiti不在同一个事务中,则如果重试执行作业,将预定多张票。
什么是排他作业? What are exclusive jobs?
排他作业不能与同一个流程实例中的其他排他作业同时执行。考虑上面展示的流程:如果我们将服务任务都声明为排他的,则JobExecutor将确保 相关的作业都不会并发执行。相反,它将确保不论何时从特定流程实例中获取了排他作业,都将从同一个流程实例中获取所有其他的排他作业,并将它们代理至同一 个工作线程。这保证了作业的顺序执行。
如何启用这个特性?从Activiti 5.9起,排他作业成为默认配置。所有异步延续与定时器事件,都因此默认成为排他的。另外,如果希望作业成为非排他的,可以使用activiti:exclusive="false"
配置。例如,下面的服务任务是异步,但非排他的。
1 <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>标签,将用户或组指派为流程的潜在启动者。这里有一个例子:
1
2
3
4
5
6
7
8
9
10
11 <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>的属性。这里有一个例子:
1
2
3 <process id="potentialStarter" activiti:candidateStarterUsers="user1, user2"
activiti:candidateStarterGroups="group1">
...
这些属性可以同时使用。
在流程启动认证定义后,开发者可以使用下列方法获取该认证定义。这段代码获取可以由给定用户启动的流程定义列表:
1 processDefinitions = repositoryService.createProcessDefinitionQuery().startableByUser("userxxx").list();
也可以获取给定流程定义中,所有定义为潜在启动者的身份联系
1 identityLinks = repositoryService.getIdentityLinksForProcessDefinition("processDefinitionId");
下面的例子展示了如何获取能够启动给定流程的用户列表:
1 List<User> authorizedUsers = identityService().createUserQuery().potentialStarter("processDefinitionId").list();
用完全相同的方法,可以获取配置为给定流程定义的潜在启动者的组列表:
1 List<Group> authorizedGroups = identityService().createGroupQuery().potentialStarter("processDefinitionId").list();
8.9. 数据对象 Data objects
BPMN提供了将数据对象定义为流程或子流程元素的一部分的可能性。根据BPMN规范,可以包含复杂的XML结构,并可以从XSD定义中引入。作为Activiti支持的第一批数据对象,支持下列XSD类型:
1
2
3
4
5
6 <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代码片段提供了示例:
1
2
3
4
5
6
7 <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的返回值。例如
1 StartFormData FormService.getStartFormData(String processDefinitionId)
或者
1 TaskFormdata FormService.getTaskFormData(String taskId)
默认情况下,内建表单引擎能够“看到”参数与流程变量。因此如果任务表单参数1对1匹配流程变量,则不需要专门声明。例如,对于下列声明:
1 <startEvent id="start" />
当执行到达startEvent时,所有流程变量都可用。然而
1 formService.getStartFormData(String processDefinitionId).getFormProperties()
将为空,因为并未指定映射。
在上面的例子中,所有提交的参数将被存储为流程变量。这意味着简单地在表单中添加输入框,就可以存储新变量。
参数从流程变量衍生出来,但不是必须存储为流程变量。例如,流程变量可以是类地址的JPA实体。而用户界面技术使用的StreetName
表单参数,可以通过#{address.street}
表达式连接。
类似的,表单中用户需要提交的参数可以存储为流程变量,也可以作为某个流程变量的嵌套参数,使用UEL值表达式,如#{address.street}
。
提交的参数的默认行为,是存储为流程变量,除非使用formProperty
声明指定。
流程也可以在表单参数与流程变量之间进行转换。
例如:
1
2
3
4
5
6
7
8 <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()
方法获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 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();
}
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 <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代码片段
1
2
3
4
5
6
7 <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时,将会渲染为流程启动表单
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)、String
、BigInteger
、BigDecimal
、java.util.Date
与java.sql.Date
。
10.2. 配置 Configuration
要使用JPA实体,引擎必须引用EntityManagerFactory
。可以通过配置引用,或者提供持久化单元名(Persistence Unit Name)来实现。用作变量的JPA实体将将被自动检测,并会按情况处理。
下面的示例配置使用jpaPersistenceUnitName:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 <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
)中找到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 <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>
也可以在编程构建引擎时,使用相同的配置,例如:
1
2
3
4 ProcessEngine processEngine = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResourceDefault()
.setJpaPersistenceUnitName("activiti-pu")
.buildProcessEngine();
配置参数:
-
jpaPersistenceUnitName
:要使用的持久化单元的名字。(要确保该持久化单元在classpath中可用。根据规范,默认位置为/META-INF/persistence.xml
)。jpaEntityManagerFactory
与jpaPersistenceUnitName
二选一。 -
jpaEntityManagerFactory
:对实现了javax.persistence.EntityManagerFactory
的bean的引用,将用于载入实体,并刷入更新。jpaEntityManagerFactory与jpaPersistenceUnitName二选一。 -
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
值参数,用于持久化。在运行测试前,先创建一个实体并保存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 @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
载入。
1
2
3
4 Map<String, Object> variables = new HashMap<String, Object>();
variables.put("entityToUpdate", entityToUpdate);
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("UpdateJPAValuesProcess", variables);
我们流程定义的第一个节点,是一个服务任务
,将调用entityToUpdate
上的setValue
方法。它将解析为我们之前启动流程实例时设置的JPA变量,并使用当前引擎的上下文关联的++EntityManager+载入。
1
2 <serviceTask id='theTask' name='updateJPAEntityTask'
activiti:expression="${entityToUpdate.setValue('updatedValue')}" />
当服务任务完成时,流程实例在流程定义中定义的用户任务处等待,让我们可以查看流程实例。在这时,EntityManager
已经刷入,对实体的修改也已经存入数据库。当我们使用entityToUpdate
变量的值时,将重新载入,我们会得到value
参数设置为updatedValue
的实体。
1
2
3
4
5 // 流程'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实体作为变量值的流程实例
与执行
。请注意对于ProcessInstanceQuery
与ExecutionQuery
的JPA实体查询,只支持variableValueEquals(name, entity)
。而variableValueNotEquals
、variableValueGreaterThan
、variableValueGreaterThanOrEqual
、variableValueLessThan
与variableValueLessThanOrEqual
方法都不支持,并会在值传递为JPA实体时,抛出ActivitiException
。
1
2 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 发送拒信),以便客户可以收到拒信得到通知。
-
请注意这个流程不包含任何表单,因为它只用于单元测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 <?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
。
1
2
3
4
5 historyService.createHistoricProcessInstanceQuery()
.finished()
.processDefinitionId("XXX")
.orderByProcessInstanceDuration().desc()
.listPage(0, 10);
11.1.2. 历史变量实例查询 HistoricVariableInstanceQuery
在已完成的,id为’XXX’的流程实例中,取得所有HistoricVariableInstances
,以变量名排序。
1
2
3
4 historyService.createHistoricVariableInstanceQuery()
.processInstanceId("XXX")
.orderByVariableName.desc()
.list();
11.1.3. 历史活动实例查询 HistoricActivityInstanceQuery
取得最新的,服务任务类型的,已完成的,流程定义的id为XXX的,HistoricActivityInstance
。
1
2
3
4
5
6 historyService.createHistoricActivityInstanceQuery()
.activityType("serviceTask")
.processDefinitionId("XXX")
.finished()
.orderByHistoricActivityInstanceEndTime().desc()
.listPage(0, 1);
11.1.4. 历史详情查询 HistoricDetailQuery
下一个例子,取得id为123的流程中,所有变量的更新记录。这个查询只会返回HistoricVariableUpdate
。请注意有可能某个变量名有多个HistoricVariableUpdate
实体,代表流程中的每一次变量更新。可以使用orderByTime
(变量更新的时间)或orderByVariableRevision
(运行时变量更新时的版本号),按其发生顺序排序。
1
2
3
4
5 historyService.createHistoricDetailQuery()
.variableUpdates()
.processInstanceId("123")
.orderByVariableName().asc()
.list()
这个例子,取得流程id为"123"的,任何任务中或启动时提交的,所有表单参数。这个查询只返回HistoricFormProperties
。
1
2
3
4
5 historyService.createHistoricDetailQuery()
.formProperties()
.processInstanceId("123")
.orderByVariableName().asc()
.list()
最后一个例子,取得id为"123"的任务进行的所有变量更新操作。将返回该任务设置的所有变量(任务局部变量)的HistoricVariableUpdates
,而不会返回流程实例中设置的。
1
2
3
4
5 historyService.createHistoricDetailQuery()
.variableUpdates()
.taskId("123")
.orderByVariableName().asc()
.list()
可以在TaskListener
中使用TaskService
或DelegateTask
设置任务局部变量:
1 taskService.setVariableLocal("123", "myVariable", "Variable value");
1
2
3 public void notify(DelegateTask delegateTask) {
delegateTask.setVariableLocal("myVariable", "Variable value");
}
11.1.5. 历史任务示例查询 HistoricTaskInstanceQuery
取得所有任务中,前10个花费最多时间完成(最长持续时间)的,已完成的HistoricTaskInstance
。
1
2
3
4 historyService.createHistoricTaskInstanceQuery()
.finished()
.orderByHistoricTaskInstanceDuration().desc()
.listPage(0, 10);
取得删除原因包含"invalid",最后一次指派给’kermit’用户的HistoricTaskInstance
。
1
2
3
4
5 historyService.createHistoricTaskInstanceQuery()
.finished()
.taskDeleteReasonLike("%invalid%")
.taskAssignee("kermit")
.listPage(0, 10);
11.2. 历史配置 History configuration
可以使用org.activiti.engine.impl.history.HistoryLevel枚举(或在5.11版本前,ProcessEngineConfiguration
中定义的HISTORY常量),以编程方式配置历史级别:
1
2
3
4 ProcessEngine processEngine = ProcessEngineConfiguration
.createProcessEngineConfigurationFromResourceDefault()
.setHistory(HistoryLevel.AUDIT.getKey())
.buildProcessEngine();
也可以在activiti.cfg.xml或Spring上下文中配置级别:
1
2
3
4 <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,像这样读取:
1
2
3
4
5 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/
确保"Contact all updates sites.."复选框已选中,因为这样Eclipse就可以下载需要的所有插件。
12.2. Activiti Designer编辑器功能 Activiti Designer editor features
-
创建Activiti项目与流程图(diagram)。
-
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文件。
-
可以将BPMN 2.0 XML文件导入Activiti Designer,会自动创建流程图。只需要将BPMN 2.0 XML文件复制到项目中,并使用Activiti Diagram Editor视图打开它。Activiti Designer使用文件中的BPMN DI信息来创建流程图。如果BPMN 2.0 XML文件中没有BPMN DI信息,则不会创建流程图。
-
要进行部署,可以使用Activiti Designer创建BAR文件,或JAR文件。在包浏览器中的Activiti项目上点击右键,在弹出菜单的下方选择Create deployment artifacts(创建部署包)选项。要了解关于Designer部署功能的更多信息,请查看部署章节。
-
生成单元测试(在包浏览器中的BPMN 2.0 XML文件上点击右键,选择generate unit test 生成单元测试)。将创建一个单元测试及运行在嵌入式H2数据库上的Activiti配置。这样就可以运行单元测试,来测试你的流程定义。
-
Activiti项目可以生成为Maven项目。要配置依赖,需要运行mvn eclipse:eclipse。请注意在流程设计时,不需要Maven依赖。只在运行单元测试时才需要依赖。
12.3. Activiti Designer BPMN功能 Activiti Designer BPMN features
-
支持空启动事件,错误启动事件,定时器启动事件,空结束事件,错误结束事件,顺序流,并行网关,排他网关,包容网关,事件网关,嵌入式子流程,事件 子流程,调用活动,泳池,泳道,脚本任务,用户任务,服务任务,邮件任务,手动任务,业务规则任务,接收任务,定时器边界事件,错误边界事件,信号边界事 件,定时器捕获事件,信号捕获事件,信号抛出事件,空抛出事件,与四个Alfresco特有元素(用户,脚本,邮件任务与启动事件)。
-
可以在元素上悬停并选择新的任务类型,快速改变任务的类型。
-
可以在元素上悬停并选择新的元素类型,快速添加新的元素。
-
Java服务任务支持Java类,表达式或代理表达式配置。另外也可以配置字段扩展。
-
支持泳池与泳道。但因为Activiti将不同的泳池认作不同的流程定义,因此最好只使用一个泳池。如果使用多个泳池,要小心不要在泳池间画顺序流,否则会在Activiti引擎中部署流程时发生错误。可以在一个泳池中添加任意多的泳道。
-
可以通过填写name参数,为顺序流添加标签。可以决定放置标签的位置,位置将保存为BPMN 2.0 XML DI信息的一部分。
-
支持事件子流程。
-
支持展开嵌入式子流程。也可以在一个嵌入式子流程中加入另一个嵌入式子流程。
-
支持在任务与嵌入式子流程上的定时器边界事件。然而,在Activiti Designer中,在用户任务或嵌入式子流程上使用定时器边界事件最合理。
-
支持额外的Activiti扩展,例如邮件任务,用户任务的候选人配置,或脚本任务配置。
-
支持Activiti执行与任务监听器。也可以为执行监听器添加字段扩展。
-
支持在顺序流上添加条件。
12.4. Activiti Designer部署功能 Activiti Designer deployment features
在Activiti引擎上部署流程定义与任务表单并不困难。需要有一个包含有流程定义BPMN 2.0 XML文件的BAR文件,与可选的用于在Activiti Explorer中查看的任务表单和流程图片。在Activiti Designer中,创建BAR文件十分简单。在完成流程实现后,只要在包浏览器中的Activiti项目上点击右键,在弹出菜单下方选择Create deployment artifacts(创建部署包)选项。
然后就会创建一个部署目录,包含BAR文件,与可能的JAR文件。其中JAR文件包含Activiti项目中的Java类。
这样就可以在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/java
与src/main/resources
目录,这是Maven约定的Java源文件与资源文件目录。打开pom.xml
文件并添加下列行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <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文件,为项目定义了一个groupId
,artifactId
与version
。我们会创建一个定制项,包含一个money业务的自定义节点。
在pom.xml
文件中添加这些依赖,将集成库添加至项目依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <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
中添加下列代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 <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/java
与src/main/resources
源码目录。
当然也可以使用m2eclipse插件,并简单地在项目的上下文菜单(右键点击)中启用Maven依赖管理。然后在项目的上下文菜单中选择 |
这就完成了配置。现在可以开始为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
的选项。
-
选择
User Libraries
选项,将在右侧显示树形界面,可以添加库。应该可以看到一个默认组,可以用于添加Activiti Designer的扩展(根据Eclipse安装不同,也可能看到几个其他的)。
-
选择
Activiti Designer Extensions
组,并点击Add JARs…
按钮。跳转至存储扩展的目录,并选择希望添加的扩展文件。完成后,配置界面会将扩展作为Activiti Designer Extensions
组的成员进行显示,像下面这样。
-
点击
OK
按钮保存并关闭配置对话框。Activiti Designer Extensions
会自动添加至你创建的新Activiti项目。可以在导航条或包管理器的项目树下的用户库条目中看到。如果工作空间中已经有了Activiti项目,也可以看到组中显示了新扩展,像下面这样。
打开的流程图将在其画板上显示新扩展的图形(或者禁用部分图形,取决于扩展中的配置)。如果已经打开了流程图,关闭并重新打开就能在画板上看到变化。
为画板添加图形 Adding shapes to the palette
项目配置完后,可以很轻松的为画板添加图形。每个添加的图形都表现为JAR中的一个类。请注意这些类并不是Activiti引擎运行时会使用的类。
在扩展中可以为每个图形描述Activiti
Designer可用的参数。在这些图形中,也可以定义运行时特性,并将由引擎在流程实例到达该节点时使用。运行时特性可以使用任何Activiti对普
通ServiceTask
支持的选项。查看这个章节了解更多信息。
图形的类是简单的Java类,加上一些注解。这个类需要实现CustomServiceTask
接口,但不应该直接实现这个接口,而应该扩展AbstractCustomServiceTask
基
类(目前必须直接扩展这个类,而不能在中间使用abstract类)。在这个类的Javadoc中,可以看到其默认提供的,与需要覆盖的方法介绍。覆盖可
以实现很多功能,例如为画板及画布中的图形提供图标(两个可以不一样),或者指定你希望节点实现的基图形(活动,时间,网关)。
1
2
3
4
5
6
7
8 /**
* @author John Doe
* @version 1
* @since 1.0.0
*/
public class AcmeMoneyTask extends AbstractCustomServiceTask {
...
}
需要实现getName()
方法,来决定节点在画板上的名字。也可以将节点放在自己的抽屉中,并提供图标,只需要覆盖AbstractCustomServiceTask
的对应方法就可以。如果希望提供图标,请确保放在JAR的src/main/resources
包中,需要是16X16像素的JPEG或PNG格式图片。你要提供的路径是到这个目录的相对路径。
可以通过在类中添加成员,并使用@Property
注解,来为形状添加参数。像这样:
1
2
3 @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
详细介绍的列表。添加了一个备注字段,也可以看到节点包含了一个图标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 /**
* @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,画板与相应的图形将像是这样:
money任务的参数界面在下面显示。请注意accountNumber
字段的必填信息。
在创建流程图、填写参数字段时,用户可以使用静态文本,或者使用流程变量的表达式(如"This little piggy went to ${piggyLocation}")。一般来说,用户可以在text字段自由填写任何文本。如果你希望用户使用表达式,并(使用@Runtime
)为CustomServiceTask
添加运行时行为,请确保在代理类中使用Expression
字段,以便表达式可以在运行时正确解析。可以在这个章节找到更多关于运行时行为的信息。
字段的帮助信息由每个参数右侧的按钮提供。点击该按钮将弹出显示下列内容。
配置自定义服务任务的运行时执行 Configuring runtime execution of Custom Service Tasks
当设置好字段,并将扩展应用至Designer后,用户就可以在建模流程时,配置服务任务的这些参数。在大多数情况下,会希望在Activiti执行流程时,使用这些用户配置参数。要做到这一点,必须告诉Activiti,当流程到达你CustomServiceTask
时,需要使用哪个类。
有一个特别的注解,@Runtime
,用于指定CustomServiceTask
的运行时特性。这里有些如何使用的例子:
1 @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
参数中。
可以在 |
请注意不应该在你的扩展JAR中包括运行时类,因为它与Activiti库是分离的。Activiti需要在运行时能够找到它们,因此需要将其放在Activiti引擎的clsspath中。 |
Designer代码树中的示例项目包含了配置@Runtime
的不同选项的例子。可以从查看money-tasks项目开始。引用代理类的示例在money-delegates项目中。
参数类型 Property types
这个章节介绍了CustomServiceTask
能够使用的参数类型,可以将类型设置为PropertyType
的值。
PropertyType.TEXT
创建如下所示的单行文本字段。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将字段的背景变为浅红色。
PropertyType.MULTILINE_TEXT
创建如下所示的多行文本字段(高度固定为80像素)。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将字段的背景变为浅红色。
PropertyType.PERIOD
创建一个组合编辑框,可以使用转盘控件编辑每一个单位的数量,来指定一段时间长度,结果如下所示。可以是必填字段(含义是不能所有的值都是0,也就 是至少有一个部分要有非零值),并将验证消息作为提示信息显示。验证失败会将整个字段的背景变为浅红色。字段的值保存为1y 2mo 3w 4d 5h 6m 7s格式的字符串,代表1年,2月,3周,4天,6分钟及7秒。即使有部分为0,也总是存储整个字符串。
PropertyType.BOOLEAN_CHOICE
创建一个单独的boolean复选框,或者开关选择。请注意可以在Property
注解上指定required
属性,但不会生效,不然用户就无法选择是否选中复选框。流程图中存储的值为java.lang.Boolean.toString(boolean),其结果为"true"或"false"。
PropertyType.RADIO_CHOICE
创建如下所示的一组单选按钮。选中任何一个单选按钮都自动排除任何其他的选择(也就是说,单选)。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将组的背景变为浅红色。
这个参数类型需要注解的类成员同时使用@PropertyItems
注解(例如如下所示)。可以使用这个额外的注解,以字符串数组的方式,指定条目的列表。需要为每一个条目添加两个数组项:第一个,用于显示的标签;第二个,用于存储的值。
1
2
3
4 @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;
PropertyType.COMBOBOX_CHOICE
创建如下所示的,带有固定选项的下拉框。可以是必填字段,并将验证消息作为提示信息显示。验证失败会将下拉框的背景变为浅红色。
这个参数类型需要注解的类成员同时使用@PropertyItems
注解(例如如下所示)。可以使用这个额外的注解,以字符串数组的方式,指定条目的列表。需要为每一个条目添加两个数组项:第一个,用于显示的标签;第二个,用于存储的值。
1
2
3
4
5
6 @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;
PropertyType.DATE_PICKER
创建如下所示的日期选择控件。可以是必填字段,并将验证消息作为提示信息显示(请注意,这个控件会自动填入当前系统时间,因此值很难为空)。验证失败会将控件的背景变为浅红色。
这个参数类型需要注解的类成员同时使用@DatePickerProperty
注解(例如如下所示)。可以使用这个额外的注解,指定在流程图中存储日期时使用的日期格式,以及要用于显示的日期选择类型。这些属性都是可选的,当没有指定时会使用默认值(DatePickerProperty
注解的静态变量)。dateTimePattern
属性应该使用SimpleDateFormat
类支持的格式。当使用swtStyle
属性时,应该指定SWT
的DateTime
控件支持的整形值,因为将使用这个控件渲染这个类型的参数。
1
2
3
4 @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;
PropertyType.DATA_GRID
创建一个如下所示的数据表格控件。数据表格可以让用户输入任意行数据,并为每一行输入固定列数的值(每一组行列的组合代表一个单元格)。用户可以添加与删除行。
这个参数类型需要注解的类成员同时使用@DataGridProperty
注解(例如如下所示)。可以使用这个额外的注解,指定数据表格的细节属性。需要用itemClass
属性引用另一个类,来决定表格中有哪些列。Activiti Designer期望其成员类型为List
。按照约定,可以将itemClass
属性的类用作其泛型类型。如果,例如,在表格中编辑一个杂货清单,用GroceryListItem
类定义表格的列。在CustomServiceTask
中,可以这样引用它:
1
2
3 @Property(type = PropertyType.DATA_GRID, displayName = "杂货清单 Grocery List")
@DataGridProperty(itemClass = GroceryListItem.class)
private List<GroceryListItem> groceryList;
与CustomServiceTask
一样,当使用数据表格时,"itemClass"可以使用相同的注解指定字段类型,目前支持TEXT
,MULTILINE_TEXT
与PERIOD
。你会注意到不论其PropertyType
是什么,表格都会为每个字段创建一个单行文本控件。这是为了表格保持整洁与可读。如果考虑下PERIOD
这种PropertyType
的显示模式,就可以想象出它绝不适合在表格的单元格中显示。对于 MULTILINE_TEXT
与PERIOD
,会为每个字段添加双击机制,并会为该PropertyType
弹出更大的编辑器。数值将在用户点击OK后存储至字段,因此可以在表格中显示。
必选属性使用与普通TEXT
字段类似的方式处理,当任何字段失去焦点时,会验证整个表格。验证失败的单元格,背景色将变为浅红色。
默认情况下,这个组件允许用户添加行,但不能决定行的顺序。如果希望允许排序,需要将orderable
属性设置为true,这将在每一行末尾启用按钮,以将该行在表格内上移或下移。
目前,这个参数类型不能正确注入运行时类。 |
在画板中禁用默认图形 Disabling default shapes in the palette
这种自定义需要在你的扩展中引入一个实现了DefaultPaletteCustomizer
接口的类。不应该直接实现这个接口,而要扩展AbstractDefaultPaletteCustomizer
基类。目前,这个类不提供任何功能,但DefaultPaletteCustomizer
未来的版本中会提供更多功能,这样基类将提供更多合理的默认值,这样你的扩展将在未来的版本中更好用。
扩展AbstractDefaultPaletteCustomizer
需要实现一个方法,disablePaletteEntries()
,并必须返回一个PaletteEntry
值的list。请注意如果从默认集合中移除图形,导致某个抽屉中没有图形,则该抽屉也会被移除。如果需要禁用所有的默认图形,只需要在结果中添加PaletteEntry.ALL
。作为例子,下面的代码禁用了画板中的手动任务和脚本任务图形。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 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
抽屉中不再显示手动任务与脚本任务图形。
要禁用所有默认图形,需要使用类似下面的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 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;
}
}
结果像是这样(请注意画板中不再显示默认图形所在的抽屉):
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
。
通常,会想要将ProcessValidator
与ExportMarshaller
一起使用。例如有一些CustomServiceTask
,带有一些希望在流程中使用的参数。然而,在生成流程前,希望验证其中一些值。联合使用ProcessValidator
与ExportMarshaller
是最佳的方式,Activiti Designer也允许你无缝拼接扩展。
要创建一个ProcessValidator
或ExportMarshaller
,需要创建
与