在之前的内容中,我们已经完成了流程引擎的初始化和启动。一旦流程引擎启动,要执行流程,首先需要将相应的流程模型部署到流程引擎中。本文将会深入探讨流程模型部署的整个过程,从源头到流程引擎的具体执行,一一进行详细的分析。通过这个过程,将会了解到在流程引擎中,是如何将流程模型转换为可供执行的流程定义的。
一、部署入口
在之前的文章中,我们讲过,Activiti 的逻辑通常都通过命令(Cmd)来执行的,流程部署也不例外。其调用方式通过 RepositoryService 的 deploy 方式实现:
DeploymentBuilder createDeployment();
也可以通过 DeploymentBuilder 的 deploy 方法,不过 DeploymentBuilder 的 deploy 方法实际上调用的也是 RepositoryService 的 deploy 方法:
@Override
public Deployment deploy() {
return repositoryService.deploy(this);
}
而实际开发中,我们通常采用如下方式来部署流程:
repositoryService
.createDeployment()
.addClasspathResource("xxx.bpmn.xml")
.deploy();
即通过 RepositoryService 创建 DeploymentBuilder 的实例对象,然后设置对应的参数,如资源文件、部署名称等。总的来说,最终的部署都是由 RepositoryService 的 deploy 方法来实现,而 RepositoryService 的 deploy 方法则通过命令 DeployCmd 来实现:
public Deployment deploy(DeploymentBuilderImpl deploymentBuilder) {
return commandExecutor.execute(new DeployCmd<Deployment>(deploymentBuilder));
}
关于 Cmd 的执行,我们之前已经讲解过调用过程,简单来说,就是从 CommandExecutor 开始,经过一系列拦截器,最终由 CommandInvoker 调用 Cmd 的 execute 方法,接下来我们再详细解释 CommandInvoker 是如何执行 Cmd 的。
二、CommandInvoker分析
在分析 CommandInvoker 之前,我们需先了解 FlowableEngineAgenda,因为在 CommandInvoker 的实现依赖于 FlowableEngineAgenda。因此我们先来了解 FlowableEngineAgenda 默认实现 DefaultFlowableEngineAgenda,其原理如下图所示:
在 DefaultFlowableEngineAgenda 中包含一个名为 operations 的链表(LinkedList),Cmd 在执行的过程中,将需要执行的 Runable(或者 AbstractOperation,AbstractOperation 继承自 Runable)先通过 add 方法将其添加到链表的末尾,执行的时候,再从链表的最前面开始获取(poll 方法),其作用相当于先进先出(FIFO)的队列。
如下是DefaultFlowableEngineAgenda的依赖关系,在Activiti中是没有AbstractAgenda抽象类 ,在flowable中operations是被定义在这个类中,并且新增了List<ExecuteFutureActionOperation<?>> futureOperations参数,用来存储待执行的未来的一些异步操作,比如定时任务。
接下里我们再来看 CommandInvoker 的执行逻辑,首先判断当前的命令上下文是否正在被复用(reused),以及Agenda是否为空。如果正在复用上下文且议程不为空,说明已经有一个议程循环正在执行,此时直接执行命令并返回结果。之后如果不是复用上下文或Agenda为空,那么创建一个新的操作计划,将待执行的命令包装在一个 Runnable 中,,因为DefaultFlowableEngineAgenda接收的参数是 Runnable,在 Runnable 的 run 方法中再去调用 Cmd 的 execute 方法:
这里flowable考虑了上下文是否复用的情况,以提高执行效率,Activiti是没有的。
public <T> T execute(final CommandConfig config, final Command<T> command, CommandExecutor commandExecutor) {
final CommandContext commandContext = Context.getCommandContext();
FlowableEngineAgenda agenda = CommandContextUtil.getAgenda(commandContext);
if (commandContext.isReused() && !agenda.isEmpty()) { // there is already an agenda loop being executed
return (T) command.execute(commandContext);
} else {
// Execute the command.
// This will produce operations that will be put on the agenda.
agenda.planOperation(new Runnable() {
@Override
public void run() {
commandContext.setResult(command.execute(commandContext));
}
});
// Run loop for agenda
executeOperations(commandContext);
// At the end, call the execution tree change listeners.
// TODO: optimization: only do this when the tree has actually changed (ie check dbSqlSession).
if (!commandContext.isReused() && CommandContextUtil.hasInvolvedExecutions(commandContext)) {
agenda.planExecuteInactiveBehaviorsOperation();
executeOperations(commandContext);
}
return (T) commandContext.getResult();
}
}
对于Runnable的执行,CommandInvoker先将其放入FlowableEngineAgenda的operations 中,然后通过循环获取operations的Runnable来执行:
protected void executeOperations(final CommandContext commandContext) {
FlowableEngineAgenda agenda = CommandContextUtil.getAgenda(commandContext);
while (!agenda.isEmpty()) {
Runnable runnable = agenda.getNextOperation();
executeOperation(commandContext, runnable);
}
}
最终的执行则是调用Runnable的run方法来实现:
public void executeOperation(CommandContext commandContext, Runnable runnable) {
if (runnable instanceof AbstractOperation) {
AbstractOperation operation = (AbstractOperation) runnable;
// Execute the operation if the operation has no execution (i.e. it's an operation not working on a process instance)
// or the operation has an execution and it is not ended
if (operation.getExecution() == null || !operation.getExecution().isEnded()) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing operation {}", operation.getClass());
}
agendaOperationRunner.executeOperation(commandContext, operation);
}
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing operation {}", runnable.getClass());
}
runnable.run();
}
}
整体来讲,CommandInvoker 是先将 Cmd 封装到 Runnable 中,将其放入 FlowableEngineAgenda 的操作数链表中,然后通过循环获取操作数链表中的 Runnable,再调用其 run 方进行法执,执行过程中如果产生了新的 Runnable 或 AbstractOperation 则继续加入到 DefaultActivitiEngineAgenda 的操作数链表,直到操作数链表中的数据为空,结束执行。
三、DeployCmd分析
对 Cmd 的执行机制有所了解后,我们再来看 DeployCmd 的实现,其核心逻辑在 executeDeploy 方法,我们将该方法分为 3 部分来讲:
(1)判断是否需要去重:
if (deploymentBuilder.isDuplicateFilterEnabled()) {
List<Deployment> existingDeployments = new ArrayList<>();
if (deployment.getTenantId() == null || ProcessEngineConfiguration.NO_TENANT_ID.equals(deployment.getTenantId())) {
List<Deployment> deploymentEntities = new DeploymentQueryImpl(processEngineConfiguration.getCommandExecutor())
.deploymentName(deployment.getName())
.orderByDeploymentTime().desc()
.listPage(0, 1);
if (!deploymentEntities.isEmpty()) {
existingDeployments.add(deploymentEntities.get(0));
}
} else {
List<Deployment> deploymentList = processEngineConfiguration.getRepositoryService().createDeploymentQuery()
.deploymentName(deployment.getName())
.deploymentTenantId(deployment.getTenantId())
.orderByDeploymentTime().desc()
.listPage(0, 1);
if (!deploymentList.isEmpty()) {
existingDeployments.addAll(deploymentList);
}
}
if (!existingDeployments.isEmpty()) {
DeploymentEntity existingDeployment = (DeploymentEntity) existingDeployments.get(0);
if (!deploymentsDiffer(deployment, existingDeployment)) {
return existingDeployment;
}
}
}
如果启用了重复过滤器(DeploymentBuilderImpl 的 isDuplicateFilterEnabled 属性isDuplicateFilterEnabled()默认为false),检查是否存在相同名称的最新部署。如果存在相同名称的最新部署且内容相同,直接返回该部署,避免重复部署。这里也判断了是否存在租户使用,部署的租户ID为null或为特定值ProcessEngineConfiguration.NO_TENANT_ID,则查询不带租户ID的最新部署。
(2)插入部署数据并发送事件
// Save the data
processEngineConfiguration.getDeploymentEntityManager().insert(deployment);
if (StringUtils.isEmpty(deployment.getParentDeploymentId())) {
// If no parent deployment id is set then set the current ID as the parent
// If something was deployed via this command than this deployment would
// be a parent deployment to other potential child deployments
deployment.setParentDeploymentId(deployment.getId());
}
FlowableEventDispatcher eventDispatcher = processEngineConfiguration.getEventDispatcher();
if (eventDispatcher != null && eventDispatcher.isEnabled()) {
eventDispatcher.dispatchEvent(FlowableEventBuilder.createEntityEvent(FlowableEngineEventType.ENTITY_CREATED, deployment),
processEngineConfiguration.getEngineCfgKey());
}
如果不需要去重,或者不存在已部署的对象,则部署数据,插入的数据包括两个部分,一是 DeployEntityImpl,会将其插入到 ACT_RE_DEPLOYMENT 表中,另一个是 bpmn20.xml 流程模型文件对应的二进制数据,会将其保存到 ACT_GE_BYTEARRAY 表中。需要注意的是,虽然这里调用 insert 方法,但其实没有真实触发插入数据库的动作,目前只是将数据放入缓存中。数据插入成功后,还会触发 Deployment 实例对象创建的事件。
如下是DbSqlSession中的insert方法,insertedObjects、entityCache都是map。
public void insert(Entity entity, IdGenerator idGenerator) {
if (entity.getId() == null) {
String id = idGenerator.getNextId();
if (dbSqlSessionFactory.isUsePrefixId()) {
id = entity.getIdPrefix() + id;
}
entity.setId(id);
}
Class<? extends Entity> clazz = entity.getClass();
if (!insertedObjects.containsKey(clazz)) {
insertedObjects.put(clazz, new LinkedHashMap<>()); // order of insert is important, hence LinkedHashMap
}
insertedObjects.get(clazz).put(entity.getId(), entity);
entityCache.put(entity, false); // False -> entity is inserted, so always changed
entity.setInserted(true);
}
(3)执行部署
// Actually deploy
processEngineConfiguration.getDeploymentManager().deploy(deployment, deploymentSettings);
if (deploymentBuilder.getProcessDefinitionsActivationDate() != null) {
scheduleProcessDefinitionActivation(commandContext, deployment);
}
if (eventDispatcher != null && eventDispatcher.isEnabled()) {
eventDispatcher.dispatchEvent(FlowableEventBuilder.createEntityEvent(FlowableEngineEventType.ENTITY_INITIALIZED, deployment),
processEngineConfiguration.getEngineCfgKey());
}
return deployment;
执行部署具体逻辑由BpmnDeployer的deploy完成。完成部署后,需要判断是否要在指定时间激活,即判断是否有设定激活时间属性:processDefinitionsActivationDate,如果设定了激活时间,则调用scheduleProcessDefinitionActivation方法,该方法的实现方式是部署完后先将对应的流程定义挂起,再生成一个定时任务,在指定的时候对该流程定义进行解挂。这样就实现了指定时间激活流程定义的功能。
四、BpmnDeployer 源码分析
接下来再来详细分析 BpmnDeployer 部署流程的过程。 为了方便理解,先可以看下 BpmnDeployer 部署流程的时序图:
BpmnDeployer主要功能即将对象的XML文件转换为对应的BpmnModel对象,生成流程定义信息,即流程定义的ID、名称、描述等信息。其执行是在DeployCmd中通过DeploymentManager的deploy方法,再调用对应BpmnDeployer的deploy方法:
public void deploy(DeploymentEntity deployment, Map<String, Object> deploymentSettings) {
for (EngineDeployer deployer : deployers) {
deployer.deploy(deployment, deploymentSettings);
}
}
这里的deployers是一个集合,即可以存在多个部署器。如下:
在前文分析流程引擎初始化方法initDeployers()的时候,我们已经讲过默认会初始化一个BpmnDeployer,当然还可能存在其他部署器,比如集成Flowable的规则引擎,则会加入RulesDeployer部署器,但要注意的是,所有部署器接受的参数是一致的,所以需要部署器对部署的资源进行区分,例如,RulesDeployer只处理资源名以**.drl**结尾的资源:
当然,BpmnDeployer也只会处理以 “bpmn20.xml” 或 “bpmn” 结尾的资源文件。
接下来我们逐行分析一下 BpmnDeployer 的 deploy 方法:
@Override
public void deploy(EngineDeployment deployment, Map<String, Object> deploymentSettings) {
LOGGER.debug("Processing deployment {}", deployment.getName());
// The ParsedDeployment represents the deployment, the process definitions, and the BPMN
// resource, parse, and model associated with each process definition.
// 通过 parsedDeploymentBuilderFactory 工厂的 build 方法生成 ParsedDeployment 对象,在 build 方法中,
// 会完成对 bpmn 的 xml 文件的解析,返回的 ParsedDeployment 中包含流程定义信息。
ParsedDeployment parsedDeployment = parsedDeploymentBuilderFactory
.getBuilderForDeploymentAndSettings(deployment, deploymentSettings)
.build();
// 校验本次部署的流程定义中是否存在重复的 key,在同一次部署的时候可以部署多个流程,但是流程key不能重复
bpmnDeploymentHelper.verifyProcessDefinitionsDoNotShareKeys(parsedDeployment.getAllProcessDefinitions());
// 将部署的值复制到每个流程定义中,以确保流程定义中的属性与部署一致
bpmnDeploymentHelper.copyDeploymentValuesToProcessDefinitions(
parsedDeployment.getDeployment(), parsedDeployment.getAllProcessDefinitions());
// 设置流程定义的资源名称
bpmnDeploymentHelper.setResourceNamesOnProcessDefinitions(parsedDeployment);
// 如果需要,创建并持久化新的流程图。如果涉及将流程定义的BPMN图表生成为图像文件并进行持久化那就是需要的
createAndPersistNewDiagramsIfNeeded(parsedDeployment);
// 设置流程定义的图表名称。
setProcessDefinitionDiagramNames(parsedDeployment);
// 检查部署是否为新部署,根据不同情况执行以下操作:
if (deployment.isNew()) {
//如果不是派生部署(没有特定的标志),则获取先前版本的流程定义,更新流程定义的版本和ID,持久化流程定义和授权,更新定时器和事件。
//如果是派生部署,则获取先前派生版本的流程定义,设置派生的流程定义版本和ID,持久化流程定义和授权。
if (!deploymentSettings.containsKey(DeploymentSettings.IS_DERIVED_DEPLOYMENT)) {
Map<ProcessDefinitionEntity, ProcessDefinitionEntity> mapOfNewProcessDefinitionToPreviousVersion = getPreviousVersionsOfProcessDefinitions(parsedDeployment);
// 针对每个流程定义实体,设置版本和标识符。如果在给定的 map 中存在旧版本的流程定义实体,则将版本设置为该旧实体的版本加一;否则,版本设置为1。
// 流程定义的 ID 则由 3 部分组成,流程 key、版本号和由 ID 生成器生成的 ID,三者之间用冒号分开。如果生成的 ID 长度大于 64 位了,则流程定义 ID 直接用 ID 生成器生成的 ID,不再加 key 和版本号。
// 在此方法中为新的流程定义实体设置唯一的标识符。获取流程模型中的初始元素,即流程的起始节点。
// 如果事件分发器已启用,将创建 ENTITY_CREATED 事件,并使用事件分发器将事件分派出去,通知流程引擎监听器有新的实体被创建。
setProcessDefinitionVersionsAndIds(parsedDeployment, mapOfNewProcessDefinitionToPreviousVersion);
// 将解析过的流程定义实体持久化保存到数据库中,并为这些新的流程定义添加授权信息,以便后续可以进行相应的操作和权限控制。
// 在保存流程定义之前,确保流程定义实体的所有值已正确设置。方法的执行前提是部署是新的,流程定义尚未保存过,并且其所有属性已准备就绪。
persistProcessDefinitionsAndAuthorizations(parsedDeployment);
// 更新流程定义中的定时器(timers)和事件(events)。对于每个流程定义实体,执行以下操作:
// - 调用 bpmnDeploymentHelper 的 updateTimersAndEvents 方法,传递当前流程定义实体、对应的旧版本流程定义实体以及解析过的部署信息。
// 确保在部署过程中,对于新版本的流程定义,定时器和事件的定义能够正确地与旧版本进行比较和更新。
// 这可以确保在部署升级时,已经存在的定时器和事件能够正确地进行迁移和更新,以便后续流程实例的执行和管理。
updateTimersAndEvents(parsedDeployment, mapOfNewProcessDefinitionToPreviousVersion);
} else {
Map<ProcessDefinitionEntity, ProcessDefinitionEntity> mapOfNewProcessDefinitionToPreviousDerivedVersion =
getPreviousDerivedFromVersionsOfProcessDefinitions(parsedDeployment);
// 为存在历史版本的流程定义设置适当的版本号、标识和属性,并在必要时进行事件分发,以便后续的流程实例启动和管理。
setDerivedProcessDefinitionVersionsAndIds(parsedDeployment, mapOfNewProcessDefinitionToPreviousDerivedVersion, deploymentSettings);
persistProcessDefinitionsAndAuthorizations(parsedDeployment);
}
} else {// 如果不是新部署,确保流程定义与已持久化版本保持一致。
makeProcessDefinitionsConsistentWithPersistedVersions(parsedDeployment);
}
// 更新缓存和工件,以确保流程定义的最新状态
cachingAndArtifactsManager.updateCachingAndArtifacts(parsedDeployment);
// 如果是新部署,触发流程定义实体初始化事件
// 此步骤在Activiti中是没有的
if (deployment.isNew()) {
dispatchProcessDefinitionEntityInitializedEvent(parsedDeployment);
}
// 对每个流程定义进行以下操作:
// - 获取与流程定义关联的 BPMN 模型。
// - 根据流程定义的 ID 和 key 创建本地化值,用于国际化显示。
for (ProcessDefinitionEntity processDefinition : parsedDeployment.getAllProcessDefinitions()) {
BpmnModel bpmnModel = parsedDeployment.getBpmnModelForProcessDefinition(processDefinition);
createLocalizationValues(processDefinition.getId(), bpmnModel.getProcessById(processDefinition.getKey()));
}
}
五、总结
本文详细剖析了流程部署的过程。总的来说,流程部署的核心在于将流程模型的 XML 文件解析为 BpmnModel 对象,然后生成流程定义信息,并最终对 BpmnModel 对象和流程定义信息进行缓存。尽管如此,本文尚未对将流程模型的 XML 文件转换为 BpmnModel 对象的具体过程进行深入探讨。而这个转换过程恰恰是流程部署的关键一步。
在下一篇文章中,我们将会详细解析将流程模型的 XML 文件转换为 BpmnModel 对象的全过程。通过深入研究这个过程,您将更加清晰地了解 Activiti 中是如何将流程模型的描述转化为可执行的流程定义信息的。敬请期待下文的详细讲解。