flowable源码解析之流程部署

在之前的内容中,我们已经完成了流程引擎的初始化和启动。一旦流程引擎启动,要执行流程,首先需要将相应的流程模型部署到流程引擎中。本文将会深入探讨流程模型部署的整个过程,从源头到流程引擎的具体执行,一一进行详细的分析。通过这个过程,将会了解到在流程引擎中,是如何将流程模型转换为可供执行的流程定义的。

一、部署入口

在之前的文章中,我们讲过,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 中是如何将流程模型的描述转化为可执行的流程定义信息的。敬请期待下文的详细讲解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flowable是一个开源的流程引擎,可以用于处理和管理各种类型的业务流程Flowable源码可以在其官方的GitHub仓库上找到,具体地址是https://github.com/flowable/flowable-engine/releases/tag/flowable-6.7.2。 Flowable的启动流程有两种方式,但最终都是执行了StartProcessInstanceCmd命令。在我以流程key方式启动来分析源码中,启动流程的入口是通过runtimeService.startProcessInstance方法来实现的。 通过研究Flowable源码,可以深入了解其内部的实现机制,从而更好地理解Flowable的工作原理和使用方法。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [flowable 6.7.2 源码压缩包](https://download.csdn.net/download/weixin_44393822/86790116)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [flowable部署和启动源码解析](https://blog.csdn.net/u012483153/article/details/106736343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Flowable流程启动源码分析](https://blog.csdn.net/CH_PaulFrank/article/details/116800070)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值