with spring boot

两种部署方式 一种是与spring boot 集成 称为Activiti Core 一种是使用docker 容器? 装一堆东西称为Activiti Cloud Getting Started

依赖 pom

pom 修改

<!-- https://mvnrepository.com/artifact/org.activiti/activiti-spring-boot-starter -->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>x.x.x</version>
</dependency>
<!-- 还有数据库依赖 -->
 
 
<!-- 中央仓库只有 7.x.M6, 以后的版本发布在私服 -->
<repositories>
    <repository>
    <id>activiti-releases</id>
    <url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

自动建表ACT_RE_DEPLOYMENT会少了两个字段 VERSION_ PROJECT_RELEASE_VERSION_ 手动添加之 正常结构注意顺序

CREATE TABLE `act_re_deployment`  (
  `ID_` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '', 
  `NAME_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, 
  `CATEGORY_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, 
  `KEY_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, 
  `TENANT_ID_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '', 
  `DEPLOY_TIME_` timestamp(3) NULL DEFAULT NULL, 
  `ENGINE_VERSION_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, 
  `VERSION_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, 
  `PROJECT_RELEASE_VERSION_` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL, 
  PRIMARY KEY (`ID_`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

配置 yml

spring:
############ activiti 工作流
  activiti: 
    #表示启动时检查数据库表, 不存在则创建
    database-schema-update: true 
    #表示哪种情况下使用历史表, 这里配置为full表示全部记录历史, 方便绘制流程图
    history-level: full
    #true表示使用历史表, 如果不配置, 则工程启动后可以检查数据库, 只建立了17张表
    db-history-used: true
-- 清空工作流 表数据
delete from act_ru_identitylink;
delete from act_ru_variable;
delete from act_ru_task;
delete from act_ru_execution;
 
delete from act_ge_bytearray;
delete from act_re_deployment;
 
 
delete from act_re_procdef
delete from act_hi_varinst
 
 
--元数据 视版本而定
--[
INSERT INTO `act_ge_property` VALUES ('cfg.execution-related-entities-count''false'1);
INSERT INTO `act_ge_property` VALUES ('next.dbid''1'1);
INSERT INTO `act_ge_property` VALUES ('schema.history''create(7.0.0.0) upgrade(7.0.0.0->7.1.0.0)'2);
INSERT INTO `act_ge_property` VALUES ('schema.version''7.1.0.0'2);
 
]--

官方的简单示例

参考官方的一个示例项目 activiti-api-basic-full-example-bean

建模文件 categorize-human-content.bpmn20.xml

放到classpath:/processes/categorize-human-content.bpmn20.xml 启动后会自动部署

启动 run

@Override
public void run(String... args) {
    //
    securityUtil.logInAs("system");
    Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(010));
    logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems());
    for (ProcessDefinition pd: processDefinitionPage.getContent()) {
        logger.info("\t > Process definition: " + pd);
    }
}
 

用户鉴权

securityUtil.logInAs("system");

用的是spring 用户 SecurityUtil.java

 public void logInAs(String username) {
 
    UserDetails user = userDetailsService.loadUserByUsername(username);
    if (user == null) {
        throw new IllegalStateException("User " + username + " doesn't existplease 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);
}
 

踩坑指南 用户必须具有ROLE_ACTIVITI_USER 角色代码 才可以参与流程访问

文档中有那么一句话 Something important to notice hereis that in order to interact with the TaskRuntime API as a useryou need to have the role: ACTIVITI_USER (Granted Authority: ROLE_ACTIVITI_USER) .

初始化内存用户的时候, 也有这个角色 ROLE_ACTIVITI_USER 代码 DemoApplicationConfiguration.java

public UserDetailsService myUserDetailsService() {
 
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
 
    String[][] usersGroupsAndRoles = {
            {"salaboy""password""ROLE_ACTIVITI_USER""GROUP_activitiTeam"}, 
            {"ryandawsonuk""password""ROLE_ACTIVITI_USER""GROUP_activitiTeam"}, 
            {"erdemedeiros""password""ROLE_ACTIVITI_USER""GROUP_activitiTeam"}, 
            {"other""password""ROLE_ACTIVITI_USER""GROUP_otherTeam"}, 
            {"system""password""ROLE_ACTIVITI_USER"}, 
            {"admin""password""ROLE_ACTIVITI_ADMIN"}, 
    };
 
    for (String[] user: usersGroupsAndRoles) {
        List<String> authoritiesStrings = Arrays.asList(Arrays.copyOfRange(user2user.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;
}

查询已部署的流程

Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(010));
logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems());
for (ProcessDefinition pd: processDefinitionPage.getContent()) {
    logger.info("\t > Process definition: " + pd);
}

out

Available Process definitions: 1 Process definition: ProcessDefinition{id=‘categorizeHumanProcess:1:2e3392e1-c64b-11ea-b436-005056c00008’name=‘categorizeHumanProcess’key=‘categorizeHumanProcess’description=‘null’formKey=‘null’version=1}

启动流程 processText

@Scheduled(initialDelay = 1000fixedDelay = 5000)
    public void processText() {
        securityUtil.logInAs("system");
        //随机 内容 dto
        Content content = pickRandomString();
        SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yy HH:mm:ss");
 
        logger.info("> Starting process to process content: " + content + " at " + formatter.format(new Date()));
        //ProcessPayloadBuilder 负载流程 builder
        //使用 processRuntime 对象启动它; 返回流程实例
        ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
                .start()
                //key 对应上面查到ProcessDefinition 的key; (其实是 act_re_procdef 表的key字段?); 最终是 bpmn20.xml 定义的id
                .withProcessDefinitionKey("categorizeHumanProcess")
                .withName("Processing Content: " + content)
                .withVariable("content"content)
                .build());
 
        // 开始流程后会在 act_ru_task 插一条记录
        logger.info(">>> Created Process Instance: " + processInstance);
 
 
    }
// Content  对象是个DTO 有以下属性
public class Content {
 
    private String body;
    private boolean approved;
    private List<String> tags;
    ....
}
 

用户处理任务 checkAndWorkOnTasksWhenAvailable

    @Scheduled(initialDelay = 1000fixedDelay = 5000)
    public void checkAndWorkOnTasksWhenAvailable() {
        securityUtil.logInAs("salaboy");//鉴权用户
        //获取 查询任务列表// 在xml 定义<bpmn:expression xsi:type="bpmn:tFormalExpression">activitiTeam</bpmn:expression>
        //流程activitiTeam用户组只有这个组的用户才能查询到改任务// so act_ru_identitylink
        Page<Task> tasks = taskRuntime.tasks(Pageable.of(010));
        if (tasks.getTotalItems() > 0) {
            for (Task t: tasks.getContent()) {
 
                logger.info("> Claiming task: " + t.getId());
                //注意必须先声明任务, 声明之后, 没有人可以看到任务才可以完成任务 (事务?)
                taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(t.getId()).build());
                //拿到流程的 withVariable 变量
                List<VariableInstance> variables = taskRuntime.variables(TaskPayloadBuilder.variables().withTaskId(t.getId()).build());
                VariableInstance variableInstance = variables.get(0);
                if (variableInstance.getName().equals("content")) {
                    Content contentToProcess = variableInstance.getValue();
                    logger.info("> Content received inside the task to approve: " + contentToProcess);
 
                    if (contentToProcess.getBody().contains("activiti")) {
                        logger.info("> User Approving content");
                        //用户同意
                        contentToProcess.setApproved(true);
                    } else {
                        logger.info("> User Discarding content");
                        //用户拒绝
                        contentToProcess.setApproved(false);
                    }
                    //完成任务
                    taskRuntime.complete(TaskPayloadBuilder.complete()
                            .withTaskId(t.getId()).withVariable("content"contentToProcess).build());
                }
 
 
            }
 
        } else {
            logger.info("> There are no task for me to work on.");
        }
 
    }

网关 顺序流表达式处理

图例(各种箭头) Expression 配置了 ${content.approved == false} 所以当用户同意时设置contentToProcess.setApproved(true);值 完成任务时 taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(t.getId()).withVariable("content"contentToProcess).build());会导致网关执行不同的顺序流向.

连接器

根据不同的 顺序流向. 执行这两个连接器的代码..

@Bean
public Connector tagTextConnector() {
    return integrationContext -> {
        Content contentToTag = (Content) integrationContext.getInBoundVariables().get("content");
        contentToTag.getTags().add(" :) ");
        integrationContext.addOutBoundVariable("content", 
                contentToTag);
        logger.info("Final Content: " + contentToTag);
        return integrationContext;
    };
}
 
@Bean
public Connector discardTextConnector() {
    return integrationContext -> {
        Content contentToDiscard = (Content) integrationContext.getInBoundVariables().get("content");
        contentToDiscard.getTags().add(" :( ");
        integrationContext.addOutBoundVariable("content", 
                contentToDiscard);
        logger.info("Final Content: " + contentToDiscard);
        return integrationContext;
    };
}

对应XML定义

  1. 首先定义了一个流程id name <bpmn:process id="categorizeHumanProcess" name="categorizeHumanProcess" isExecutable="true" camunda:versionTag="2">

  2. 定义一个开始事件

   <bpmn:startEvent id="StartEvent_1">
      <bpmn:outgoing>SequenceFlow_09xowo4</bpmn:outgoing>
    </bpmn:startEvent>
  1. 顺序流 接受开始事件指向一个用户任务Task_1ylvdew
 <bpmn:sequenceFlow id="SequenceFlow_09xowo4" sourceRef="StartEvent_1" targetRef="Task_1ylvdew" />
  1. Task_1ylvdew用户任务定义
<bpmn:userTask id="Task_1ylvdew" name="Process Content">
      <bpmn:incoming>SequenceFlow_09xowo4</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_1jzbgkj</bpmn:outgoing>
      <bpmn:potentialOwner>
        <bpmn:resourceAssignmentExpression>
          <bpmn:formalExpression>activitiTeam</bpmn:formalExpression>
        </bpmn:resourceAssignmentExpression>
      </bpmn:potentialOwner>
    </bpmn:userTask>
  1. 又一个顺序流 从任务Task_1ylvdew 到网关 ExclusiveGateway_0c36qc6
<bpmn:sequenceFlow id="SequenceFlow_1jzbgkj" sourceRef="Task_1ylvdew" targetRef="ExclusiveGateway_0c36qc6" />
  1. 网关 单纯只是分叉(执行顺序流表达式?), 从顺序流SequenceFlow_1jzbgkjSequenceFlow_0tsc63vSequenceFlow_049fuit 两个顺序流
<bpmn:exclusiveGateway id="ExclusiveGateway_0c36qc6" name="Content Accepted?">
      <bpmn:incoming>SequenceFlow_1jzbgkj</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_0tsc63v</bpmn:outgoing>
      <bpmn:outgoing>SequenceFlow_049fuit</bpmn:outgoing>
    </bpmn:exclusiveGateway>
  1. 这个两个顺序流有个表达式, 根据content.approved变量决定 流向
 
 <bpmn:sequenceFlow id="SequenceFlow_0tsc63v" name="yes" sourceRef="ExclusiveGateway_0c36qc6" targetRef="Task_0snvh02">
    <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${content.approved == true}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="SequenceFlow_049fuit" name="no" sourceRef="ExclusiveGateway_0c36qc6" targetRef="Task_1asxw87">
    <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${content.approved == false}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
  1. 两个分支的任务(连接器?)实现名称是 tagTextConnectordiscardTextConnector
<bpmn:serviceTask id="Task_0snvh02" name="Tag categorized Content" implementation="tagTextConnector">
    <bpmn:incoming>SequenceFlow_0tsc63v</bpmn:incoming>
    <bpmn:outgoing>SequenceFlow_1nn2llw</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:serviceTask id="Task_1asxw87" name="Discard and Notify user" implementation="discardTextConnector">
    <bpmn:incoming>SequenceFlow_049fuit</bpmn:incoming>
    <bpmn:outgoing>SequenceFlow_0upfncf</bpmn:outgoing>
</bpmn:serviceTask>
  1. 最后流向结束事件
<bpmn:sequenceFlow id="SequenceFlow_0upfncf" sourceRef="Task_1asxw87" targetRef="EndEvent_13bsqqd" />
<bpmn:sequenceFlow id="SequenceFlow_1nn2llw" sourceRef="Task_0snvh02" targetRef="EndEvent_1ogwwp9" />
 
 
 
 

这中间有一个 Content 的数据对象

userTask 指定办理人

必须在 XML 中定义!!!

  1. 在xml 中的任务(圆角矩形) Assignee 属性 指定办理人
<bpmn:userTask id="Task_1ylvdew" name="Process Content" camunda:assignee="${username}">

注意这个前缀应该是 activiti:assignee=视建模软件会改变我K!!

然而部署总是 前缀错误最终..

<bpmn:userTask id="Task_1ylvdew" name="Process Content" >
    <bpmn:incoming>SequenceFlow_09xowo4</bpmn:incoming>
    <bpmn:outgoing>SequenceFlow_1jzbgkj</bpmn:outgoing>
    <bpmn:potentialOwner>
    <bpmn:resourceAssignmentExpression>
        <!-- user(指定用户) group(指定用户组) -->
        <bpmn:formalExpression>user(${username})group(management)</bpmn:formalExpression>
    </bpmn:resourceAssignmentExpression>
    </bpmn:potentialOwner>
</bpmn:userTask>
  1. 或者在xml 中的任务(圆角矩形) Assignee 属性使用占位符 ${username} 然后通过API动态给 variable 赋值
StartProcessPayload precess = ProcessPayloadBuilder.start()
			.withProcessDefinitionKey("categorizeHumanProcess")
			.withVariable("username""admin")

参考6.0 的文档

踩坑指南

startProcessInstance 无用户任务?

runtimeService::startProcessInstanceByKey 开始流程后, 无用户任务问题, act_ru_task 表亦无记录;

原因是 UserTask的Asynchronous属性为;

勾选用户任务Asynchronous, 流程启动后, 开始节点完成后, 验证下一个节点的地址操作交给定时任务异步来完成, 定时任务执行时间和次数可在act_ru_job表中查看, 用户任务还没在任务表中生成, 默认等待5分钟后才会在act_ru_task表生成用户任务

但是有时我想一个节点完成了就提交事务, 不要依赖下一个节点, 也就是不管下一个节点发生什么都不关已完成的节点的事; 要实现这种, 可使用Asynchronous来实现完成任务在一个事务中执行, 验证下一个节点的地址操作交给定时任务异步来完成;

手动获取定时任务并执行:

Map variableMap = new HashMap();
ProcessInstance pi = runtimeService.startProcessInstanceByKey("TestBook", variableMap);
Job job = managementService.createJobQuery().executionId(pi.getId()).singleResult();
managementService.executeJob(job.getId());