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 进行了一次网络研讨会,应该可以帮助您深入了解 Spring Boot 的 Activiti 集成的基础知识。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);
        }
    };
}

这里发生的事情是,我们创建所有运行流程所需变量的映射,并在启动流程时传递它。如果您检查流程定义,您将看到我们在许多地方使用 `${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)

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

让我们稍微改进一下,并将以下依赖项添加到我们的 pom.xml 中

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

在类路径中添加此项会执行一项巧妙的操作:它会获取 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"} ]}' https://127.0.0.1:8080/runtime/process-instances

这将返回流程实例的 JSON 表示。

{
     "tenantId": "",
     "url": "https://127.0.0.1:8080/runtime/process-instances/5",
     "activityId": "sid-42BAE58A-8FFB-4B02-AAED-E0D8EA5A7E39",
     "id": "5",
     "processDefinitionUrl": "https://127.0.0.1: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 执行器端点。如果我们重新启动应用程序并访问 `https://127.0.0.1:8080/activiti/`,我们将获得有关流程的一些基本统计信息。您可以想象一下,在一个实时系统中,您部署并执行了更多流程定义,您可以看到这有多么有用。

相同的执行器也注册为一个 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"}' https://127.0.0.1:8080/start-hire-process

现在我们可以完成我们的流程。你也可以为此创建自定义端点,公开具有不同表单的不同任务查询……但我将把它留给你的想象力,并使用默认的 Activiti REST 端点来完成流程。

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

curl -u admin:admin -H "Content-Type: application/json" https://127.0.0.1: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
}

所以,我们的流程现在处于电话面试阶段。在一个真实的应用程序中,将会有一个任务列表和一个表单,可以填写以完成此任务。让我们完成此任务(我们必须设置telephoneInterviewOutcome变量,因为排他网关使用它来路由执行)

curl -u admin:admin -H "Content-Type: application/json" -d '{"action" : "complete", "variables": [ {"name":"telephoneInterviewOutcome", "value":true} ]}' https://127.0.0.1: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 使编写此类测试变得轻而易举。

以下是“成功路径”的单元测试代码示例(省略了@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 集成。但是,当前主版本与 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社区中所有即将举行的活动。

查看全部