spring boot2集成activiti6實現工作流


前言

最近項目中需要用到工作流審批流程,業務功能比較簡單,就是員工請假,領導審批同意或者駁回的操作。本來准備自己做一套簡單的審批流程(數據庫記錄下狀態的這種),但是考慮到后期的拓展性,可能會有多審批、加簽等復雜的操作,還是決定使用工作流框架,最后選擇了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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM