模型设计器项目qqdznyyglpt-workflow-designer主要完成自定义流程、表单设计,可以在模型设计器中完成整个功能的流转,但仅限与admin(密码默认为test)相同租户的用户可使用全功能,其他租户用户登录后只有流程设计、表单配置、决策表配置功能,无法在设计器进行流程执行。需要更改的模型、表单、决策表生效,则必须重新发布,发布之后新启的流程按最新的走,发布之前的流程仍然按前一版本执行。
用户手册地址:https://www.activiti.org/userguide/
数据库及邮件服务器配置文件地址:qqdznyyglpt-workflow-designer\src\META-INF\activiti-app\activiti-app.properties 。
打印工作流SQL语句的开关在log4j.properties中配置log4j.logger.org.activiti.engine.impl.persistence.entity=DEBUG即可。
用户任务需要人为处理完成。常用属性配置包括名称、是否为多实例、分配(任务处理人/处理组配置)、到期时间、表单(引用的表单、表单元素、表单地址),各属性详细用法见1.4节属性元素。
服务任务可以用于执行外部的java类,可以通过以下3种方式实现:
脚本任务是一个自动任务,当流程执行到脚本任务即会自动执行该任务上配置的脚本。
脚本格式主要定义使用什么脚本类型(JSR-223)编写脚本代码,目前JavaScript是默认的,其他脚本类型均需要增加对应的脚本引擎依赖包。在脚本中所有流程中参数均可以被引用,也可以使用execution.setVariable(“variableName”, variableValue)设置流程参数,设置流程参数名称时,需避免使用out, out:print, lang:import, context, elcontext。
以上是脚本JavaScript的脚本示例(可将zip导入designer的应用中),其中field0是流程中已存在变量,可以直接引用,assignee是新设置的流程变量,可以在下一节点获取并使用。
业务规则任务可以同步执行一个或多个规则,Activiti使用Drools引擎执行业务规则,*.drl的规则文件需和流程定义一起导入(同外置表单)。
当有多个规则文件配置时,可使用逗号隔开;当有多个输入变量,也可使用逗号隔开;但输出变量只能有一个名称,缺省值是org.activiti.engine.rules.OUTPUT
接收任务是用于等待某个特定消息的任务到达。
手动任务定义了一个扩展BPM引擎的外部任务,对于流程来说手动任务就是一个传递任务。
邮件任务主要用于自定义邮件发送,使用邮件任务时,首先需要在activiti-app.properties文件中配置邮件服务器地址。邮件任务的各元素属性的配置内容均可使用或者加入表达式。
Camel任务不是标准的BPMN2.0包含的,他通过从Camel中收发消息来增强Activiti的集成性,在Activiti中他被作为一个专用的服务任务被应用。需要在Spring容器中配置camelContext
Mule任务不是标准的BPMN2.0包含的,在Activiti中他被作为一个专用的服务任务,他可以给Mule发送消息以增强Activiti的集成性。
他的属性元素定义如下表:
决策任务主要是根据决策表配置的决策原则自动进行任务节点处理,决策表中的变量可以被引用作为流转条件的判断。
如果决策表里某个输出没有结果 ,则在下一个节点引用时该变量为空,如果是在判断节点引用该变量,可能会报无法识别属性某某的错误。如下示例中,当决策表自动判断loanAmount>2时,会出结果rejectLoan=true,如果此时在后续的判断节点引用${riskCheck==false},则会抛出找不到riskCheck属性的异常。
以下是示例应用,可导入designer的应用中进行测验。
当需要将几个流程节点都作为一个子流程时,可以将该几个流程节点作为一整体,及创建一个子流程,如下图中间审批可以作为一个子流程,定时器监听的是整个子流程的耗时时间。子流程中必须包含一个空起始时间,必须包含一个结束事件,并且序列流不能越过子流程框定的范围。
事件子流程是由事件触发的子流程,事件子流程由非空开始事件发起,并且事件子流程没有任何的出入序列流。Activiti只支持使用错误开始事件和消息开始事件触发事件子流程。
主要是为了让流程配置看起来层次分明,逻辑明朗,部署的时候会根据泳道的流程ID生成流程定义任务。泳道列表包含泳道池和泳道,首先将泳道池拖拽到界面,然后将泳道拖拽到泳道池中形成泳道池的分割线。一个流程实例的设计中只能包含一个泳池。
分支也叫网关,是用于控制流程流转的。
有且仅有一个后续分支连线会被执行,当分支连线上的流转条件为true(或者没有设置条件)时,流程将通过该分支连线继续执行;如果存在多个分支连线流转条件均为true,则默认执行第一个分支连线;如果没有满足条件的分支连线,则会抛出找不到后续节点的异常。
并行分支包含分叉和聚合,当包含多个分叉任务节点,每执行完一个任务节点就会在聚合口等待,直到所有聚合节点包含的任务节点均执行完成后,流程才会进入后续节点。并行分支后的任务节点是被强制生成并行实例的,即使在并行分支连线上有配置的流转条件也无效。
包容分支是排他分支和并行分支的结合体,也包含分叉和聚合,与并行分支不同的是他可以在分支连线上定义流转条件,并且只会并行执行流转条件为true的连线上的任务,与排他分支不同的是他可以同时并行执行多个任务。
事件分支后续节点只能连接中间捕获事件,并且必须连接两个或两个以上的中间捕获事件(否则执行过程中会报错),另外,连接事件分支的中间捕获事件必须只有一个信号输入流。
如下图当流程执行到事件分支时被挂起,流程实例订阅了一个信号事件并创建了一个间隔时间为10分钟的定时器。这将导致流程引擎等待10分钟去接收一个信号事件,如果信号在10分钟内到达,则定时器被取消流程在信号后继续执行,如果信号没有被激活,则流程在定时器后继续执行,并且信号订阅被取消。
事件是用于在流程生命周期中模拟某件事的发生,主要分为捕获事件和抛出事件。流程执行到捕获事件时将等待一个触发器发生,流程执行到抛出事件时相关触发器将被激活。
定时器事件由定义的定时器到点触发,可以被用作开始事件、中间事件、边界事件,并且定时器事件只有jobExecutorActivate 或 asyncExecutorActivate属性被设置为true的时候定时器才会执行。
定时器必须包含以下三种方式之一的配置,否则流程将发布不成功:
在流程定义时可以进行信号定义,信号事件广播范围分为全局信号事件和流程实例信号事件,全局信号事件可用于流程间的通讯,一旦全局信号事件被抛出,所有订阅了该信号事件的流程实例都会接受到信号,并且捕获信号只能捕获信号流程发起之后的信号;流程实例信号事件用于流程内的通讯。
可以使用中间信号抛出事件进行信号抛出,也可以使用如下代码抛出信号:
RuntimeService.signalEventReceived(String signalName);(全局)
RuntimeService.signalEventReceived(String signalName, String executionId);(指定实例)
可以使用中间信号捕获事件或者边界信号事件处理信号的捕获。
查询信号事件订阅代码:
List executions = runtimeService.createExecutionQuery().signalEventSubscriptionName("alert").list();
以下是一个信号事件实例,如果3分钟之内并行事件节点接收到了全局/实例抛出的信号事件,则流程开始执行信号事件对应的节点receive check/receive hello,如果3分钟之内未收到任何信号事件,则流程开始执行定时器对应的节点timer,并且信号订阅取消。
以下是上图的应用打包文件,可以将其导入designer的应用中执行试试。
消息事件也是在流程上定义,可以通过消息开始事件、中间消息捕获事件和边界消息捕获事件进行捕获。不同于信号事件的是一个消息事件只能指向一个唯一接收人。Activiti不关注消息的抛出和接收,应用可以通过自定义接入JMS、webservice、REST 请求进行消息处理。
可通过以下代码触发执行订阅的消息事件:
messageEventReceived(String messageName, String executionId);
messageEventReceived(String messageName, String executionId, HashMap processVariables);
BPMN中的错误事件不是指的程序异常,而是业务异常的一种方式,相关事件包括错误开始事件、错误结束事件、边界错误事件。错误开始事件可以用于触发一个事件子流程,但不能用于开启一个流程实例。当流程执行到一个错误结束事件时,当前执行结束并有一个错误抛出,该错误可以被边界错误事件捕获,如果没有匹配的边界错误事件,则会抛出异常。
在实现了JavaDelegates接口的类中可以抛出BpmnError这个异常,这个异常可以被边界错误事件或者子流程错误事件捕获。
错误事件的使用配置可参考信号事件。
所有边界事件均属捕获事件,边界事件包括边界定时器事件、边界信息事件、边界消息事件、边界终止事件、边界错误事件、边界补偿事件。边界事件不能有多个流程序列流,如果需要执行多个流出序列流,可以使用并行节点。边界定时器事件可以在额外触发邮件提醒的情况下,还保证流程的正常流转。
中间事件分为中间抛出事件和中间捕获事件,中间抛出事件又分为中间空抛出事件和中间信号抛出事件、中间补偿抛出事件,中间空抛出事件多用于记录在流程中达到了某个状态,多结合执行监听器使用;中间捕获事件又分为中间定时器捕获事件、中间消息捕获事件、中间信号捕获事件。
流程执行的起始节点,包括定时器开始事件、消息开始事件、信号开始事件、错误开始事件和空开始事件,每个类型的开始事件都由该类型事件的特性进行触发。一般多采用空开始事件。详情见用户手册的第8.2.6至8.2.11章节。
流程结束的标志,包括空结束事件、错误结束事件、取消结束事件和终止结束事件,所有结束事件都是抛出事件。一般多使用空结束事件。
当流程到达终止结束事件时,当前流程实例或者子流程将被终止,但在多实例中他只能结束当前实例,其他实例不受影响。如果将terminateAll属性设置为true,则根节点流程也会终止。
取消结束事件只能结合事务子流程使用,当流程到达取消结束事件时,取消事件将被抛出并可被边界取消事件捕获,边界取消事件将取消事务并触发补偿。
Activiti默认按顺序执行流程节点,从一个完成节点到下一个等待状态的节点之间的所有节点元素是同单元任务,其执行的成功与失败均具有原子性,例如下图的②、③、④是同单元任务,如果执行到第④步出错,则流程将回滚到执行节点①。
而有时我们不想让后续流程的执行结果影响到前面流程执行完成,因此我们可以使用异步,他可以让我们人为控制流程的回滚边界,如上图我们将节点④的异步属性设置为TRUE,那么当④发生异常时,①会照例被执行完成,②和③也会完成执行,流程会挂起在④节点。
实际应用场景试想:比如我们在提交订单后会给客户发一条短信,但是我们不想因为短信的发送失败导致订单无法提交,此时就可以将发送短信的任务设置为异步。
用于保证相同实例下的并行任务不会被同时执行,系统默认该属性为true。
补偿属性选择为是时该节点即为补偿节点,可用于补偿另一个活动的影响,补偿节点没有出入顺序流,也不包含在一般流程中,他只会在补偿事件被抛出时执行,一个补偿节点必须定向关联一个边界补偿事件(反之亦然)。
边界补偿事件是在其被附着的节点成功完成时才被激活(其他边界事件是被附着节点开始时执行,结束时被取消),在流程完成时才会取消。目前Activiti designer中暂未加入抛出补偿事件。
使用边界事件时通常需要配置一个中断任务,默认是勾选状态,表示当达到边界事件抛出条件时,当前边界事件附属的任务将被主动终止,而边界事件关联的任务及后续流程将被激活,并且边界事件附属任务的后续流程都将被取消。
任务分配可以选择系统身份和固定值,系统身份可以直接选择系统中配置的用户和用户组,固定值可以是一个表达式,也可以是用户或者用户组的ID值。
图一
分配属性中可配置唯一处理人(可直接处理任务,对应task表的assignee字段,一个任务只能有一个唯一处理人)、候选处理人(需签收才能处理任务,对应identitylink表type等于candidate的用户ID)、候选处理组(组员需签收才能处理任务,对应identitylink表type等于candidate的组ID),处理优先级是唯一处理人>候选处理人=候选处理组。
经workflow-service扩展之后,如果在任务节点上配置了唯一处理人,同时在该任务节点的上一节点的表单里配置了下一处理人,则表单上配置的下一处理人无效,默认该任务节点的处理人为节点上配置的人,处理组同理。如果任务节点上未配置唯一处理人,同时在该任务节点的上一节点的表单里配置了下一处理人,当选择的下一处理人只有一个人时,默认设置为该任务节点的唯一处理人;当选择的下一处理人有多个时,均默认为候选处理人。
可以指定一个具体的时间,也可以指定一个事件间隔(例如PT30M),如果是时间间隔则以流程发起的时间开始往后推算。
以下3个属性的优先级是表单地址>引用的表单>表单元素。Designer中表单的数据都是以字段记录的方式进行保存,执行中流程的表单保存在act_ru_variable,执行完成流程的表单保存在act_hi_varinst。
连接线都可以配置流转条件,如果一个节点有多个后续节点,并且其中有一个后续节点没有配置流转条件,则流程会默认流向该节点(即没配置默认该条件为true)。
表达式可以使用值表达式(${myParam}
)和方法表达式(${myBean.methodName(params…)==true/false}
)两种,其中值表达式中的参数一定要在上一个任务中设值并且在调用completeTask的时候传入,否则会报找不到属性XXX的异常。方法表达式可通过自定义类实现,默认参数包含execution、task、authenticatedUserId,方法表达式需要在config类文件中配置bean才会被识别。
多实例是并行实例的一种,多实例是通过任务节点上的属性配置实现并行,并行实例则是通过并行分支的方式实现。
它们的共同点是当到达指定节点时,都可以同时生成一批任务,分发给不同的人进行并行处理,只有所有子任务处理完成之后该并行任务才算完结。
多实例可以配置在各种类型的任务节点、子流程、回调活动节点上。因多实例是在单节点上进行配置,故其生成的子任务除了处理人不同,其他属性都一样,比如关联的表单、节点名称。多实例可以进行完结条件控制,比如分配的人员在处理率达到80%时,自动完结。
并行实例每个子任务节点及其属性是可以自定义配置的,比如关联的表单、处理人可以一致也可以不同。并行实例在开启时所有子任务节点会强制生成,并且只有在所有子任务节点全部完成的情况下才会自行完结。
1. 多实例变量
多实例包含的默认变量,可以通过execution.getVariable(x)方法获取:
nrOfInstances:创建的实例总数
nrOfActiveInstances:激活的实例数,针对顺序类型的多实例,该变量值等于1
nrOfCompletedInstances:已执行实例数
loopCounter:表示多实例流程循环的下标
通过自定义扩展之后,用户任务类型的多实例新增了以下四种变量,同意和拒绝的数量统计来源于用户在提交多实例表单时选择的自定义按钮名称,按钮名称为不同意、不通过、拒绝、退回时计为拒绝实例数量,其他则计为同意实例数量。
nrOfRejectCount:多实例中选择拒绝的实例数量
rejectPersons:多实例中选择拒绝的人的ID列表,逗号分隔
nrOfAcceptCount:多实例中选择同意的实例数量
acceptPersons:多实例中选择同意的人的ID列表,逗号分隔
/**
*
*
* 监听所有任务的创建和完成事件,此类主要处理多实例任务监听器
*
*
* @author AsyDong
* @time 2019年6月28日 下午4:39:59
* @version 1.0
*/
public class MultiInstanceTaskEventListener implements ActivitiEventListener {
/**
* 日志处理器
*/
private final Logger logger = LoggerFactory.getLogger(MultiInstanceTaskEventListener.class);
@Override
public void onEvent(ActivitiEvent event) {
ActivitiEntityEventImpl eventImpl = (ActivitiEntityEventImpl) event;
TaskEntity taskEntity = (TaskEntity) eventImpl.getEntity();
ExecutionEntity execution = taskEntity.getExecution();
// 判断当前任务是否是多实例节点,如果是,记录当前处理结果,并统计次数
MultiInstanceLoopCharacteristics multiIns = ((Activity) execution.getCurrentFlowElement()).getLoopCharacteristics();
if (multiIns != null) {
switch (event.getType()) {
case TASK_CREATED:
// 刚启动多实例任务时,设置自定义初始变量
if (execution.getVariable(MultiInstance.NUMBER_OF_COMPLETED_INSTANCES) != null && 0 == (Integer) execution.getVariable(MultiInstance.NUMBER_OF_COMPLETED_INSTANCES)) {
execution.setVariable(MultiInstance.NUMBER_OF_REJECT_COUNT, 0);
execution.setVariable(MultiInstance.REJECT_PERSON_COLLECTION, "");
execution.setVariable(MultiInstance.NUMBER_OF_ACCEPT_COUNT, 0);
execution.setVariable(MultiInstance.ACCEPT_PERSON_COLLECTION, "");
logger.info("创建多实例任务节点:{},处理人:{}", taskEntity.getName(), taskEntity.getAssignee());
}
break;
case TASK_COMPLETED:
// 获取当前同意和不同意的节点数量
int aptCount = execution.getVariable(MultiInstance.NUMBER_OF_ACCEPT_COUNT) == null ? 0 : (Integer) execution.getVariable(MultiInstance.NUMBER_OF_ACCEPT_COUNT),
rjtCount = execution.getVariable(MultiInstance.NUMBER_OF_REJECT_COUNT) == null ? 0 : (Integer) execution.getVariable(MultiInstance.NUMBER_OF_REJECT_COUNT);
// 获取当前同意和不同意的人
String aptPerson = execution.getVariable(MultiInstance.ACCEPT_PERSON_COLLECTION) == null ? "" : (String) execution.getVariable(MultiInstance.ACCEPT_PERSON_COLLECTION),
rjtPerson = execution.getVariable(MultiInstance.REJECT_PERSON_COLLECTION) == null ? "" : (String) execution.getVariable(MultiInstance.REJECT_PERSON_COLLECTION);
// FIXME 这里的判断只包含了按钮,是否有更好的解决方案?
Object outVal = execution.getVariable("form_" + taskEntity.getFormKey() + "_outcome");
String outValStr ;
if (outVal != null && ((outValStr = outVal.toString()).indexOf("不同意") != -1 || outValStr.indexOf("不通过") != -1
|| outValStr.indexOf("拒绝") != -1 || outValStr.indexOf("退回") != -1)) {
rjtCount += 1;
rjtPerson += taskEntity.getAssignee() + "|";
execution.setVariable(MultiInstance.NUMBER_OF_REJECT_COUNT, rjtCount);
execution.setVariable(MultiInstance.REJECT_PERSON_COLLECTION, rjtPerson);
} else {
aptCount += 1;
aptPerson += taskEntity.getAssignee() + "|";
execution.setVariable(MultiInstance.NUMBER_OF_ACCEPT_COUNT, aptCount);
execution.setVariable(MultiInstance.ACCEPT_PERSON_COLLECTION, aptPerson);
}
logger.info("任务完成。同意人数:{},同意人:{},拒绝人数:{},拒绝人:{}", aptCount,aptPerson,rjtCount,rjtPerson);
break;
default:
break;
}
}
}
@Override
public boolean isFailOnException() {
return false;
}
}
然后在ActivitiEngineConfiguration中配置:
List eventListeners = new ArrayList<>();
eventListeners.add(globalEventListener());
Map> typeEvtMap = processEngineConfiguration.getTypedEventListeners();
if(typeEvtMap == null){
typeEvtMap = new HashMap<>();
}
typeEvtMap.put("TASK_CREATED,TASK_COMPLETED", eventListeners);
processEngineConfiguration.setTypedEventListeners(typeEvtMap);
2. 多实例属性说明
下图红色方框是多实例配置的属性(分配属性不属于多实例属性,只是可配合使用):
多实例类型
顺序(sequential ):按顺序生成多个实例,处理完一个后才会生成下一个,当前活动实例数(nrOfActiveInstances)一直是1
并行(parallel):生成多个实例同时并行发放给指定的处理人
多实例基数
定义多实例生成的实例数。当结合采集方式生成多实例时,该基数只能小于或等于采集集合的size,否则执行过程中将发生系统错误 NoSuchElementException;当基数小于采集集合的size时,则按照集合中元素的顺序生成等于基数指定数量的实例。如果不配合使用采集方式,也可以直接指定基数,则系统会同时生成指定数量的相同实例。
多实例采集
可以指定一个List变量,也可以选择处理人或处理组列表,系统会自动生成等于List size数量的实例。当该List设置为处理人员列表时,可定义一个元素变量名,然后将分配—固定值—处理人/候选处理人设置为该元素变量名,则系统会为每一个生成的实例依次指定List中的人员。处理组同理。
多实例元素变量
表示多实例采集中元素对应的变量,该变量保存着集合中元素的最新值,可以使用表达式(例如:${assignee}
)获取。
多实例完成条件
可以接受标准的UEL表达式,如果返回值为true则该多实例自动结束。例如 ${nrOfCompletedInstances/nrOfInstances >= 0.6 }
只要所有流程实例完成了60%即该多实例任务结束。
3. 多实例处理人配置
多实例任务的处理人有两种设置方式:
一是,将分配—固定值—处理人/候选处理人/候选处理组设置为多实例元素变量名,则系统会自动按照采集数据(必须是用户ID/用户组ID的集合)将多实例子任务直接依次按人/组分配下去。
二是,直接在节点属性的分配—系统身份—候选用户/单个用户上进行配置,此种方式下系统生成的多实例子任务的处理人均一样;
如果不同类型的监听器监听了同一事件,他们的执行优先级是事件监听器>执行监听器>任务监听器。所有被作为委托表达式进行配置的类(方法表达式同理),均需要在中processEngineConfiguration注册为bean,例如下面的myEvent(在原生类上声明@Component)和myDelegateExecutionListener(在当前类声明@Bean方法):
另外,当使用的类和委托表达式是单例模式时,需要考虑线程安全。
执行监听器是在流程执行过程中当某个事件被触发时执行,可用于求值表达式或者执行外部JAVA代码。
可被执行监听器捕获的事件包括:
每种类型的事件均可使用以下三种方式实现监听:
${myBean.methodName(execution.eventName)}
,DelegateExecution及其属性都可以作为参数传入,该种方式下配置的参数字段将无效;${myDelegateExecutionListener}
,即配置一个实现了org.activiti.engine.delegate.JavaDelegate/ExecutionListener的类,当使用该种方式时,也可以配置参数字段,但不一定需要所有的配置字段都有类属性声明,获取方式同自定义类实现方式;任务监听器用于在触发某个任务相关的事件时执行自定义java逻辑或是表达式,任务监听器只能在用户任务节点上配置,可被任务监听器捕获的任务相关事件类型包括:
每种类型的事件均可使用以下三种方式实现监听:
${myBean.methodName(task.eventName)}
,DelegateTask及其属性都可以作为参数传入;${myDelegateTaskListener}
,即配置一个实现了org.activiti.engine.delegate. TaskListener的类;设计模型时可以在流程定义上配置事件监听器,他的作用域只限于该模型的流程实例的事件监听。事件监听器还可以选择以抛出其他事件的方式执行捕获到的事件,勾选“重新抛出事件”即可进行相关配置。事件监听器上配置的类或者委托表达式需要实现org.activiti.engine.delegate.event.ActivitiEventListener接口。
全局事件监听器必须实现org.activiti.engine.delegate.event.ActivitiEventListener接口,并且在流程引擎的配置文件ActivitiEngineConfiguration.java中进行配置,如果配置在属性eventListeners上,则将监听所有的事件,如果配置在属性typedEventListeners上,则可以指定监听特定的事件。