前言
最近項目中需要用到工作流審批流程,業務功能比較簡單,就是員工請假,領導審批同意或者駁回的操作。本來准備自己做一套簡單的審批流程(數據庫記錄下狀態的這種),但是考慮到后期的拓展性,可能會有多審批、加簽等復雜的操作,還是決定使用工作流框架,最后選擇了Activiti。
簡介
Activiti是一種輕量級,可嵌入的BPM引擎,而且還設計適用於可擴展的雲架構。可以和springboot完美結合。
首先需要了解它的7 個服務接口和28張表:
服務接口 | 說明 |
RepositoryService | 倉庫服務,用於管理倉庫,比如部署或刪除流程定義、讀取流程資源等。 |
IdentifyService | 身份服務,管理用戶、組以及它們之間的關系。 |
RuntimeService | 運行時服務,管理所有正在運行的流程實例、任務等對象。 |
TaskService | 任務服務,管理任務。 |
FormService | 表單服務,管理和流程、任務相關的表單。 |
HistroyService | 歷史服務,管理歷史數據。 |
ManagementService | 引擎管理服務,比如管理引擎的配置、數據庫和作業等核心對象 |
表 | 說明 |
ACT_RE_* | RE’表示repository。 這個前綴的表包含了流程定義和流程靜態資源 (圖片,規則,等等) |
ACT_RU_* | RU’表示runtime。這些運行時的表,包含流程實例,任務,變量,異步任務,等運行中的數據。 Activiti只在流程實例執行過程中保存這些數據,在流程結束時就會刪除這些記錄。 這樣運行時表可以一直很小速度很快。 |
ACT_ID_* | ‘ID’表示identity。 這些表包含身份信息,比如用戶,組等等。 |
ACT_HI_* | ‘HI’表示history。 這些表包含歷史數據,比如歷史流程實例, 變量,任務等等。 |
ACT_GE_* | 通用數據, 用於不同場景下,如存放資源文件。 |
表結構詳細介紹:https://blog.csdn.net/qq_38011415/article/details/101127222
准備
首先安裝下可視化插件,我的用是Idea開發,在插件中心安裝actiBPM插件:
這樣打開BPM文案可以以圖形化的方式查看流程:
如果流程圖出現中文亂碼,請移步:https://blog.csdn.net/amandalm/article/details/81196710
整合
一、增加依賴
<!--activiti--> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter-basic</artifactId> <version>6.0.0</version> <exclusions> <exclusion> <artifactId>mybatis</artifactId> <groupId>org.mybatis</groupId> </exclusion> </exclusions> </dependency>
避坑:https://blog.csdn.net/HXNLYW/article/details/103694280
二、增加配置信息
自定義數據源
spring: # 數據庫配置 datasource: type: com.alibaba.druid.pool.DruidDataSource dynamic: primary: master #設置默認的數據源或者數據源組,默認值即為master datasource: master: url: xxx driver-class-name: com.mysql.cj.jdbc.Driver username: root password: xxx activiti: url: xxx driver-class-name: com.mysql.cj.jdbc.Driver username: root password: xxx # 工作流 activiti: # 自動部署驗證設置: # true(默認)自動部署流程 # false 不自動部署,需要手動部署發布流程 check-process-definitions: true # 可選值為: false,true,create-drop,drop-create # 默認為true。為true表示activiti會對數據庫中的表進行更新操作,如果不存在,則進行創建。 database-schema-update: true
在啟動類增加如下內容
@Bean @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.activiti") public DataSource activitiDataSource() { return new DruidDataSource(); } @Bean public SpringProcessEngineConfiguration springProcessEngineConfiguration( PlatformTransactionManager transactionManager, SpringAsyncExecutor springAsyncExecutor) throws IOException { return baseSpringProcessEngineConfiguration( activitiDataSource(), transactionManager, springAsyncExecutor); }
三、排除安全配置類
@SpringBootApplication(exclude={ org.activiti.spring.boot.SecurityAutoConfiguration.class })
如果不排除這個配置類會報如下錯誤:
Caused by: java.io.FileNotFoundException: class path resource [org/springframework/security/config/annotation/authentication/configurers/GlobalAuthenticationConfigurerAdapter.class] cannot be opened because it does not exist
at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:180)
at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:51)
四、新增bpm流程文件
如果spring.activiti.check-process-definitions配置設置為true,需要在resource目錄下新建processes文件夾(activiti默認訪問此文件下的流程文件),並新增bpm流程文件。
否則會報流程文件找不到:
Caused by: java.io.FileNotFoundException: class path resource [processes/] cannot be resolved to URL because it does not exist
at org.springframework.core.io.ClassPathResource.getURL(ClassPathResource.java:195)
到此,配置就已經完成了,啟動項目,會自動生成28張表。
案例
本文以常見的請假流程為案例實現一個簡單的請假審批流程。
service層 (邏輯封裝)
/** * @author gourd * @description 工作流服務 * @date 2018/10/30 11:25 **/ @Service @Slf4j @Transactional(rollbackFor = Exception.class) @DS(value = "activiti") public class WorkFlowServiceImpl implements WorkFlowService { @Autowired private RepositoryService repositoryService; @Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; @Autowired private HistoryService historyService; @Autowired private ProcessEngine processEngine; public static final String DEAL_USER_ID_KEY = "dealUserId"; public static final String DELEGATE_STATE = "PENDING"; /** * 啟動工作流 * * @param pdKey * @param businessKey * @param variables * @return */ @Override public String startWorkflow(String pdKey, String businessKey, Map<String,Object> variables) { ProcessDefinition processDef = getLatestProcDef(pdKey); if (processDef == null) { // 部署流程 processEngine.getRepositoryService() .createDeployment()//創建部署對象 .name(pdKey) .addClasspathResource("processes/"+pdKey+".bpmn") .deploy(); processDef = getLatestProcDef(pdKey); } ProcessInstance process = runtimeService.startProcessInstanceById(processDef.getId(), businessKey, variables); return process.getId(); } /** * 繼續流程 * * @param taskId * @param variables */ @Override public void continueWorkflow(String taskId, Map variables){ //根據taskId提取任務 Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); DelegationState delegationState = task.getDelegationState(); if(delegationState != null && DELEGATE_STATE.equals(delegationState.toString())){ // 委托任務,先需要被委派人處理完成任務 taskService.resolveTask(taskId,variables); }else { // 當前受理人 String dealUserId =variables.get(DEAL_USER_ID_KEY).toString(); // 簽收 taskService.claim(taskId, dealUserId); } // 設置參數 taskService.setVariables(taskId, variables); // 完成 taskService.complete(taskId); } /** * 委托流程 * @param taskId * @param variables */ @Override public void delegateWorkflow(String taskId, Map variables){ // 受委托人 String dealUserId =variables.get(DEAL_USER_ID_KEY).toString(); // 委托 taskService.delegateTask(taskId, dealUserId); } /** * 結束流程 * @param pProcessInstanceId */ @Override public void endWorkflow(String pProcessInstanceId,String deleteReason){ // 結束流程 runtimeService.deleteProcessInstance(pProcessInstanceId, deleteReason); } /** * 獲取當前的任務節點 * @param pProcessInstanceId */ @Override public String getCurrentTask(String pProcessInstanceId){ Task task = taskService.createTaskQuery().processInstanceId(pProcessInstanceId).active().singleResult(); return task.getId(); } /** * * 根據用戶id查詢待辦流程實例ID集合 * */ @Override public List<String> findUserProcessIds(String userId, String pdKey, Integer pageNo, Integer pageSize) { List<Task> resultTask; if(pageSize == 0 ){ // 不分頁 resultTask = taskService.createTaskQuery().processDefinitionKey(pdKey) .taskCandidateOrAssigned(userId).list(); }else { resultTask = taskService.createTaskQuery().processDefinitionKey(pdKey) .taskCandidateOrAssigned(userId).listPage(pageNo-1,pageSize); } //根據流程實例ID集合 List<String> processInstanceIds = resultTask.stream() .map(task -> task.getProcessInstanceId()) .collect(Collectors.toList()); return processInstanceIds == null ? new ArrayList<>() : processInstanceIds; } /** * 獲取流程圖像,已執行節點和流程線高亮顯示 */ @Override public void getProcessImage(String pProcessInstanceId, HttpServletResponse response) { log.info("[開始]-獲取流程圖圖像"); // 設置頁面不緩存 response.setHeader("Pragma", "No-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); response.setContentType("image/png"); InputStream imageStream = null; try (OutputStream os = response.getOutputStream()){ // 獲取歷史流程實例 HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery() .processInstanceId(pProcessInstanceId).singleResult(); if (historicProcessInstance == null) { throw new ServiceException("獲取流程實例ID[" + pProcessInstanceId + "]對應的歷史流程實例失敗!"); } else { // 獲取流程歷史中已執行節點,並按照節點在流程中執行先后順序排序 List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery() .processInstanceId(pProcessInstanceId).orderByHistoricActivityInstanceId().asc().list(); // 已執行的節點ID集合 List<String> executedActivityIdList = new ArrayList<String>(); int index = 1; log.info("獲取已經執行的節點ID"); for (HistoricActivityInstance activityInstance : historicActivityInstanceList) { executedActivityIdList.add(activityInstance.getActivityId()); log.info("第[" + index + "]個已執行節點=" + activityInstance.getActivityId() + " : " +activityInstance.getActivityName()); index++; } // 獲取流程定義 BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId()); // 已執行的線集合 List<String> flowIds = getHighLightedFlows(bpmnModel, historicActivityInstanceList); // 流程圖生成器 ProcessDiagramGenerator pec = processEngine.getProcessEngineConfiguration().getProcessDiagramGenerator(); // 獲取流程圖圖像字符流(png/jpg) imageStream = pec.generateDiagram(bpmnModel, "jpg", executedActivityIdList, flowIds, "宋體", "微軟雅黑", "黑體", null, 2.0); int bytesRead = 0; byte[] buffer = new byte[8192]; while ((bytesRead = imageStream.read(buffer, 0, 8192)) != -1) { os.write(buffer, 0, bytesRead); } } log.info("[完成]-獲取流程圖圖像"); } catch (Exception e) { log.error("【異常】-獲取流程圖失敗!",e); }finally { if(imageStream != null){ try { imageStream.close(); } catch (IOException e) { log.error("關閉流異常:",e); } } } } public List<String> getHighLightedFlows(BpmnModel bpmnModel, List<HistoricActivityInstance> historicActivityInstances) { // 24小時制 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 用以保存高亮的線flowId List<String> highFlows = new ArrayList<String>(); for (int i = 0; i < historicActivityInstances.size() - 1; i++) { // 對歷史流程節點進行遍歷 // 得到節點定義的詳細信息 FlowNode activityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(i).getActivityId()); // 用以保存后續開始時間相同的節點 List<FlowNode> sameStartTimeNodes = new ArrayList<FlowNode>(); FlowNode sameActivityImpl1 = null; // 第一個節點 HistoricActivityInstance activityImpl_ = historicActivityInstances.get(i); HistoricActivityInstance activityImp2_; for (int k = i + 1; k <= historicActivityInstances.size() - 1; k++) { // 后續第1個節點 activityImp2_ = historicActivityInstances.get(k); if (activityImpl_.getActivityType().equals("userTask") && activityImp2_.getActivityType().equals("userTask") && df.format(activityImpl_.getStartTime()).equals(df.format(activityImp2_.getStartTime()))) { // 都是usertask,且主節點與后續節點的開始時間相同,說明不是真實的后繼節點 } else { //找到緊跟在后面的一個節點 sameActivityImpl1 = (FlowNode) bpmnModel.getMainProcess().getFlowElement(historicActivityInstances.get(k).getActivityId()); break; } } // 將后面第一個節點放在時間相同節點的集合里 sameStartTimeNodes.add(sameActivityImpl1); for (int j = i + 1; j < historicActivityInstances.size() - 1; j++) { // 后續第一個節點 HistoricActivityInstance activityImpl1 = historicActivityInstances.get(j); // 后續第二個節點 HistoricActivityInstance activityImpl2 = historicActivityInstances.get(j + 1); if (df.format(activityImpl1.getStartTime()).equals(df.format(activityImpl2.getStartTime()))) { // 如果第一個節點和第二個節點開始時間相同保存 FlowNode sameActivityImpl2 = (FlowNode) bpmnModel.getMainProcess().getFlowElement(activityImpl2.getActivityId()); sameStartTimeNodes.add(sameActivityImpl2); } else {// 有不相同跳出循環 break; } } // 取出節點的所有出去的線 List<SequenceFlow> pvmTransitions = activityImpl.getOutgoingFlows(); // 對所有的線進行遍歷 for (SequenceFlow pvmTransition : pvmTransitions) { // 如果取出的線的目標節點存在時間相同的節點里,保存該線的id,進行高亮顯示 FlowNode pvmActivityImpl = (FlowNode) bpmnModel.getMainProcess().getFlowElement(pvmTransition.getTargetRef()); if (sameStartTimeNodes.contains(pvmActivityImpl)) { highFlows.add(pvmTransition.getId()); } } } return highFlows; } /** * 獲取最新版本流程 * * @param modelName * @return */ private ProcessDefinition getLatestProcDef(String modelName) { return repositoryService.createProcessDefinitionQuery().processDefinitionKey(modelName). latestVersion().singleResult(); } }
controller層調用(個人喜歡用swagger調試)
/** * @author gourd */ @RestController @Api(tags = "activiti",description = "工作流控制器") @RequestMapping("/activiti") @Slf4j public class ActivitiController { @Autowired private WorkFlowService workFlowService; @PostMapping("/qj-apply") @ApiOperation(value="啟動請假流程") public BaseResponse startWorkflow(@RequestParam(required = false) String pdKey){ Map param = new HashMap(4){{ put("applyUserId","001"); put("approveUserIds", Arrays.asList("001","002","003")); }}; if(StringUtils.isBlank(pdKey)){ pdKey="QjFlow"; } // 啟動流程 String pdId = workFlowService.startWorkflow(pdKey, "QJ001", param); // 獲取請假申請任務節點 String Id = workFlowService.getCurrentTask(pdId); // 完成請假申請任務節點 Map continueParam = new HashMap(2){{ put("dealUserId",param.get("applyUserId")); }}; workFlowService.continueWorkflow(Id,continueParam); return BaseResponse.ok("請假已提交"); } @PostMapping("/qj-approve") @ApiOperation(value="審批請假流程") public BaseResponse continueWorkflow(@RequestParam String pId,@RequestParam String result){ Map param = new HashMap(2){{ put("dealUserId","001"); put("result",result); }}; // 獲取請假審批任務節點 String Id = workFlowService.getCurrentTask(pId); // 完成請假審批任務節點 workFlowService.continueWorkflow(Id,param); return BaseResponse.ok("審批成功"); } @PostMapping("/qj-delegate") @ApiOperation(value="委托請假流程") public BaseResponse delegateWorkflow(@RequestParam String pId,@RequestParam String userId){ Map param = new HashMap(2){{ put("dealUserId",userId); }}; // 獲取請假審批任務節點 String Id = workFlowService.getCurrentTask(pId); // 完成請假審批任務節點 workFlowService.delegateWorkflow(Id,param); return BaseResponse.ok("委托成功"); } /** * 查詢用戶待辦流程實例 * @param userId * @param pdKey */ @GetMapping("/user-process") @ApiOperation(value="查詢用戶待辦流程實例") public BaseResponse findUserProcessIds(@RequestParam String userId, @RequestParam(required = false) String pdKey) { if(StringUtils.isBlank(pdKey)){ pdKey="QjFlow"; } // 獲取流程圖 return BaseResponse.ok(workFlowService.findUserProcessIds(userId,pdKey,1,0)); } /** * 讀取流程資源 * @param pId 流程實例id */ @GetMapping("/read-resource") @ApiOperation(value="讀取流程資源") public void readResource(@RequestParam String pId) { // 獲取流程圖 workFlowService.getProcessImage(pId); }
測試截圖:
swagger接口文檔:
獲取流程實例圖:http://localhost:8088/gourd/activiti/read-resource?pId=57505
結尾
以上就是springboot整合activiti的所有配置和代碼,代碼均已上傳至我的開源項目:gourd-hu;有興趣的小伙伴可以下載看下,里面整合了許多開發常用的框架和功能。同時本文是我平時學習和工作的一個記錄和總結,如有不對的地方,歡迎指正。
spring-cloud-plus:https://blog.csdn.net/HXNLYW/article/details/104635673