工作流Activiti入门 | Monster

工作流Activiti入门





###### 前言

- 利用工作游泳时间学习了Activiti工作流引擎,给出了一个初步使用DEMO,为后续开发的审批流支持

- [资源]: https://www.bilibili.com/video/BV1Ut411y7NT

- [测试案例]: https://catmonster-01.coding.net/p/activiti-test

- [官方文档]: https://www.activiti.org/userguide/



- 使用activiti6.0,基于SpringBoot2.2.xMybatisPlus3.0



###### Activiti

- Activiti 是由 jBPM 的创建者 Tom Baeyens 离开 JBoss 之后建立的项目,构建在开发 jBPM 版本 1 到 4 时积累的多年经验的基础之上,旨在创建下一代的 BPM 解决方案。

- Activiti是一个开源的工作流引擎,它实现了BPMN 2.0规范,可以发布设计好的流程定义,并通过api进行流程调度。

- 简单的理解起来,Activiti大概就是一个工作审批流,在一些日常的工作中,审批是十分常见的,例如:请假、报销、审批等,以请假为例,普通员工填写请假单,然后交由部门主管审核,审核通过再交由行政部门审核,审核完毕,一个请假流程就此结束
- 也可以将其理解为一个载体或引擎,推动着业务的正确执行



###### Activiti表及其核心组件

- 一直以为工作流是一个很高深莫测的玩意,但实际上他通过生成28张数据库表,通过流程中每个任务步骤对表的操作来实现流程管理

- ProcessEngine对象

这是Activiti工作的核心。负责生成流程运行时的各种实例及数据、监控和管理流程的运行

- ProcessEngine流程引擎构建时会自动对数据库构建28张表,不同的内置Service操作不同的模块表,其中:

- RepositoryService流程部署相关
- act_ge_bytearray存储二进制文件,流程图等
- act_re_deployment流程部署表
- act_re_procdef流程定义表
- act_ge_property工作流的ID算法和版本信息表
- RuntimeService流程运行实例
- act_ru_execution 流程实例
- TaskService流程节点任务
- act_ru_task流程节点任务
- act_ru_variable流程运行时变量
- act_ru_identitylink流程办理人Assignee信息
- HistoryService历史记录
- act_hi_procinst历史流程实例
- act_hitaskinst历史任务
- act_hi_actinst历史活动节点
- act_hi_varinst历史流程变量
- act_hi_identitylink历史流程办理人
- act_hi_comment批注
- act_hi_attachment附件
- IdentityService用户组管理
- act_id_group
- act_id_membership 用户和组的关系表
- act_id_info用户的详细信息
- act_id_user用户表
- 其余的暂时没用过,后续补充…

###### Activiti生成表

- 手动获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test(){
// 得到流程引擎
ProcessEngineConfiguration confiuration = ProcessEngineConfiguration.createStandaloneProcessEngineConfiguration();

confiuration.setJdbcDriver("com.mysql.cj.jdbc.Driver");
// 为了防止mysql串库 需要指定 nullCatalogMeansCurrent=true
confiuration.setJdbcUrl("jdbc:mysql://localhost:3306/act_test?serverTimezone=CTT&useSSL=false&nullCatalogMeansCurrent=true");
confiuration.setJdbcUsername("root");
confiuration.setJdbcPassword("1234");

// ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE

// 生成表的方式 true false drop-create
confiuration.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);

processEngine = confiuration.buildProcessEngine();
}
  • SpringBoot方式

    • 依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      <!-- mysql驱动 -->
      <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
      </dependency>
      <!-- mybatis-plus -->
      <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus</artifactId>
      <version>3.3.0</version>
      </dependency>
      <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-generator</artifactId>
      <version>3.3.0</version>
      </dependency>
      <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.3.0</version>
      </dependency>
      <!-- velocity -->
      <dependency>
      <groupId>org.apache.velocity</groupId>
      <artifactId>velocity-engine-core</artifactId>
      <version>2.0</version>
      </dependency>
      <!-- activiti -->
      <dependency>
      <groupId>org.activiti</groupId>
      <artifactId>activiti-spring-boot-starter-basic</artifactId>
      <version>6.0.0</version>
      <!-- avtiviti中依赖了mybatis3,这里用的mybatis-plus需要排除其中的依赖 -->
      <exclusions>
      <exclusion>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      </exclusion>
      </exclusions>
      </dependency>
  • 启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // activiti6中配置了security,与springboot中的自动配置冲突,故将其排除
    @SpringBootApplication(exclude = SecurityAutoConfiguration.class)
    public class ActivitiTestApplication {

    public static void main(String[] args) {
    SpringApplication.run(ActivitiTestApplication.class, args);
    }

    }
Eclipse插件
  • 在官方文档中有介绍一款Eclipse插件
  • Idea2020之后因为存在bug将此插件移除了,但我们相信完美的Jetbrains一定会推出一款完美的插件的。。

  • 安装

    • eclipse -->help --> install New SoftWareadd将 官方给予的namelocation填上即可

      Name: Activiti BPMN 2.0 designer

      Location: http://activiti.org/designer/update/

    • 安装完毕后,新建一个文件能看到activiti模块即为安装成功

流程图
  • 选择Activiti Diagram即可构建一个(.bpmn)流程图,实质为xml,通过插件我们可以通过拖动的方式快捷的构建流程图

  • 常用控件

    • StartEvent 流程开始标识

    • EndEvent流程结束标识

    • userTask用户任务

    • sequenceFlow连接线

    • execlusiveGateway排他网关

    • ParalleGateway并行网关

    • receiveTask接受任务

使用
  • 以一个简单的单线流程为例

    在流程图中,你可以通过EclipseProperties视图设置其相关属性,也可以在xml中定义,userTask任务中通过Name属性设置该任务的名称,在Main configAssignee中指定其执行人

  • 流程部署

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    public void test(){
    // 流程部署service
    RepositoryService repositoryService = processEngine.getRepositoryService();

    // 指定流程名称 生成流程部署对象 这里通过直接加载流程图和图片文件部署
    Deployment deployment = repositoryService.createDeployment().name("请假流程-001").addClasspathResource("hello.bpmn").addClasspathResource("hello.png").deploy();
    // 也可以使用zip方式 如下
    // InputStream inputStream = this.getClass().getResourceAsStream("/hello.zip");
    // ZipInputStream zipInputStream = new ZipInputStream(inputStream);
    // Deployment deployment = repositoryService.createDeployment().name("请假流程-002").addZipInputStream(zipInputStream).deploy(); // 以流的方式部署

    System.out.println("部署成功,ID = " + deployment.getId());
    }

    此时act_ge_bytearray、act_re_deployment、act_re_procdef、act_ge_property四张表中可以看到流程部署定义相关的信息

  • 启动流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    public void test(){
    // 启动流程
    RuntimeService runtimeService = processEngine.getRuntimeService();
    // 启动时可以传递流程变量处理业务和控制流程
    // 根据流程定义的Id启动 act_re_procdef的Id
    // runtimeService.startProcessInstanceById("hello:1:4");
    // 根据流程定义的key启动 常用以此种方式
    runtimeService.startProcessInstanceByKey("myProcess");

    System.out.println("流程启动成功...");
    }

    流程启动之后,依照流程图进入第一个任务提交申请act_ru_task表中存在了此任务节点的相关信息

  • 查询任务

    • mybati-plusjpa风格类似,给出一个例子

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Test
      public void test(){
      TaskService taskService = processEngine.getTaskService();
      List<Task> taskList = taskService.createTaskQuery().taskAssignee("LISA").list();
      if (null!=taskList && !taskList.isEmpty()) {
      taskList.forEach(item->{
      System.out.println("任务Id:" + item.getId() + ";流程实例Id:" + item.getProcessInstanceId() + ";执行实例Id:" + item.getExecutionId() + ";流程定义Id:" + item.getProcessDefinitionId() + ";当前任务名称:" + item.getName() + ";任务办理人:" + item.getAssignee());
      });
      }
      }
  • 执行任务

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void test(){
    TaskService taskService = processEngine.getTaskService();
    // 通过任务id 完成当前任务 此处也可以传递流程变量信息
    taskService.complete("2505");
    System.out.println("任务完成..");
    }

    此任务执行完后,在此查看act_ru_task表,发现已经到达下一个任务节点部门经理审批,一次类推,流程控制实现

补充
  • 关于一些流程表的crud操作此处不做演示,可以从coding中获取

  • sequenceFlow连线

    • 关于连线,用于指向流程的下一个任务节点,通过在连线中传递条件作为判断依据,一次可以实现选择流程路径的功能(if..else),在sequenceFlowMain configCondition中通过$#(暂时没有感受到在activiti中两者传递变量的区别)设置

    • 流程图如下

    • 此种方式,在需要选择流程的任务完成时需要传递一个标识路径的判断条件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Test
      public void test(){
      TaskService taskService = processEngine.getTaskService();
      Map<String,Object> map = new HashMap<>();
      map.put("outcome","不重要"); // 携带流程变量决定分支
      taskService.complete("15002",map);

      System.out.println("任务完成...........");
      }
  • 排他网关ExclusiveGateway

    • 排他网关同上述类似,旨在一个任务执行完成之后通过条件在多路分支中选择一条路径往下执行,类似于(if..elseif..else),通过exclusiveGateway连接,在sequenceFlowMain configCondition中通过$#设置

    • 流程图如下

    • 使用时同上完成任务时传递一个变量cust即可

  • 并行网关ParallelGateway

    • 并行网关相当于在一个流程下有多个分支同时进行,只有当所有分支执行完成,流程才结束
    • 流程图
    • 必须当所有任务完成,流程才会结束
  • 接收活动|等待活动ReceiveTask

    • 接收任务是一个简单任务,他会等待对应消息的到达,当流程到达接收任务,流程状态会保存到数据库中,在任务创建后,意味着流程会进入等待状态,直到引擎接收了一个特定的消息,这会触发流程穿过接收任务执行

    • 使用receiveTask控件

    • 使用receiveTask,流程启动后,此时task表中是没有任务的,任务处于等待状态,业务执行完毕后,需要通过传递流程实例id使流程向后继续执行,使用trigger()方法(有点类似于不断触发执行下一步)

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      @Test
      public void doTask(){
      RuntimeService runtimeService = processEngine.getRuntimeService();
      String processInstanceId = "2501";

      Execution execution1 = runtimeService.createExecutionQuery()
      .processInstanceId(processInstanceId)
      .activityId("receivetask1") // 当前活动id 对应流程图中活动节点Id的值
      .singleResult();

      // TODO 查询当日销售额

      // 流程变量设置当日销售额
      runtimeService.setVariable(execution1.getId(),"当日销售额",30000);

      // 向后执行一步 如果流程处于等待状态,使得流程继续执行 新版本中 signal换成了 trigger
      runtimeService.trigger(execution1.getId());
      }

      /**
      * 发送信息
      */
      @Test
      public void sendMessage(){
      RuntimeService runtimeService = processEngine.getRuntimeService();

      // 第二步流程
      Execution execution2 = runtimeService.createExecutionQuery()
      .processInstanceId("2501")
      .activityId("receivetask2") // 当前活动id 对应流程图中活动节点Id的值
      .singleResult();


      Integer sum = (Integer) runtimeService.getVariable(execution2.getId(), "当日销售额");

      // 发送短信
      System.out.println("发送短信:当日销售额 " + sum);

      // 流程向后执行
      runtimeService.trigger(execution2.getId());

      System.out.println("流程执行完成...");

      }
  • 任务监听TaskListener

    • activiti支持任务监听,通过实现TaskLinstener接口与流程节点绑定实现监听,以通过监听方式设置任务处理人为例

    • 监听实现类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class TaskListerImpl implements TaskListener {

      @Override
      public void notify(DelegateTask delegateTask) {
      // 从session中获取对应办理人

      // 指定办理人
      delegateTask.setAssignee("Monster");
      }
      }
    • 在流程图中通过设置任务节点ListenersTask listeners与实现类关联

  • 组任务与任务拾取

    • 使用组任务可以实现任务的候选情景,类似于当一个任务发布,所有的候选人都可以看见此任务,但并未指定具体的处理人,当处理人被分配,只有该处理人可以看到此任务并进行处理

    • 通过流程图任务节点Main configCandidate users可以配置候选人列表,也可以通过流程变量传递,类型为String,通过,相隔

    • 启动时设置候选人列表,也可以手动添加成员

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
       /**
      * 通过流程变量启动
      */
      @Test
      public void runByVariables(){
      RuntimeService runtimeService = processEngine.getRuntimeService();
      Map<String,Object> map = new HashMap<>();
      map.put("usernames","小C,小D,小E,小F");

      ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("groupTask",map);
      System.out.println("流程启动成功...");
      }

      /**
      * 添加成员
      */
      @Test
      public void add(){
      TaskService taskService = processEngine.getTaskService();
      String taskId = "2506";

      taskService.addCandidateUser(taskId,"Monster");

      System.out.println("添加成功");
      }
  • 启动流程后,查看act_ru_identitylink表,存放了所有候选人信息且出现了两次,Typecandidate表示候选人,participant表示申请人

  • 查看任务组成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     /**
    * 查询组任务成员列表
    */
    @Test
    public void findGroupUser(){
    String taskId = "2506";
    List<IdentityLink> list = processEngine.getTaskService().getIdentityLinksForTask(taskId);

    list.forEach(item->{
    System.out.println(item.getUserId());
    });
    }
  • 删除成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     /**
    * 删除成员
    */
    @Test
    public void remove(){
    TaskService taskService = processEngine.getTaskService();
    String taskId = "2506";

    // 此删除只会删除 candidate 候选者信息 保留了参与者信息
    taskService.deleteCandidateUser(taskId,"Monster");


    System.out.println("删除成功");
    }
  • 删除后再次查看act_ru_identitylink,该成员只删除了candidate的那一行,participant作为记录依旧保存,查看该任务的组成员列表,此成员已经失去了候选资格

  • 任务拾取

    可以将任务分配给一个指定的候选人,当任务分配完毕后,除了该处理人以外的其他成员不可见此任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 任务拾取
    */
    @Test
    public void claim(){
    TaskService taskService = processEngine.getTaskService();

    String taskId = "2506";

    taskService.claim(taskId,"小A");

    System.out.println("任务拾取结束...");
    }
  • 任务回退

    对于此种方式的任务回退,貌似只需要将任务的Assignee重新设置为null即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     /**
    * 任务回退
    */
    @Test
    public void claimBack(){
    TaskService taskService = processEngine.getTaskService();
    String taskId = "2505";

    // 所谓的回退 也就是将任务的 assignee设置为空
    taskService.setAssignee(taskId,null);

    System.out.println("任务回退成功...");
    }
例子
  • 依照某个业务中大致给出了一个简单的DEMO,流程图:

  • 用户到员工的关系设计大致如下:

  • 粗糙的测试(大部分id被直接写死),可以从源码中获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    @SpringBootTest
    class ActivitiTestApplicationTests {

    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private TaskService taskService;

    @Autowired
    private IBillService billService;

    @Autowired
    private IPositionService positionService;

    @Autowired
    private IEmployeeService employeeService;

    @Autowired
    private IUserService userService;

    /**
    * 部署流程
    */
    @Test
    void contextLoads() {
    System.out.println(repositoryService);

    repositoryService.createDeployment().name("审批-test")
    .addClasspathResource("test.bpmn")
    .addClasspathResource("test.png")
    .deploy();

    System.out.println("部署成功....");
    }

    /**
    * 运行流程 提交单据 新增
    */
    @Test
    public void startProd(){
    // 获取单据 0 未提交 1 审核中 2 审核完成 3 已放弃
    String id = "a33e6d57a65b11ea8ee4005056c00001";
    Bill bill = billService.getById(id);


    Map<String,Object> content = new HashMap<>();
    content.put("userId",bill.getUserId());

    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("myProcess", content);

    // bill.setStatus(1);
    // billService.updateById(bill); // 更新状态

    System.out.println("流程启动成功..."+processInstance.getId());
    }


    /**
    * 各用户的任务
    */
    @Test
    public void getTask(){
    // TODO
    // 38dd78b9a63b11ea8ee4005056c00001
    String userId = "38dd78b9a63b11ea8ee4005056c00001";

    List<Task> list = taskService.createTaskQuery()
    .taskAssignee(userId)
    .orderByTaskCreateTime().desc()
    .list();

    list.forEach(item->{
    System.out.printf("id:%s,\n任务名称:%s\n",item.getId(),item.getName());
    });
    }

    /**
    * 提交申请
    */
    @Test
    public void submit(){


    String position = "a534696da63a11ea8ee4005056c00001"; //科长类型
    // 所有的科长
    List<Employee> empList = employeeService.list(new QueryWrapper<Employee>().lambda().eq(Employee::getPosition, position));
    // f23a5f56a63a11ea8ee4005056c00001 科长
    String empId = "f23a5f56a63a11ea8ee4005056c00001";
    User user = userService.getOne(new QueryWrapper<User>().lambda().eq(User::getEmpId, empId));
    if(null==user){
    // TODO 此员工并未绑定user表
    }

    String taskId = "12503";
    String processInstanceId = "2501";
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
    Map<String,Object> content = new HashMap<>();
    content.put("sectionChief",user.getId());
    taskService.addComment(taskId,processInstanceId,task.getName());
    taskService.complete(taskId,content); // 执行流程 选择审批的科长


    //更新表
    String id = "a33e6d57a65b11ea8ee4005056c00001";
    Bill bill = billService.getById(id);

    bill.setStatus(1);
    billService.updateById(bill);

    System.out.println("提交申请完成");


    }

    /**
    * 科长审批
    */
    @Test
    public void sectionChiefSubmit(){


    // 查询经理
    // 经理 5040b59ea63b11ea8ee4005056c00001
    String empId = "f75a841aa63a11ea8ee4005056c00001";
    Employee manager = employeeService.getById(empId);

    User user = userService.getOne(new QueryWrapper<User>().lambda().eq(User::getEmpId, manager.getId()));

    String taskId = "15003";
    String processInstanceId = "2501";
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

    Map<String,Object> content = new HashMap<>();
    content.put("manager",user.getId());

    boolean sign = true;
    String comment = sign?"通过":"驳回";

    content.put("sign",sign); //通过
    taskService.addComment(taskId,processInstanceId,"["+comment+"]"+task.getName());


    // content.put("sign",false); // 驳回

    taskService.complete(taskId,content);


    System.out.println("科长审批完成...");

    }

    /**
    * 经理审批
    */
    @Test
    public void managerSubmit(){

    String taskId = "17503";
    String processInstanceId = "2501";
    Task task = taskService.createTaskQuery().taskId(taskId).singleResult();

    Map<String,Object> content = new HashMap<>();
    boolean sign = true;
    String comment = sign?"通过":"驳回";

    content.put("sign",sign); //驳回
    taskService.addComment(taskId,processInstanceId,"["+comment+"]"+task.getName());


    taskService.complete(taskId,content);

    String id = "a33e6d57a65b11ea8ee4005056c00001";
    Bill bill = billService.getById(id);

    bill.setStatus(2);
    billService.updateById(bill);

    System.out.println("审批完成,流程结束......");

    }



    }
-------------本文结束感谢您的阅读-------------
既然来了就打个赏吧= =
0%