下载示例代码

https://community.alfresco.com/external-link.jspa?url=https%3A%2F%2Fgithub.com%2Fgravitonian%2Factiviti7-api-basic-process

maven 依赖

示例项目使用 7.0.0.Beta3,我们需要将它换成最新的 7.1.7。

首先是 spring-boot 版本,需要更新至 2.1.2 :

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
</parent>

添加一个属性:

<activiti-dependencies.version>7.1.7</activiti-dependencies.version>

修改 activiti 的版本:

<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.activiti.cloud.dependencies</groupId>
				<artifactId>activiti-cloud-dependencies</artifactId>
				<version>${activiti-dependencies.version}</version>
				<scope>import</scope>
				<type>pom</type>
			</dependency>
		</dependencies>
	</dependencyManagement>

7.1.7 还没有正式发布,目前放在 alfresco 的私有库上,我们还需要添加一个私有库:

<repositories>
        <repository>
            <id>alfresco</id>
            <name>Activiti Releases</name>
            <url>https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </repository>
    </repositories>

修改代码

有几个地方报红了,需要修改一下。7.1 和 7.0 相比较,主要是命名或包结构的改变。

  • 修改 MyProcessEventListener.java 46 行,将 SequenceFlowTakenEvent 修改为 SequenceFlowEvent。
  • 修改 ProcessStartController.java 47 行,将 withProcessInstanceName 修改为 withName。
  • 在 ServiceTask1Connector.java 中,实现抽象方法:
@Override
public IntegrationContext apply(IntegrationContext integrationContext) {
    return integrationContext;
}

OK,可以运行了。

流程图

代码中使用的流程图文件是 resources/processes/sample-process.bpmn20.xml。这是一个非常简单的流程图:


总共只有两个任务:一个人工任务,一个服务任务。

注意,流程定义文件必须放在 resources/processes 目录下。

spring security

activiti 7 集成了 spring security。也就是说,哪怕是你的项目本身使用的并不是 spring security,但只要你使用了 activiti 7,那么默认就已经启用了 spring security。

为了使用 Process 运行时,我们必须登录某个用户(这个用户必须具有 ROLE_ACTIVITI_USER 角色)。如果是在 Java 代码中调用 Process 运行时,我们可以在调用之前模拟登录。你可以使用 securityUtil 类的 logInAs 方法来模拟登录:securityUtil.logInAs(“system”);

因为我们的 app 是前后端分离的,所以我们的登录只能是通过浏览器进行(使用 Basic Auth)。具体的做法是在 Activiti7ApplicationConfiguration 中实现的:

@Configuration
@EnableWebSecurity
public class Activiti7ApplicationConfiguration  extends WebSecurityConfigurerAdapter {
...

String[][] usersGroupsAndRoles = {
 {"mbergljung", "1234", "ROLE_ACTIVITI_USER", "GROUP_activitiTraining"},
 {"testuser", "1234", "ROLE_ACTIVITI_USER", "GROUP_activitiTraining"},
 {"system", "1234", "ROLE_ACTIVITI_USER"},
 {"admin", "1234", "ROLE_ACTIVITI_ADMIN"},
 };

注意 @ EnableWebSecurity 注解。在这里添加了几个用户,前 3 个属于 ROLE_ACTIVITI_USER 角色,可用于 Basic Auth 登录。

同时也禁用了 crsf 防护:

http
 .csrf().disable()

列出所有流程定义

这个是在控制器 ProcessDefinitionsController 中实现的:

@GetMapping("/process-definitions")
    public List<ProcessDefinition> getProcessDefinitions() {
        Page<ProcessDefinition> processDefinitionPage = processRuntime.processDefinitions(Pageable.of(0, 10));
        logger.info("> Available Process definitions: " + processDefinitionPage.getTotalItems());

        for (ProcessDefinition pd : processDefinitionPage.getContent()) {
            logger.info("\t > Process definition: " + pd);
        }

        return processDefinitionPage.getContent();
    }

用浏览器访问 http://localhost:8080/process-definitions,返回:

[{“id”:“5be497e7-4b82-11e9-be91-62c7d1691b1b”,“name”:“Sample Process”,“version”:1,“key”:“sampleproc-e9b76ff9-6f70-42c9-8dee-f6116c533a6d”}]

如果你不小心使用了 admin 进行了登录,那么所有的 Process 引擎 API 调用都会返回 403 错误(因为它不属于 ROLE_ACTIVITI_USER 角色)。这时你可以先修改代码中 admin 的用户名为别的(或者删除 admin 用户),然后重新运行 app,此时刷新页面,浏览器又会弹出登录框,你就可以用其它 3 个用户登录,服务器调用就能正常返回。

启动流程

启动流程由 ProcessStartController 类实现:

@RequestMapping("/start-process")
    public ProcessInstance startProcess(
            @RequestParam(value="processDefinitionKey", defaultValue="SampleProcess") String processDefinitionKey) {
        ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder
                .start()
                .withProcessDefinitionKey(processDefinitionKey)
                .withProcessDefinitionKey("Sample Process: " + new Date())//.withProcessInstanceName("Sample Process: " + new Date())
                .withVariable("someProcessVar", "someProcVarValue")
                .build());
        logger.info(">>> Created Process Instance: " + processInstance);

        return processInstance;
    }

现在,使用 ProcessPayloadBuilder 来构建一个流程实例。流程实例变量(全局变量)用 withVariable() 方法赋值。

用浏览器访问 http://localhost:8080/start-process?processDefinitionKey=sampleproc-e9b76ff9-6f70-42c9-8dee-f6116c533a6d,启动流程。其中 sampleproc-e9b76ff9-6f70-42c9-8dee-f6116c533a6d 是流程定义的 key。返回:

{“id”:“48519e31-4b86-11e9-8c1b-62c7d1691b1b”,“name”:“Sample Process: Thu Mar 21 11:06:12 CST 2019”,“processDefinitionId”:“2beace0f-4b86-11e9-8c1b-62c7d1691b1b”,“processDefinitionKey”:“sampleproc-e9b76ff9-6f70-42c9-8dee-f6116c533a6d”,“startDate”:“2019-03-21T03:06:12.252+0000”,“status”:“RUNNING”,“processDefinitionVersion”:1}

记住这个流程实例的 id,后面会用到。

如何部署新的流程定义?

只需要你将 .bpmn/.bpmn20.xml 文件放到 resources/processes 目录下,重启 app,就可以自动部署新的流程定义。

列出流程实例

列出流程实例在 ProcessInstanceController 中实现:

@GetMapping("/process-instances")
 public List<ProcessInstance> getProcessInstances() {
 List<ProcessInstance> processInstances =
 processRuntime.processInstances(Pageable.of(0, 10)).getContent();

 return processInstances;
 }

浏览器访问 http://localhost:8080/process-instances,返回:

[{“id”:“48519e31-4b86-11e9-8c1b-62c7d1691b1b”,“name”:“Sample Process: Thu Mar 21 11:06:12 CST 2019”,“processDefinitionId”:“2beace0f-4b86-11e9-8c1b-62c7d1691b1b”,“processDefinitionKey”:“sampleproc-e9b76ff9-6f70-42c9-8dee-f6116c533a6d”,“startDate”:“2019-03-21T03:06:12.252+0000”,“status”:“RUNNING”,“processDefinitionVersion”:1}]

返回结果和创建流程时返回的一模一样,因为目前我们就只有这个流程实例。

列出流程实例的其它信息

列出流程实例在 ProcessInstanceController 中实现:

@GetMapping("/process-instance-meta")
    public ProcessInstanceMeta getProcessInstanceMeta(@RequestParam(value="processInstanceId") String processInstanceId) {
        ProcessInstanceMeta processInstanceMeta = processRuntime.processInstanceMeta(processInstanceId);

        return processInstanceMeta;
    }

访问:http://localhost:8080/process-instance-meta?processInstanceId=48519e31-4b86-11e9-8c1b-62c7d1691b1b(注意修改 processInstanceId 参数),返回:

{“processInstanceId”:“48519e31-4b86-11e9-8c1b-62c7d1691b1b”,“activeActivitiesIds”:[“UserTask_0b6cp1l”]}

activeActivitiesIds 返回的是当前流程正在执行的活动。

列出用户待办

在 TaskManagementController 中:

@GetMapping("/my-tasks")
    public List<Task> getMyTasks() {
        Page<Task> tasks = taskRuntime.tasks(Pageable.of(0, 10));
        logger.info("> My Available Tasks: " + tasks.getTotalItems());

        for (Task task : tasks.getContent()) {
            logger.info("\t > My User Task: " + task);
        }

        return tasks.getContent();
    }

访问 http://localhost:8080/my-tasks,返回:

[{“id”:“48543644-4b86-11e9-8c1b-62c7d1691b1b”,“name”:“User Task 1”,“status”:“ASSIGNED”,“assignee”:“testuser”,“createdDate”:“2019-03-21T03:06:12.262+0000”,“priority”:50,“processDefinitionId”:“2beace0f-4b86-11e9-8c1b-62c7d1691b1b”,“processInstanceId”:“48519e31-4b86-11e9-8c1b-62c7d1691b1b”}]

注意,只有 testuser 用户有待办。请登录 testuser。

如何切换 Basic Auth 用户

如果是 IE,请执行 Javascript document.execCommand("ClearAuthenticationCache");
如果是 Chrome,请用 shift + command + n 打开无痕模式,在这种模式下,总是需要输入密码。

列出所有待办

访问 http://localhost:8080/all-tasks。需要 admin 用户。

办理任务

访问 localhost:8080/complete-task?taskId=48543644-4b86-11e9-8c1b-62c7d1691b1b(注意检查 taskId 是否和你的匹配),需要 testuser 用户。返回:

Completed Task: 48543644-4b86-11e9-8c1b-62c7d1691b1b

监听器

传统的流程监听和任务监听是通过继承的方式实现的。这意味着这些代码和流程的执行是以同步的方式执行。在云部署的情况下,这通常不是一种好办法。Activiti 7 流程引擎采用了事件机制,这样我们就可以订阅事件了,这是异步的。

要实现流程监听需要实现 ProcessRuntimeEventListener 接口。这是由 MyProcessEventListener 完成的:

@Override
    public void onEvent(RuntimeEvent runtimeEvent) {

        if (runtimeEvent instanceof ProcessStartedEvent)
            logger.info("Do something, process is started: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof ProcessCompletedEvent)
            logger.info("Do something, process is completed: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof ProcessCancelledEvent)
            logger.info("Do something, process is cancelled: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof ProcessSuspendedEvent)
            logger.info("Do something, process is suspended: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof ProcessResumedEvent)
            logger.info("Do something, process is resumed: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof ProcessCreatedEvent)
            logger.info("Do something, process is created: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof SequenceFlowEvent)
            logger.info("Do something, sequence flow is taken: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof VariableCreatedEvent)
            logger.info("Do something, variable was created: " + runtimeEvent.toString());
        else
            logger.info("Unknown event: " + runtimeEvent.toString());

    }

通过 spring 的自动装配机制,实现了 ProcessRuntimeEventListener 的监听器会自动装配成 spring bean,然后注入到 ProcessRuntime 中。

类似的,任务事件监听必须实现 TaskRuntimeEventListener 接口的 onEvent 方法,即 MyTaskEventListener 类:

public void onEvent(RuntimeEvent runtimeEvent) {

        if (runtimeEvent instanceof TaskActivatedEvent)
            logger.info("Do something, task is activated: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof TaskAssignedEvent) {
            TaskAssignedEvent taskEvent = (TaskAssignedEvent)runtimeEvent;
            Task task = taskEvent.getEntity();
            logger.info("Do something, task is assigned: " + task.toString());
        } else if (runtimeEvent instanceof TaskCancelledEvent)
            logger.info("Do something, task is cancelled: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof TaskCompletedEvent)
            logger.info("Do something, task is completed: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof TaskCreatedEvent)
            logger.info("Do something, task is created: " + runtimeEvent.toString());
        else if (runtimeEvent instanceof TaskSuspendedEvent)
            logger.info("Do something, task is suspended: " + runtimeEvent.toString());
        else
            logger.info("Unknown event: " + runtimeEvent.toString());

    }