Activiti的表单
1、概述
对于一些较为稳定的业务流程,全部的功能可以直接由程序员实现,这些功能包括流程的制定、具体领域业务代码的实现和表现层交互实现等,但是在实际应用中,不变的业务并不存在。为了让程序能更好地适应业务流程的变化,在工作流领域出现了动态流程和动态表单等概念。
Activiti提供了两种设置表单的方式,流程引擎的开发者可以根据不同的情况来选择合适的方式。
2、表单属性
可以在流程的开始事件或者任务中使用activiti:formProperty
元素定义一个表单属性,使用
FormService的方法可以查询及设置这些属性。在流程文件中定义的这些表单属性,与具体的表现层技术无关,界面层如何将参数传递给流程引擎,由具体的表现层技术决定,Activiti的流程配置文件及FormService,只提供一个桥梁。
<process id="process1" name="process1">
<startEvent id="startevent1" name="Start">
<extensionElements>
<activiti:formProperty id="userName" name="userName"
variable="userName" type="string" />
</extensionElements>
</startEvent>
<userTask id="usertask1" name="User Task"></userTask>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow1" name="" sourceRef="startevent1"
targetRef="usertask1"></sequenceFlow>
<sequenceFlow id="flow2" name="" sourceRef="usertask1"
targetRef="endevent1"></sequenceFlow>
</process>
在代码中,为开始事件定义了一个“userName”表单属性,表示在该流程的开始表单中,需要用户填写“userName”字段,类型为字符串。定义了该表单属性,就可以使用FormService的submitStartFormData方法启动流程,“userName”会被设置到流程参数中,activiti:formProperty的variable属性是流程参数名称。
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = engine.getRepositoryService();
FormService formService = engine.getFormService();
RuntimeService runtimeService = engine.getRuntimeService();
//部署
Deployment deploy = repositoryService.createDeployment().addClasspathResource("demo18/FormProperty.bpmn").deploy();
//查找流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(deploy.getId()).singleResult();
//使用表单参数启动流程
Map<String, String> vars = new HashMap<>();
vars.put("userName", "tom");
ProcessInstance pi = formService.submitStartFormData(pd.getId(), vars);
//查询参数
System.out.println(runtimeService.getVariable(pi.getId(), "userName"));
在本例中,“userName”的值被写死为“tom’”,而在实际情况中,这些表单属性值一般来源于用户填写的表单,使用表单属性这种方式来定义流程的表单,使得流程属性(参数)与具体的表现层技术无关。用户填写的表单和流程引擎之间,通过submitStartFormData方法产生关联。
3、外部表单
相对于定义表单属性的方式,使用外部表单的方式使流程和表单之间的关系更加松散,只需要在开始事件或者任务中使用activiti:formKey
来配置外部表单的链接,这个链接可以是个普通的HTML页面、一个XML文件或者一个URL,表单的内容和样式完全由外部决定。在部署时,需要将这个外部表单添加到部署中(DeploymentBuilder的部署方法),Activiti的部署API,会将其内容存放到资源表中,可以使用FormService提供的方法来读取这些“外部’表单的内容。采用这种方式定义表单,表面上流程与表单的具体参数解耦(流程只需要知道表单的链接),但在流程中获取参数时,流程必须很清楚外部表单的内容,因为流程中所使用的参数,在业务层面就决定了流程和表单之间不可分割的关系。
<process id="ExternalForm" name="ExternalForm" isExecutable="true">
<startEvent id="startevent1" name="Start" activiti:formKey="form/start.jsp"></startEvent>
<userTask id="usertask1" name="User Task" activiti:formKey="form/task.form"></userTask>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow1" sourceRef="startevent1"
targetRef="usertask1"></sequenceFlow>
<sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
</process>
在代码中的开始事件中,添加了activiti:formKey属性,声明该流程的开始表单使用的是start.jsp。本例中的start.jsp是一个普通的JSP文件,task.form的内容与start.jsp类似,
在task.form中会将流程参数输出。
start.jsp:
<div>
<span style="color: green;">开始表单: <input type="text" name="days" /></span>
</div>
task.form
<div>
<span style="color: red;">任务表单: ${days}</span>
</div>
// 创建流程引擎
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = engine.getRepositoryService();
FormService formService = engine.getFormService();
TaskService taskService = engine.getTaskService();
// 部署全部文件
Deployment dep = repositoryService.createDeployment()
.addClasspathResource("demo18/ExternalForm.bpmn")
.addClasspathResource("form/start.jsp")
.addClasspathResource("form/task.form").deploy();
// 流程定义
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().deploymentId(dep.getId()).singleResult();
// 启动流程并设置days参数
Map<String, String> vars = new HashMap<String, String>();
vars.put("days", "4");
ProcessInstance pi = formService.submitStartFormData(pd.getId(), vars);
// 输出开始表单内容
Object obj = formService.getRenderedStartForm(pd.getId());
System.out.println(obj);
// 输出被渲染后的任务表单内容
Task task = taskService.createTaskQuery().processInstanceId(pi.getId()).singleResult();
Object form = formService.getRenderedTaskForm(task.getId());
System.out.println(form);
<div>
<span style="color: green;">开始表单: <input type="text" name="days" /></span>
</div>
<div>
<span style="color: red;">任务表单: 4</span>
</div>
使用外部表单的方式来定制表单,完全将表单的内容交由外部决定,表单与流程之间仅仅通过formKey耦合。如果用户的表现层使用的是HTML或者JSP,那么就可以通过多种途径来获取开始表单的内容,例如调用FormService来读取,使用include等方式。用户填写的表单被提交到服务器后,就调用submitStartFormData方法来启动流程。
4、动态表单
<process id="leave-formkey" name="请假流程-外置表单" isExecutable="true">
<!--
开始节点:activiti:initiator属性的作用:可以把启动流程实例的操作人以变量名称“applyUserId”
保存到数据库中,需要配合identityService.setAuthenticatedUserId(String userId)方法使用,
其中userId即当前操作人,在实际的应用中应该是当前的用户ID,引擎会把setAuthenticatedUserId()
方法的参数作为流程启动人,通过调用HistoricProcessInstance实例的getStartUserId()可以获取一
个历史(也可能正在运行)流程实例由哪个用户启动。要获取设置的用户ID,可以通过调用Authentication.
getAuthenticatedUserId()方法来实现。
-->
<startEvent id="startevent1" name="Start"
activiti:initiator="applyUserId"
activiti:formKey="bk/leave-start.form"></startEvent>
<userTask id="deptLeaderVerify" name="部门经理审批"
activiti:candidateGroups="deptLeader"
activiti:formKey="bk/approve-deptLeader.form"></userTask>
<exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway>
<userTask id="hrVerify" name="人事经理审批"
activiti:candidateGroups="hr"
activiti:formKey="bk/approve-hr.form"></userTask>
<exclusiveGateway id="exclusivegateway2" name="Exclusive Gateway"></exclusiveGateway>
<userTask id="reportBack" name="销假"
activiti:assignee="${applyUserId}"
activiti:formKey="bk/report-back.form"></userTask>
<endEvent id="endevent1" name="End"></endEvent>
<userTask id="modifyApply" name="调整申请内容"
activiti:assignee="${applyUserId}"
activiti:formKey="bk/modify-apply.form"></userTask>
<exclusiveGateway id="exclusivegateway3" name="Exclusive Gateway"></exclusiveGateway>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="deptLeaderVerify"></sequenceFlow>
<sequenceFlow id="flow2" sourceRef="deptLeaderVerify" targetRef="exclusivegateway1"></sequenceFlow>
<!--
当表达式 ${deptLeaderApprove == 'false'}成立时,将输出流指定到ID为modifyApply的用户任务
当表达式 ${deptLeaderApprove == 'true'}成立时,将输出流指定到ID为hrAudit的用户任务
-->
<sequenceFlow id="flow3" name="同意" sourceRef="exclusivegateway1" targetRef="hrVerify">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${deptLeaderApproved == 'true'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow4" sourceRef="hrVerify" targetRef="exclusivegateway2"></sequenceFlow>
<sequenceFlow id="flow5" name="同意" sourceRef="exclusivegateway2" targetRef="reportBack">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${hrApproved == 'true'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow6" sourceRef="reportBack" targetRef="endevent1">
<extensionElements>
<activiti:executionListener event="take" expression="${execution.setVariable('result', 'ok')}"></activiti:executionListener>
</extensionElements>
</sequenceFlow>
<sequenceFlow id="flow7" name="不同意" sourceRef="exclusivegateway2" targetRef="modifyApply">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${hrApproved == 'false'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow8" name="不同意" sourceRef="exclusivegateway1" targetRef="modifyApply">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${deptLeaderApproved == 'false'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow9" sourceRef="modifyApply" targetRef="exclusivegateway3"></sequenceFlow>
<sequenceFlow id="flow10" name="调整后继续申请" sourceRef="exclusivegateway3" targetRef="deptLeaderVerify">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${reApply == 'true'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow11" name="取消申请,并设置取消标志" sourceRef="exclusivegateway3" targetRef="endevent1">
<extensionElements>
<activiti:executionListener event="take" expression="${execution.setVariable('result', 'canceled')}"></activiti:executionListener>
</extensionElements>
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${reApply == 'false'}]]>
</conditionExpression>
</sequenceFlow>
<textAnnotation id="textannotation1" textFormat="text/plain">
<text>请求被驳回后员工可以选择继续申请,或者取消本次申请</text>
</textAnnotation>
<association id="association1" sourceRef="modifyApply" targetRef="textannotation1"></association>
</process>
leave-start.form
<div class="control-group">
<label class="control-label" for="startDate">开始时间:</label>
<div class="controls">
<input type="text" id="startDate" name="startDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
</div>
</div>
<div class="control-group">
<label class="control-label" for="endDate">结束时间:</label>
<div class="controls">
<input type="text" id="endDate" name="endDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
</div>
</div>
<div class="control-group">
<label class="control-label" for="reason">请假原因:</label>
<div class="controls">
<textarea id="reason" name="reason" required></textarea>
</div>
</div>
approve-deptLeader.form
<div class="control-group">
<label class="control-label" for="startDate">申请人:</label>
<div class="controls">${applyUserId}</div>
</div>
<div class="control-group">
<label class="control-label" for="startDate">开始时间:</label>
<div class="controls">
<input type="text" id="startDate" name="startDate" value="startDate" readonly />
</div>
</div>
<div class="control-group">
<label class="control-label" for="endDate">结束时间:</label>
<div class="controls">
<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
</div>
</div>
<div class="control-group">
<label class="control-label" for="reason">请假原因:</label>
<div class="controls">
<textarea id="reason" name="reason" readonly>${reason}</textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="deptLeaderApproved">审批意见:</label>
<div class="controls">
<select name="deptLeaderApproved" id="deptLeaderApproved">
<option value="true">同意</option>
<option value="false">拒绝</option>
</select>
</div>
</div>
approve-ht.form
<div class="control-group">
<label class="control-label" for="startDate">申请人:</label>
<div class="controls">${applyUserId}</div>
</div>
<div class="control-group">
<label class="control-label" for="startDate">开始时间:</label>
<div class="controls">
<input type="text" id="startDate" name="startDate" value="startDate" readonly />
</div>
</div>
<div class="control-group">
<label class="control-label" for="endDate">结束时间:</label>
<div class="controls">
<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
</div>
</div>
<div class="control-group">
<label class="control-label" for="reason">请假原因:</label>
<div class="controls">
<textarea id="reason" name="reason" readonly>${reason}</textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="hrApproved">审批意见:</label>
<div class="controls">
<select name="hrApproved" id="hrApproved">
<option value="true">同意</option>
<option value="false">拒绝</option>
</select>
</div>
</div>
report-back.form
<div class="control-group">
<label class="control-label" for="startDate">申请人:</label>
<div class="controls">${applyUserId}</div>
</div>
<div class="control-group">
<label class="control-label" for="startDate">开始时间:</label>
<div class="controls">
<input type="text" id="startDate" name="startDate" value="startDate" readonly />
</div>
</div>
<div class="control-group">
<label class="control-label" for="endDate">结束时间:</label>
<div class="controls">
<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
</div>
</div>
<div class="control-group">
<label class="control-label" for="reason">请假原因:</label>
<div class="controls">
<textarea id="reason" name="reason" readonly>${reason}</textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="reportBackDate">销假日期:</label>
<div class="controls">
<input type="text" id="reportBackDate" name="reportBackDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
</div>
</div>
modify-apply.form
<div class="control-group">
<label class="control-label" for="startDate">开始时间:</label>
<div class="controls">
<input type="text" id="startDate" name="startDate" value="${startDate}" class="datepicker" data-date-format="yyyy-mm-dd" required />
</div>
</div>
<div class="control-group">
<label class="control-label" for="endDate">结束时间:</label>
<div class="controls">
<input type="text" id="endDate" name="endDate" value="${endDate}" class="datepicker" data-date-format="yyyy-mm-dd" required />
</div>
</div>
<div class="control-group">
<label class="control-label" for="reason">请假原因:</label>
<div class="controls">
<textarea id="reason" name="reason" required>${reason}</textarea>
</div>
</div>
<div class="control-group">
<label class="control-label" for="reason">是否继续申请:</label>
<div class="controls">
<select id="reApply" name="reApply">
<option value='true'>重新申请</option>
<option value='false'>结束流程</option>
</select>
</div>
</div>
public class BkTest {
@Test
public void test() {
//部署文件
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = processEngine.getRepositoryService();
repositoryService.createDeployment()
.addClasspathResource("bk/leave-formkey.bpmn")
.addClasspathResource("bk/approve-deptLeader.form")
.addClasspathResource("bk/approve-hr.form")
.addClasspathResource("bk/leave-start.form")
.addClasspathResource("bk/modify-apply.form")
.addClasspathResource("bk/report-back.form")
.deploy();
//设置启动用户
IdentityService identityService = processEngine.getIdentityService();
identityService.setAuthenticatedUserId("tom");
//启动流程
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().processDefinitionKey("leave-formkey").singleResult();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Map<String, String> variables = new HashMap<>();
Calendar ca = Calendar.getInstance();
String startDate = sdf.format(ca.getTime());
ca.add(Calendar.DAY_OF_MONTH, 2);//请假的开始、结束日期
String endDate = sdf.format(ca.getTime());
variables.put("startDate", startDate);
variables.put("endDate", endDate);
variables.put("reason", "公休");
FormService formService = processEngine.getFormService();
ProcessInstance pi = formService.submitStartFormData(pd.getId(), variables);
Assert.assertNotNull(pi);
//部门领导审批通过
TaskService taskService = processEngine.getTaskService();
Task deptTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
variables = new HashMap<>();
variables.put("deptLeaderApproved", "true");
formService.submitTaskFormData(deptTask.getId(), variables);
//人事审批通过
Task htTask = taskService.createTaskQuery().taskCandidateGroup("hr").singleResult();
variables = new HashMap<>();
variables.put("hrApproved", "true");
formService.submitTaskFormData(htTask.getId(), variables);
//销假(根据申请人的用户ID读取)
Task reportBackTask = taskService.createTaskQuery().taskAssignee("tom").singleResult();
variables = new HashMap<>();
variables.put("reportBackDate", ca.getTime().toString());//设置销假日期
formService.submitTaskFormData(reportBackTask.getId(), variables);
//验证流程是否已经结束
HistoryService historyService = processEngine.getHistoryService();
HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().finished().singleResult();
Assert.assertNotNull(historicProcessInstance);
//读取历史变量
Map<String, Object> map = packageVariables(pi, historyService);
Assert.assertEquals("ok", map.get("result"));
}
private Map<String, Object> packageVariables(ProcessInstance processInstance, HistoryService historyService) {
Map<String, Object> historyVariables = new HashMap<>();
List<HistoricVariableInstance> list = historyService.createHistoricVariableInstanceQuery().processInstanceId(processInstance.getId()).list();
for (HistoricVariableInstance instance : list) {
historyVariables.put(instance.getVariableName(), instance.getValue());
System.out.println(instance.getVariableName() + "------" + instance.getValue());
}
return historyVariables;
}
}
applyUserId------tom
reason------公休
endDate------2023-09-16
startDate------2023-09-14
deptLeaderApproved------true
hrApproved------true
reportBackDate------Sat Sep 16 22:55:25 CST 2023
result------ok