Activiti 与 Spring Boot 入门

工程 | Josh Long | 2015 年 3 月 8 日 | ...

本文是 Activiti 联合创始人兼社区成员 Joram Barrez (@jbarrez) 的客座文章,他供职于 Alfresco。感谢 Joram!我希望能看到更多类似的社区客座文章,所以——像往常一样——不要犹豫,如果你有想法或贡献,请随时联系我 (@starbuxman)!-Josh


引言

Activiti 是一个采用 Apache 许可的业务流程管理 (BPM) 引擎。这类引擎的核心目标是获取由人工任务和服务调用组成的流程定义,并按特定顺序执行它们,同时暴露各种 API 来启动、管理和查询该流程定义的流程实例数据。与许多竞争对手不同,Activiti 轻量级且易于与任何 Java 技术或项目集成。所有这些,并且它可以在任何规模下工作——从几十个到数千甚至数百万个流程执行。

Activiti 的源代码可以在 Github 上找到。该项目由 Alfresco 创建并赞助,但也得到了全球各行各业的贡献。

流程定义通常被可视化为流程图式的图表。近年来,BPMN 2.0 标准(一个 OMG 标准,类似 UML)已成为这些图表的实际“语言”。该标准定义了图表上的特定形状应如何解释,无论是在技术上还是在业务上,以及它如何存储,即以非时髦的 XML 文件形式... 但幸运的是,大多数工具会为你隐藏这些细节。这是一个标准,你可以使用任何数量的兼容工具来设计(甚至运行)你的 BPMN 流程。话虽如此,如果你问我,没有比 Activiti 更好的选择了!

Spring Boot 集成

Activiti 和 Spring 很好地协同工作。Spring Boot 的约定优于配置方法与 Activiti 流程引擎的设置和使用完美契合。开箱即用,你只需要一个数据库,因为流程执行可能持续几秒到几年不等。显然,作为流程定义的内在组成部分,它需要调用各种系统并与之交互数据,涉及各种技术。借助 Spring Boot 添加所需的依赖并集成各种(样板)逻辑的简便性,使得这一切变得非常容易。

在微服务架构中使用 Spring Boot 和 Activiti 也非常有意义。Spring Boot 可以轻松快速地启动并运行一个生产就绪的服务,而在分布式微服务架构中,Activiti 流程可以将各种微服务粘合在一起,同时融入人工工作流程(任务和表单)来实现特定目标。

Activiti 中的 Spring Boot 集成是由 Spring 专家 Josh Long 创建的。几个月前,Josh 和我一起做了一个网络研讨会,它应该能让你很好地了解 Activiti 对 Spring Boot 集成的基础知识。Activiti 用户指南中关于 Spring Boot 的章节也是获取更多信息的好地方。

入门

本例的代码可以在我的 Github 仓库中找到。

我们在此实现的流程是一个开发者招聘流程。当然,它经过了简化(以便适应本网页),但你应该能掌握核心概念。流程图如下:

如引言中所述,这里的每种形状都因 BPMN 2.0 标准而具有非常具体的解释。但即使不了解 BPMN,这个流程也很容易理解:

  • 流程启动时,求职者的简历会存储在一个外部系统中。
  • 然后流程等待电话面试完成。这由用户完成(见角落的小人图标)。
  • 如果电话面试不理想,则发送一封礼貌的拒绝邮件。否则,应进行技术面试和财务谈判。
  • 请注意,申请人可以在任何时候取消。这在图表中显示为大矩形边界上的事件。当事件发生时,矩形内的一切都将被终止,流程停止。
  • 如果一切顺利,则发送欢迎邮件。

这是此流程的 BPMN 定义

让我们创建一个新的 Maven 项目,并添加 Spring Boot、Activiti 和数据库所需的依赖。为了简单起见,我们将使用内存数据库。

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-basic</artifactId>
    <version>${activiti.version}</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.185</version>
</dependency>

因此,创建一个最基本的 Spring Boot + Activiti 应用只需要两个依赖项

@SpringBootApplication
public class MyApp {

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

你现在就可以运行这个应用了,它在功能上不会做任何事情,但在幕后它已经

  • 创建一个内存 H2 数据库
  • 使用该数据库创建一个 Activiti 流程引擎
  • 将所有 Activiti 服务暴露为 Spring Bean
  • 配置各种细节,例如 Activiti 异步作业执行器、邮件服务器等。

让我们来运行一些东西。将 BPMN 2.0 流程定义文件放入 src/main/resources/processes 文件夹。放置在这里的所有流程都会自动部署(即解析并使其可执行)到 Activiti 引擎。为了简单起见,我们先创建一个 CommanLineRunner,它将在应用启动时执行

@Bean
CommandLineRunner init( final RepositoryService repositoryService,
                              final RuntimeService runtimeService,
                              final TaskService taskService) {

    return new CommandLineRunner() {

        public void run(String... strings) throws Exception {
            Map<String, Object> variables = new HashMap<String, Object>();
            variables.put("applicantName", "John Doe");
            variables.put("email", "[email protected]");
            variables.put("phoneNumber", "123456789");
            runtimeService.startProcessInstanceByKey("hireProcess", variables);
        }
    };
}

所以这里发生的事情是,我们创建了一个运行流程所需所有变量的 Map,并在启动流程时将其传递。如果你查看流程定义文件,你会看到我们在许多地方(例如任务描述中)使用 ${variableName} 引用了这些变量。

流程的第一步是一个自动步骤(见小齿轮图标),它是使用引用 Spring Bean 的表达式实现的

其实现如下

activiti:expression="${resumeService.storeResume()}"

当然,我们需要这个 bean,否则流程将无法启动。所以我们来创建它

@Component
public class ResumeService {

    public void storeResume() {
        System.out.println("Storing resume ...");
    }

}

现在运行应用,你会看到该 bean 被调用

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.0.RELEASE)

2015-02-16 11:55:11.129  INFO 304 --- [           main] MyApp                                    : Starting MyApp on The-Activiti-Machine.local with PID 304 ...
Storing resume ...
2015-02-16 11:55:13.662  INFO 304 --- [           main] MyApp                                    : Started MyApp in 2.788 seconds (JVM running for 3.067)

就是这样!恭喜你使用 Activiti 在 Spring Boot 中运行了你的第一个流程实例!

让我们稍微增加一些趣味,在 pom.xml 中添加以下依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-rest-api</artifactId>
    <version>${activiti.version}}</version>
</dependency>

将其添加到 classpath 中会做一件巧妙的事情:它会将 Activiti REST API(使用 Spring MVC 编写)完全暴露在你的应用中。Activiti 的 REST API 在 Activiti 用户指南中有完整文档

REST API 通过基本认证进行保护,默认情况下没有任何用户。让我们按照如下所示为系统添加一个管理员用户(将其添加到 MyApp 类中)。当然,不要在生产系统中使用这种方式,生产环境中你会希望将认证与 LDAP 或其他服务集成。

@Bean
InitializingBean usersAndGroupsInitializer(final IdentityService identityService) {

    return new InitializingBean() {
        public void afterPropertiesSet() throws Exception {

            Group group = identityService.newGroup("user");
            group.setName("users");
            group.setType("security-role");
            identityService.saveGroup(group);

            User admin = identityService.newUser("admin");
            admin.setPassword("admin");
            identityService.saveUser(admin);

        }
    };
}

启动应用。我们现在可以像在 CommandLineRunner 中一样启动流程实例,但这次使用 REST

curl -u admin:admin -H "Content-Type: application/json" -d '{"processDefinitionKey":"hireProcess", "variables": [ {"name":"applicantName", "value":"John Doe"}, {"name":"email", "value":"[email protected]"}, {"name":"phoneNumber", "value":"1234567"} ]}' http://localhost:8080/runtime/process-instances

它会返回流程实例的 JSON 表示

{
     "tenantId": "",
     "url": "http://localhost:8080/runtime/process-instances/5",
     "activityId": "sid-42BAE58A-8FFB-4B02-AAED-E0D8EA5A7E39",
     "id": "5",
     "processDefinitionUrl": "http://localhost:8080/repository/process-definitions/hireProcess:1:4",
     "suspended": false,
     "completed": false,
     "ended": false,
     "businessKey": null,
     "variables": [],
     "processDefinitionId": "hireProcess:1:4"
}

我只想停下来赞叹一下这有多酷。只需添加一个依赖,你就能在应用中嵌入整个 Activiti REST API!

让我们让它更酷,添加以下依赖

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>${activiti.version}</version>
</dependency>

这会为 Activiti 添加一个 Spring Boot actuator 端点。如果我们重启应用,并访问 http://localhost:8080/activiti/,我们会看到一些关于我们流程的基本统计信息。稍微想象一下,在一个真实系统中,你部署和执行着许多流程定义,你就能明白这有多么有用。

相同的 actuator 也注册为一个 JMX bean,暴露出类似的信息。

{
    completedTaskCountToday: 0,
    deployedProcessDefinitions: [
       "hireProcess (v1)"
    ],
    processDefinitionCount: 1,
   cachedProcessDefinitionCount: 1,
   runningProcessInstanceCount: {
       hireProcess (v1): 0
    },
    completedTaskCount: 0,
    completedActivities: 0,
    completedProcessInstanceCount: {
        hireProcess (v1): 0
    },
    openTaskCount: 0
}

为了完成我们的编码,让我们为招聘流程创建一个专门的 REST 端点,例如可以由一个 JavaScript Web 应用(不在本文讨论范围)调用。所以很可能,我们会有一个表单供申请人填写我们之前以编程方式传递的详细信息。顺便说一句,让我们将申请人信息存储为一个 JPA 实体。在这种情况下,数据将不再存储在 Activiti 中,而是在一个单独的表中,Activiti 需要时再引用。

你可能已经猜到了,通过添加一个依赖可以启用 JPA 支持

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>spring-boot-starter-jpa</artifactId>
    <version>${activiti.version}</version>
</dependency>

并将实体添加到 MyApp 类中


@Entity
class Applicant {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private String email;

    private String phoneNumber;

    // Getters and setters

我们还需要一个用于此实体的 Repository(将其放在单独的文件中或也在 MyApp 中)。不需要任何方法,Spring 的 Repository 神奇功能会为我们生成所需的方法。

public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
  // .. 
}

现在我们可以创建专用的 REST 端点了


@RestController
public class MyRestController {

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private ApplicantRepository applicantRepository;

    @RequestMapping(value="/start-hire-process", method= RequestMethod.POST, produces= MediaType.APPLICATION_JSON_VALUE)
    public void startHireProcess(@RequestBody Map<String, String> data) {

        Applicant applicant = new Applicant(data.get("name"), data.get("email"), data.get("phoneNumber"));
        applicantRepository.save(applicant);

        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("applicant", applicant);
        runtimeService.startProcessInstanceByKey("hireProcessWithJpa", variables);
    } 
}

注意,我们现在使用的是一个名为 'hireProcessWithJpa' 的略有不同的流程,它进行了一些调整以适应数据现在位于 JPA 实体中的事实。例如,我们不能再使用 ${applicantName},现在必须使用 ${applicant.name}。

让我们重启应用并启动一个新的流程实例

curl -u admin:admin -H "Content-Type: application/json" -d '{"name":"John Doe", "email": "[email protected]", "phoneNumber":"123456789"}' http://localhost:8080/start-hire-process

我们现在可以走完整个流程了。你也可以为此创建自定义端点,暴露带有不同表单的不同任务查询......但这留给你的想象力,我将使用默认的 Activiti REST 端点来演示流程的推进。

让我们看看当前流程实例处于哪个任务(你可以在这里传递更详细的参数,例如 'processInstanceId' 以进行更好的过滤)

curl -u admin:admin -H "Content-Type: application/json" http://localhost:8080/runtime/tasks

它返回

{
     "order": "asc",
     "size": 1,
     "sort": "id",
     "total": 1,
     "data": [{
          "id": "14",
          "processInstanceId": "8",
          "createTime": "2015-02-16T13:11:26.078+01:00",
          "description": "Conduct a telephone interview with John Doe. Phone number = 123456789",
          "name": "Telephone interview"
          ...
     }],
     "start": 0
}

所以,我们的流程现在处于 Telephone interview(电话面试)任务。在一个实际的应用中,会有一个任务列表和一个可以填写以完成此任务的表单。我们来完成这个任务(我们需要设置 telephoneInterviewOutcome 变量,因为排他网关使用它来决定流程走向)

curl -u admin:admin -H "Content-Type: application/json" -d '{"action" : "complete", "variables": [ {"name":"telephoneInterviewOutcome", "value":true} ]}' http://localhost:8080/runtime/tasks/14

现在当我们再次获取任务时,流程实例已经进入子流程(大矩形)中的两个并行任务

{
     "order": "asc",
     "size": 2,
     "sort": "id",
     "total": 2,
     "data": [
          {
              ...
               "name": "Tech interview"
          },
          {
              ...
               "name": "Financial negotiation"
          }
     ],
     "start": 0
}

现在我们可以以类似的方式继续完成流程的其余部分,但这留给你自己去尝试了。

测试

使用 Activiti 创建业务流程的优势之一在于一切都只是 Java。因此,流程可以像常规 Java 代码一样通过单元测试进行测试。Spring Boot 让编写此类测试变得非常容易。

以下是“顺利路径”(happy path)的单元测试代码(省略了 @Autowired 字段和测试邮件服务器设置)。代码还展示了如何使用 Activiti API 查询给定组和流程实例的任务。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {MyApp.class})
@WebAppConfiguration
@IntegrationTest
public class HireProcessTest {

    @Test
    public void testHappyPath() {

        // Create test applicant
        Applicant applicant = new Applicant("John Doe", "[email protected]", "12344");
        applicantRepository.save(applicant);

        // Start process instance
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("applicant", applicant);
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("hireProcessWithJpa", variables);

        // First, the 'phone interview' should be active
        Task task = taskService.createTaskQuery()
                .processInstanceId(processInstance.getId())
                .taskCandidateGroup("dev-managers")
                .singleResult();
        Assert.assertEquals("Telephone interview", task.getName());

        // Completing the phone interview with success should trigger two new tasks
        Map<String, Object> taskVariables = new HashMap<String, Object>();
        taskVariables.put("telephoneInterviewOutcome", true);
        taskService.complete(task.getId(), taskVariables);

        List<Task> tasks = taskService.createTaskQuery()
                .processInstanceId(processInstance.getId())
                .orderByTaskName().asc()
                .list();
        Assert.assertEquals(2, tasks.size());
        Assert.assertEquals("Financial negotiation", tasks.get(0).getName());
        Assert.assertEquals("Tech interview", tasks.get(1).getName());

        // Completing both should wrap up the subprocess, send out the 'welcome mail' and end the process instance
        taskVariables = new HashMap<String, Object>();
        taskVariables.put("techOk", true);
        taskService.complete(tasks.get(0).getId(), taskVariables);

        taskVariables = new HashMap<String, Object>();
        taskVariables.put("financialOk", true);
        taskService.complete(tasks.get(1).getId(), taskVariables);

        // Verify email
        Assert.assertEquals(1, wiser.getMessages().size());

        // Verify process completed
        Assert.assertEquals(1, historyService.createHistoricProcessInstanceQuery().finished().count());

    }

下一步

  • 我们还没有涉及 Activiti 周边的任何工具。除了引擎本身,还有很多工具,比如用于设计流程的 Eclipse 插件,云端的免费 Web 编辑器(也包含在你可以从 Activiti 网站下载的 .zip 文件中),一个展示引擎众多功能的 Web 应用,...
  • Activiti 的当前版本(5.17.0)集成了 Spring Boot 1.1.6。然而,当前的 master 分支版本与 1.2.1 兼容。
  • 使用 Spring Boot 1.2.0 带来了许多好东西,比如支持带有 JTA 的 XA 事务。这意味着你可以轻松地将你的流程与 JMS、JPA 和 Activiti 逻辑全部连接到同一个事务中!......这引出了下一点...
  • 在本例中,我们重点关注了人机交互(而且只是蜻蜓点水)。但你也可以在系统编排方面做很多事情。Spring Boot 集成还支持 Spring Integration,你可以利用它以非常简洁的方式做到这一点!
  • 当然,关于 BPMN 2.0 标准还有很多很多。在Activiti 文档中阅读更多内容。

订阅 Spring 邮件列表

订阅 Spring 邮件列表,保持联系

订阅

领先一步

VMware 提供培训和认证,助你快速提升。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅,为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将举办的活动

查看 Spring 社区即将举办的所有活动。

查看全部