弃坑入门:Activiti 7.1.0-M13+SpringBoot 2.5.0 开发环境搭建

由于Activiti项目团队自身的一些问题,新版本迭代很快,但功能基本上还是基于原来的Activiti 5.x/6.x,加上项目质量管控多少问题,新版本的bug多修复慢。

国内很多activiti的用户还停留在5.x/6.x的阶段,故网上各种教材文章的内容也如此,对Activiti 7.x的资料有点无面,基本上都是就一两个点的记录。

本测试将以尽量最新的环境版本来搭建,记录下备查也供有需之人参考。

1.开发环境

  • MacOS :11.3.1 ,JAVA环境,windows/linux应该也无大差异
  • jdk :Amazon Corretto 11 ,jdk其他发行版应该也无差异
  • maven : 3.8.1 , 最好修改阿里仓库镜像
  • IDE : idea 2021.1.1 ,其他版本或者顺手的IDE也行

2.activiti版本问题

  • activiti的源自jBPM,分支版本有flowable、camunda等

  • 主要经历了三个大版本,分别是 5.x 6.x 7.x。目前(2021-05-21)的最新版本如下:

  • 在maven中央仓库中有的最新版本是7.1.0.M6
    https://mvnrepository.com/artifact/org.activiti/activiti-spring-boot-starter/7.1.0.M6

  • 官网最新版本是7.1.0-M12
    https://activiti.gitbook.io/activiti-7-developers-guide/releases/7.1.0-m12

  • github最新版本是7.1.0-M13
    https://github.com/Activiti/Activiti/releases/tag/7.1.0-M13 要在项目中用的话clone下来make install,本测试就尝试最新的。

    注意后两个版本M前面是短杆不是点,别眼残

3.activiti基本概念

  • bpmn 2.0 概念问度娘;

  • 流程定义文件 ProcessDefinition File
    就是在bpmn2.0规范下用工具建立的流程模板文件,一般是".bpmn"或者".bpmn20.xml",本质上就是个xml文件。

  • 流程定义文件常见编辑工具:
    a. Eclipse+插件,插件地址 http://www.activiti.org/designer/update ,现在可用没细测;

    b. idea早期版本(2020.1之前?)+插件(actibpm.jar),插件很久没维护,有bug新版的idea不可用;

    c. bpmnjs.io,https://bpmn.io/toolkit/bpmn-js/ 可在线编辑,但默认生成的camunda格式流程定义文件与activiti不兼容;

    d.基于bpmnjs修改后的工具之一:https://github.com/activiti/activiti-modeling-app ,没细研究

    e.基于bpmnjs修改后的工具之二:activiti cloud modeler,下面详细介绍。

  • 部署 Deploy
    简单讲就是把流程定义文件加载的数据库中,将xml文件存入数据库中,并解析到相关的数据表中。一次部署操作会在activiti的数据库中保留一条部署记录。

  • 流程定义 ProcessDefinition
    流程定义的xml文件加载到activiti解析后插入table中的流程定义内容,一般一次一个流程定义文件部署会生成一条流程定义记录,一次部署一个zip若有多个定义文件就是多条记录。

    部署与流程定义中的一条记录是逻辑概念,实际上设计到一系列Table记录。

  • 流程实例 ProcessInstance
    就是从特定流程定义记录复制出来的一组记录,可以类比理解为java的类创建的对象。

  • 任务 Task
    通俗点说一个流程中的一个需要处理的节点成为一个任务。

  • 网关与事件暂不展开

4.activiti cloud modeler环境准备

参考:https://activiti.gitbook.io/activiti-7-developers-guide/getting-started/getting-started-activiti-cloud/getting-started-docker-compose

  • 安装docker以及docker-compose,安装最新好了,具体操作简单,略
  • 获得activiti-cloud-examples的docker-compose文件
cd ~
git clone https://github.com/Activiti/activiti-cloud-examples
cd activiti-cloud-examples/docker-compose

无git就安装git或者到 https://github.com/Activiti/activiti-cloud-examples 下载zip放到一个地方解压

  • 修改隐藏文件 .evn
    把 DOCKER_IP 设成Docker本机IP,不能用localhost或者127.0.0.1

  • 拉取并启动 activiti cloud modeler

make modeler
  • 查看logs
make logs
  • 查看结果
make ps
或
docker-compose ps

注意state应该都是 up 就没问题了,注意别跟自己之前的docker镜像混淆,实在不行就找台干净的机器(虚拟机4C8G也行)安装一个docker

  • 打开浏览器,打开
http://$DOCKER_IP/modeling

DOCKER_IP就是刚才.env 中设置的ip

账号/密码:modeler / password

  • 登录后就可以新建项目、流程定义模型,可保存服务器,也可下载到本地

5.springboot 2.5.0+Activiti 7.1.0-M13开发环境

  • activiti 7.1.0-M13编译并安装到本地库
git  clone https://github.com/Activiti/Activiti.git
cd Activiti
git checkout 7.1.0-M13
mvn -T 1C clean install -Dmaven.test.skip=true

上个厕所喝喝水撩撩妹…好了

  • pom.xml
    关键是引入activiti-spring-boot-starter,与其他springboot项目无大差异,依赖一切从简,与activiti核心内容无关的东西比方数据、连接池、丝袜哥…以后再说.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.0version>
        <relativePath/> 
    parent>
    <groupId>com.examplegroupId>
    <artifactId>M13-demoartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>M13-demoname>
    <description>Demo project for Spring Bootdescription>
    <properties>
        <java.version>11java.version>
    properties>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.activitigroupId>
                <artifactId>activiti-dependenciesartifactId>
                <version>7.1.0-M13version>
                <scope>importscope>
                <type>pomtype>
            dependency>
        dependencies>
    dependencyManagement>
    <dependencies>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.activitigroupId>
            <artifactId>activiti-spring-boot-starterartifactId>
        dependency>

        <dependency>
            <groupId>com.h2databasegroupId>
            <artifactId>h2artifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

  • application.yml
    默认用的是H2数据库,把history相关的配置打开。
spring:
  activiti:
    database-schema-update: true
    history-level: full
    db-history-used: true
  • 建立bpmn文件
    不会画,就把一下内容存成askleave.bpmn保存在src/main/resources/processes目录下,这个目录下的流程定义文件会自动部署;网上旧教程说还要一个同名的图片文件(svg/gif/png等)非必须。


  
    
      Flow_0tygt5u
    
    
    
    
      Flow_0kpr5l9
    
    
    
    
      
        
      
      Flow_0tygt5u
      Flow_0ignfmw
    
    
      
        
      
      Flow_0ignfmw
      Flow_18fmv1a
    
    
      
        
      
      Flow_0bhmsuw
      Flow_0o85xnk
      Flow_0kpr5l9
    
    
      
        
      
      Flow_0hgmw2q
      Flow_0bhmsuw
    
    
      Flow_18fmv1a
      Flow_0hgmw2q
      Flow_0o85xnk
    
    
    
      ${days>3}
    
    
      ${days<=3}
    
  
  
    
      
        
        
      
      
        
        
        
      
      
        
        
      
      
        
        
        
      
      
        
        
      
      
        
        
      
      
        
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
      
      
        
        
          
        
      
    
  


  • Springboot Security相关简化配置
    Activiti新API以及complete相关操作需要鉴权,Activiti默认集成了Springboot Security
    DemoSecurityConfiguration.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;

@Configuration
public class DemoSecurityConfiguration {

    private Logger logger = LoggerFactory.getLogger(DemoSecurityConfiguration.class);

    @Bean
    public UserDetailsService myUserDetailsService() {

        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();

        String[][] usersGroupsAndRoles = {
                {"user1", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"user2", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"user3", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"user4", "password", "ROLE_ACTIVITI_USER", "GROUP_activitiTeam"},
                {"user5", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
                {"user6", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
                {"user7", "password", "ROLE_ACTIVITI_USER", "GROUP_otherTeam"},
                {"system", "password", "ROLE_ACTIVITI_USER"},
                {"admin", "password", "ROLE_ACTIVITI_ADMIN"},
        };

        for (String[] user : usersGroupsAndRoles) {
            List<String> authoritiesStrings = asList(Arrays.copyOfRange(user, 2, user.length));
            logger.info("> Registering new user: " + user[0] + " with the following Authorities[" + authoritiesStrings + "]");
            inMemoryUserDetailsManager.createUser(new User(user[0], passwordEncoder().encode(user[1]),
                    authoritiesStrings.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList())));
        }

        return inMemoryUserDetailsManager;
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

SecurityUtil.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import java.util.Collection;

@Component
public class SecurityUtil {

    private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    @Autowired
    private UserDetailsService userDetailsService;

    public void logInAs(String username) {

        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {
            throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
        }
        logger.info("> Logged in as: " + username);
        SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return user.getAuthorities();
            }

            @Override
            public Object getCredentials() {
                return user.getPassword();
            }

            @Override
            public Object getDetails() {
                return user;
            }

            @Override
            public Object getPrincipal() {
                return user;
            }

            @Override
            public boolean isAuthenticated() {
                return true;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {

            }

            @Override
            public String getName() {
                return user.getUsername();
            }
        }));
        org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
    }
}

  • 监听器类
    本流程定义文件每个task都加了任务监听器
    DefaultTaskListener.java
import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultTaskListener implements TaskListener {
    private Logger logger = LoggerFactory.getLogger(DefaultTaskListener.class);

    @Override
    public void notify(DelegateTask delegateTask) {
        logger.info("任务监听器-流程实例ID: " + delegateTask.getProcessInstanceId()
                + " 执行人: " + delegateTask.getAssignee());
    }

}

  • 完整测试用例–Activiti 6.x的写法
import org.activiti.engine.*;
import org.activiti.engine.history.HistoricTaskInstance;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SpringBootTest
public class AskLeaveTest_6x_full {
    private Logger logger = LoggerFactory.getLogger(AskLeaveTest_6x_full.class);

    @Autowired
    TaskService taskService;
    @Autowired
    HistoryService historyService;
    @Autowired
    RepositoryService repositoryService;
    @Autowired
    RuntimeService runtimeService;
    @Autowired
    ManagementService managementService;
    @Autowired
    SecurityUtil securityUtil;

    @Test
    @Tag("test")
    public void askLeaveTest() {
        // 外系统的关联单号
        String businessKey = "ASK00001";
        //流程定义的key
        String processDefinitionKey = "askleave_0521";

        String days = "4";
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("days", days);

        //启动一个流程实例
        ProcessInstance processInstance = runtimeService
                .startProcessInstanceByKey(processDefinitionKey, businessKey,variables);
        logger.info(processInstance + " 流程启动成功");

        //user1 查找任务
        days = "2";
        String assignee = "user1";
        securityUtil.logInAs(assignee);
        Task task = taskService
                .createTaskQuery()
                .processDefinitionKey(processDefinitionKey)
                .taskAssignee(assignee)
                .list().get(0);
        //user1 处理任务,即填写请假单,把天数变量赋值 >3 天总监要加签
        variables.put("days", days);
        taskService.complete(task.getId(), variables); //set variables有效 !!
        logger.info(task.getId() + " 请假单填写完成,请教天数:" + days);

        //主管 查找并处理任务
        assignee = "user2";
        securityUtil.logInAs(assignee);
        List<Task> taskList = taskService
                .createTaskQuery()
                .processInstanceId(processInstance.getId()) //指定实例ID
                .taskAssignee(assignee) //指定用户
                .list();
        if (taskList.size() > 0) {
            task = taskList.get(0);
            taskService.complete(task.getId(), variables);
            logger.info(task.getId() + " 主管审批完成");
        } else {
            logger.info("主管无任务可审核");
        }

        //总监 查找并处理任务 判断是否需要加签
        assignee = "user4";
        securityUtil.logInAs(assignee);
        taskList = taskService
                .createTaskQuery()
                .processInstanceId(processInstance.getId()) //指定实例ID
                .taskAssignee(assignee) //指定用户
                .list();
        if (taskList.size() > 0) {
            task = taskList.get(0);
            taskService.complete(task.getId(), variables);
            logger.info(task.getId() + " 总监加签完成");
        } else {
            logger.info("总监无任务可加签");
        }

        //人事 查找并处理任务 归档
        assignee = "user3";
        securityUtil.logInAs(assignee);
        taskList = taskService
                .createTaskQuery()
                .processInstanceId(processInstance.getId()) //指定实例ID
                .taskAssignee(assignee) //指定用户
                .list();
        if (taskList.size() > 0) {
            task = taskList.get(0);
            taskService.complete(task.getId(), variables);
            logger.info(task.getId() + " 人事归档完成");
        } else {
            logger.info("人事无任务可归档");
        }

        //查看历史记录
        String processInstanceId = processInstance.getId();
        List<HistoricTaskInstance> HisList = historyService
                .createHistoricTaskInstanceQuery()
                .orderByHistoricTaskInstanceEndTime().desc()
                .processInstanceId(processInstanceId) //流程实例ID条件
                .list();
        logger.info("");
        for (HistoricTaskInstance hi : HisList) {
            logger.info("=============================================================");
            logger.info("getId                 :" + hi.getId());
            logger.info("getProcessDefinitionId:" + hi.getProcessDefinitionId());
            logger.info("getProcessInstanceId  :" + hi.getProcessInstanceId());
            logger.info("getName               :" + hi.getName());
            logger.info("getStartTime          :" + hi.getStartTime());
            logger.info("getEndTime            :" + hi.getEndTime());
        }
    }


}


  • 完整测试用例–Activiti 7.x的写法
import org.activiti.api.process.model.ProcessInstance;
import org.activiti.api.process.model.builders.ProcessPayloadBuilder;
import org.activiti.api.process.runtime.ProcessRuntime;
import org.activiti.api.runtime.shared.query.Page;
import org.activiti.api.runtime.shared.query.Pageable;
import org.activiti.api.task.model.Task;
import org.activiti.api.task.model.builders.TaskPayloadBuilder;
import org.activiti.api.task.runtime.TaskRuntime;
import org.activiti.engine.HistoryService;
import org.activiti.engine.history.HistoricTaskInstance;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SpringBootTest
public class AskLeaveTest_7x_full {
    private Logger logger = LoggerFactory.getLogger(AskLeaveTest_7x_full.class);

    @Autowired
    private SecurityUtil securityUtil;

    @Autowired
    private ProcessRuntime processRuntime;
    @Autowired
    private TaskRuntime taskRuntime;
    @Autowired
    HistoryService historyService;

    @Test
    @Tag("test")
    public void askLeaveTest() {
        // 外系统的关联单号
        String businessKey = "ASK00001";
        String days = "5";
        //流程定义的key
        String processDefinitionKey = "askleave_0521";
        //启动一个流程实例
        ProcessInstance processInstance = processRuntime.start(
                ProcessPayloadBuilder.start()
                        .withProcessDefinitionKey(processDefinitionKey) //key多次部署可能重复,需要加限定条件
                        .withName(processDefinitionKey + "流程实例名称")
                        .withVariable("days", 0) //初始化参数0,若流程定义中没有默认值会出错
                        .withBusinessKey(businessKey)
                        .build()
        );

        //user1 查找任务
        String assignee = "user1";

        securityUtil.logInAs(assignee);
        Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 100));

        if (tasks.getContent().size() > 0) {
            Task task = tasks.getContent().get(0);
            //user1 处理任务,即填写请假单,把天数变量赋值 >3 天总监要加签
            //null 则为 候选任务,需要现claim
            if (task.getAssignee() == null) {
                taskRuntime.claim(TaskPayloadBuilder.claim()
                        .withTaskId(task.getId())
                        .build());
            }
            //处理任务并设置变量

            taskRuntime.complete(TaskPayloadBuilder
                    .complete()
                    .withTaskId(task.getId())
                    .build());
        } else {
            logger.info(assignee + " 无任务可审核");
        }

        //user2 主管查找任务并处理任务
        assignee = "user2";
        securityUtil.logInAs(assignee);
        tasks = taskRuntime.tasks(Pageable.of(0, 100));

        if (tasks.getContent().size() > 0) {
            Task task = tasks.getContent().get(0);
            //null 则为 候选任务,需要现claim
            if (task.getAssignee() == null) {
                taskRuntime.claim(TaskPayloadBuilder.claim()
                        .withTaskId(task.getId())
                        .build());
            }
            //处理任务
            Map<String, Object> variables = new HashMap<String, Object>();
            variables.put("days", days);

            /**
             * bug 怀疑是 TaskRuntimeImpl.java 164行 那个 true应该是false
             */
            taskRuntime.complete(TaskPayloadBuilder
                    .complete()
                    .withTaskId(task.getId())
                    .withVariable("days", days) //bug?无效?
                    //.withVariables(variables) //bug?无效?
                    .build());

        } else {
            logger.info(assignee + " 无任务可审核");
        }

        //user4 总监查找任务并处理任务
        assignee = "user4";
        securityUtil.logInAs(assignee);
        tasks = taskRuntime.tasks(Pageable.of(0, 100));

        if (tasks.getContent().size() > 0) {
            Task task = tasks.getContent().get(0);
            //null 则为 候选任务,需要现claim
            if (task.getAssignee() == null) {
                taskRuntime.claim(TaskPayloadBuilder.claim()
                        .withTaskId(task.getId())
                        .build());
            }

            //处理任务
            taskRuntime.complete(TaskPayloadBuilder
                    .complete()
                    .withTaskId(task.getId())
                    .build());
        } else {
            logger.info(assignee + " 无任务可审核");
        }

        //user3 人事查找任务并处理任务
        assignee = "user3";
        securityUtil.logInAs(assignee);
        tasks = taskRuntime.tasks(Pageable.of(0, 100));

        if (tasks.getContent().size() > 0) {
            Task task = tasks.getContent().get(0);
            //null 则为 候选任务,需要现claim
            if (task.getAssignee() == null) {
                taskRuntime.claim(TaskPayloadBuilder.claim()
                        .withTaskId(task.getId())
                        .build());
            }
            //处理任务
            taskRuntime.complete(TaskPayloadBuilder
                    .complete()
                    .withTaskId(task.getId())
                    .build());
        } else {
            logger.info(assignee + " 无任务可审核");
        }

        //查看历史记录
        String processInstanceId = processInstance.getId();
        List<HistoricTaskInstance> HisList = historyService
                .createHistoricTaskInstanceQuery()
                .orderByHistoricTaskInstanceEndTime().desc()
                .processInstanceId(processInstanceId) //流程实例ID条件
                .list();
        logger.info("");
        for (HistoricTaskInstance hi : HisList) {
            logger.info("=============================================================");
            logger.info("getId                 :" + hi.getId());
            logger.info("getProcessDefinitionId:" + hi.getProcessDefinitionId());
            logger.info("getProcessInstanceId  :" + hi.getProcessInstanceId());
            logger.info("getName               :" + hi.getName());
            logger.info("getStartTime          :" + hi.getStartTime());
            logger.info("getEndTime            :" + hi.getEndTime());
        }
    }


}

5.完整源代码

https://github.com/dgatiger/activiti_m13_springboot_2.5_demo.git
大家愉快地去玩吧~~

你可能感兴趣的:(JAVA,java,activiti)