学习资料:《Activiti实战》
第七章 Spring容器集成应用实例(五)普通表单
第六章中介绍了动态表单、外置表单。这里讲解第三种表单:普通表单。
普通表单的特点:
1 把表单内容写在表现层(JSP、JSF、HTML)文件中 2 一个用户任务对应一个页面 3 业务数据和流程数据分离 4 适用于业务相对固定但复杂、流程相对固定但表现层变化多的情况
因为普通表单中,业务数据和流程数据是分离的,所以存在统一事务管理的问题。要保证Activiti和业务数据操作在同一个事务中执行。前面集成Spring时的事务管理器的配置可表明这点。本节基于第六章的主要针请假流程,对数据和表单都分离的情况下,采用普通表单实现该功能。
本节采用Spring、SpringMVC和Hibernete。
7.5.1 业务建模
(1)表结构
(2)其他
DAO和Manger用来针对请假实体的CRUD,Leave-workflowService用来处理流程相关操作。
7.5.2 启动流程
(1)部署流程
在启动流程之前,先部署流程。
第六章已经实现过这个页面,不再提。总之,点击浏览,选择文件,然后submit,将chapter7/leave.bpmn和leave.png打包部署。部署完毕之后,在该列表页面中,会出现一个新的processDefinition记录。
(2)jsp
普通表单使用时,需要将表单内容保存在一个单独的文件中。这里采用jsp文件格式。
表单设计如下:
1 <form action="${ctx }/chapter7/leave/start" class="form-horizontal" method="post" onsubmit="beforeSend()"> 2 <input type="hidden" name="startTime" /> 3 <input type="hidden" name="endTime" /> 4 <fieldset> 5 <legend><small>请假申请</small></legend> 6 <div id="messageBox" class="alert alert-error input-large controls" style="display:none">输入有误,请先更正。</div> 7 <div class="control-group"> 8 <label for="loginName" class="control-label">请假类型:</label> 9 <div class="controls"> 10 <select id="leaveType" name="leaveType" class="required"> 11 <option>公休</option> 12 <option>病假</option> 13 <option>调休</option> 14 <option>事假</option> 15 <option>婚假</option> 16 </select> 17 </div> 18 </div> 19 <div class="control-group"> 20 <label for="name" class="control-label">开始时间:</label> 21 <div class="controls"> 22 <input type="text" id="startDate" class="datepicker input-small" data-date-format="yyyy-mm-dd" /> 23 <input type="text" id="startTime" class="time input-small" /> 24 </div> 25 </div> 26 <div class="control-group"> 27 <label for="plainPassword" class="control-label">结束时间:</label> 28 <div class="controls"> 29 <input type="text" id="endDate" class="datepicker input-small" data-date-format="yyyy-mm-dd" /> 30 <input type="text" id="endTime" class="time input-small" /> 31 </div> 32 </div> 33 <div class="control-group"> 34 <label for="groupList" class="control-label">请假原因:</label> 35 <div class="controls"> 36 <textarea name="reason"></textarea> 37 </div> 38 </div> 39 <div class="form-actions"> 40 <button type="submit" class="btn"><i class="icon-play"></i>启动流程</button> 41 </div> 42 </fieldset> 43 </form>
webapp/WEB-INF/views/chapter7/leave/leaveApply.jsp如下:
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 <!DOCTYPE html> 3 <html> 4 <head> 5 <%@ include file="/common/global.jsp"%> 6 <%@ include file="/common/meta.jsp" %> 7 <%@ include file="/common/include-base-styles.jsp" %> 8 <link rel="stylesheet" href="${ctx}/js/common/plugins/timepicker.css"> 9 <title>请假申请</title> 10 <script type="text/javascript" src="${ctx }/js/common/jquery.js"></script> 11 <script type="text/javascript" src="${ctx }/js/common/bootstrap.min.js"></script> 12 <script type="text/javascript" src="${ctx }/js/common/bootstrap-datepicker.js"></script> 13 <script type="text/javascript" src="${ctx }/js/common/plugins/bootstrap-timepicker.js"></script> 14 <script type="text/javascript"> 15 $(function() { 16 $('.datepicker').datepicker(); 17 $('.time').timepicker({ 18 minuteStep: 10, 19 showMeridian: false 20 }); 21 }); 22 23 function beforeSend() { 24 $('input[name=startTime]').val($('#startDate').val() + ' ' + $('#startTime').val()); 25 $('input[name=endTime]').val($('#endDate').val() + ' ' + $('#endTime').val()); 26 } 27 </script> 28 </head> 29 <body> 30 <c:if test="${not empty message}"> 31 <div id="message" class="alert alert-success">${message}</div> 32 <!-- 自动隐藏提示信息 --> 33 <script type="text/javascript"> 34 setTimeout(function() { 35 $('#message').hide('slow'); 36 }, 5000); 37 </script> 38 </c:if> 39 <form> 40 <!--表单的代码放在这里--> 41 </form> 42 </body> 43 </html>
(3)Controller
在上面的流程定义列表页面中,增加一个字段"操作",里面包含两个button,一个是启动,一个是删除。
当点击"启动时",应该要显示表单,并且点击申请时,流程启动。
1 @Controller 2 @RequestMapping(value = "/chapter7/leave") 3 public class LeaveController { 4 5 private Logger logger = LoggerFactory.getLogger(getClass()); 6 7 @Autowired 8 private LeaveManager leaveManager; 9 10 @Autowired 11 private LeaveWorkflowService leaveService; 12 13 @Autowired 14 private TaskService taskService; 15 16 @Autowired 17 private RuntimeService runtimeService; 18 19 @RequestMapping(value = {"apply", ""}) 20 public String createForm(Model model) { 21 model.addAttribute("leave", new Leave()); 22 return "/chapter7/leave/leave-apply"; 23 } 24 25 /** 26 * 启动请假流程 27 */ 28 @RequestMapping(value = "start", method = RequestMethod.POST) 29 public String startWorkflow(Leave leave, RedirectAttributes redirectAttributes, HttpSession session) { 30 try { 31 User user = UserUtil.getUserFromSession(session); 32 Map<String, Object> variables = new HashMap<String, Object>(); 33 ProcessInstance processInstance = leaveService.startWorkflow(leave, user.getId(), variables); 34 redirectAttributes.addFlashAttribute("message", "流程已启动,流程ID:" + processInstance.getId()); 35 } catch (ActivitiException e) { 36 if (e.getMessage().indexOf("no processes deployed with key") != -1) { 37 logger.warn("没有部署流程!", e); 38 redirectAttributes.addFlashAttribute("error", "没有部署请假流程"); 39 } else { 40 logger.error("启动请假流程失败:", e); 41 redirectAttributes.addFlashAttribute("error", "系统内部错误!"); 42 } 43 } catch (Exception e) { 44 logger.error("启动请假流程失败:", e); 45 redirectAttributes.addFlashAttribute("error", "系统内部错误!"); 46 } 47 return "redirect:/chapter7/leave/apply"; 48 } 49 50 ... 51 }
(4)Service
前面提过,这里要实现业务和流程数据的分离。即业务数据存在一个表里,流程数据存在另一个表。但是又要将二者联系起来。
这里采取的办法是:
1 业务表:增加一个字段process_instance_id,方便从业务层面查询流程数据。
2 流程表:用entity的ID作为processDefinitionKey,方便从流程层面查询业务数据。
可以从代码中看出来,这里不只是单纯的启动流程。还进行了数据的存储与关联。
1 @Service 2 @Transactional 3 public class LeaveWorkflowService { 4 5 private Logger logger = LoggerFactory.getLogger(getClass()); 6 7 @Autowired 8 LeaveManager leaveManager; 9 10 @Autowired 11 private IdentityService identityService; 12 13 @Autowired 14 private RuntimeService runtimeService; 15 16 @Autowired 17 private TaskService taskService; 18 19 @Autowired 20 private RepositoryService repositoryService; 21 22 /** 23 * 保存请假实体并启动流程 24 */ 25 public ProcessInstance startWorkflow(Leave entity, String userId, Map<String, Object> variables) { 26 if (entity.getId() == null) { 27 entity.setApplyTime(new Date()); 28 entity.setUserId(userId); 29 } 30 leaveManager.save(entity);//持久化请假实体 31 String businessKey = entity.getId().toString();//实体保存后的ID,作为流程中的业务key 32 33 // 用来设置启动流程的人员ID,引擎会自动把用户ID保存到activiti:initiator中 34 identityService.setAuthenticatedUserId(userId); 35 36 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave", businessKey, variables);//将业务主ID设置为流程实例的key 37 String processInstanceId = processInstance.getId(); 38 entity.setProcessInstanceId(processInstanceId);// 将流程实例的ID保存至业务表 39 logger.debug("start process of {key={}, bkey={}, pid={}, variables={}}", new Object[]{"leave", businessKey, processInstanceId, variables}); 40 leaveManager.save(entity); 41 return processInstance; 42 } 43 44 ... 45 }
7.5.3 任务读取