领先一步
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 和我一起做了一个网络研讨会,它应该能让你很好地了解 Activiti 对 Spring Boot 集成的基础知识。Activiti 用户指南中关于 Spring Boot 的章节也是获取更多信息的好地方。
本例的代码可以在我的 Github 仓库中找到。
我们在此实现的流程是一个开发者招聘流程。当然,它经过了简化(以便适应本网页),但你应该能掌握核心概念。流程图如下:
如引言中所述,这里的每种形状都因 BPMN 2.0 标准而具有非常具体的解释。但即使不了解 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);
}
};
}
所以这里发生的事情是,我们创建了一个运行流程所需所有变量的 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
网站下载的 .zip
文件中),一个展示引擎众多功能的 Web 应用,...