领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多这篇文章是由 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 是最好的选择!
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);
}
}
您现在就可以运行此应用程序,它不会执行任何功能操作,但在后台它已经
让我们运行一些内容。将 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
网站 获取的.zip
下载文件中),一个展示引擎许多功能的 Web 应用程序……