Camunda动态生成工作流流程定义并部署更新流程(新手上路版)

8,744 阅读2分钟

环境: Spring-boot 嵌入式开发 、Canmunda版本7.16.0 、Canmunda依赖 camunda-bpm-spring-boot-starter

首先需要熟悉Camunda几个概念:

1. 部署(deploy/deployment):Camunda文档——processes.xml#流程应用程序部署

  • 部署意味着一个Camunda程序的启动,向数据库中注册自己,并上传自身的流程定义文件
  • 在processes.xml文件中,一个<process-archive> 定义意味着一次部署(源码位置org.camunda.bpm.container.impl.deployment.DeployProcessArchivesStep),如果processes.xml文件全空,则会填充默认配置(源码位置org.camunda.bpm.container.impl.deployment.ParseProcessesXmlSteporg.camunda.bpm.application.impl.metadata.spi.ProcessesXml#EMPTY_PROCESSES_XML)。process-archive的name属性不为空时,作为部署的Name属性(数据库字段ACT_RE_DEPLOYMENT#NAME_)。Name留空(默认配置留空)时,部署名采用注解@EnableProcessApplication("appName")否则为spring程序名@Value("${spring.application.name:null}")(源码位置org.camunda.bpm.application.AbstractProcessApplication#getName)

2. 恢复/激活部署(resumed/activate)与注册(register):文档同上,建议看源码注释(org.camunda.bpm.engine.ManagementService)

  • 恢复部署意味着激活一个Camunda应用(ProcessApplication),流程引擎(根据架构选择,流程引擎可能是外部的、共享的,又或者和应用同时启动)将在此应用中执行工作,作业处理器(job executor)也开始获取属于这个部署的流程作业。(如果一个部署没有被激活,属于该部署id的流程定义的流程实例可能无法正常工作——比如具有调用额外java类和资源的流程将无法执行)
  • 当应用程序重启时,会自动发起部署(源码位置org.camunda.bpm.container.impl.deployment.DeployProcessArchiveStep#performOperationStep) 根据部署名(通常也就是process-archive name)查找最近版本部署,然后按byte对比两次部署的资源(.bpmn文件)差异(源码位置org.camunda.bpm.engine.impl.cmd.DeployCmd#resolveResourcesToDeployorg.camunda.bpm.engine.repository.DeploymentHandler#shouldDeployResource),如果完全相同,视为同一个部署,则会恢复部署,若果有差异,则连同所有的流程定义(.bpmn)文件部署一个新版本的同名部署。
  • 根据<property name="isResumePreviousVersions">true</property>(默认)和<property name="resumePreviousBy">process-definition-key</property>(默认)(源码位置org.camunda.bpm.container.impl.deployment.DeployProcessArchiveStep#enableResumingOfPreviousVersionsorg.camunda.bpm.engine.impl.cmd.DeployCmd#registerProcessApplication) 会自动恢复以前的部署,process-definition-key意味着根据这次部署中所有的流程定义,所有包含具有相同processKey的流程定义(只包含一部分也算)的部署都将恢复。deployment-name则意味着所有同名部署都会恢复。

3. isDeployChangedOnly:资源过滤

  • <property name="isDeployChangedOnly">false</property>(默认)
  • 此属性指示是否只应将与最新同名部署不同的资源作为部署的一部分,开启时,会拉取同名最新部署的同名资源进行比较,如果资源有一个byte不同,或者以前的部署没有同名资源才会放入部署列表
  • 根据官网文件档,指示此属性包含副作用,会影响自动恢复部署,但是在源码org.camunda.bpm.engine.impl.cmd.DeployCmd#getProcessDefinitionsFromResources中,将排除的资源也加入了恢复部署的列表(?)
  • 如果开启了此属性,则需要特别小心 CallActivity 的流程步骤,此功能会将新流程与旧流程分隔为两个版本的部署,流程调用中如果绑定方式设为部署,则被调用的流程定义与调用流程定义因为以部署id绑定,可能需要属于同一部署名且同一版本的部署(尚未验证),故绑定方式应设为latest或version(旧版本部署肯定会被恢复/激活/注册,尚未验证旧版本也被恢复/激活/注册的情况下这种调用能不能横跨部署版本)

Java代码动态生成工作流流程定义:

Camunda文档 BPMN API

Camunda文档 Fluent BPMN Builder API

1.原始Api :(搬运官网案例)

有两个并行任务的简单流程

// 创建一个空模型
BpmnModelInstance modelInstance = Bpmn.createEmptyModel();
Definitions definitions = modelInstance.newInstance(Definitions.class);
definitions.setTargetNamespace("http://camunda.org/examples");
modelInstance.setDefinitions(definitions);

// 创建元素
StartEvent startEvent = createElement(process, "start", StartEvent.class);
ParallelGateway fork = createElement(process, "fork", ParallelGateway.class);
ServiceTask task1 = createElement(process, "task1", ServiceTask.class);
task1.setName("Service Task");
UserTask task2 = createElement(process, "task2", UserTask.class);
task2.setName("User Task");
ParallelGateway join = createElement(process, "join", ParallelGateway.class);
EndEvent endEvent = createElement(process, "end", EndEvent.class);

// 创建流
createSequenceFlow(process, startEvent, fork);
createSequenceFlow(process, fork, task1);
createSequenceFlow(process, fork, task2);
createSequenceFlow(process, task1, join);
createSequenceFlow(process, task2, join);
createSequenceFlow(process, join, endEvent);

// 验证模型并将其写入文件
Bpmn.validateModel(modelInstance);
File file = File.createTempFile("bpmn-model-api-", ".bpmn");
Bpmn.writeModelToFile(file, modelInstance);

1.Fluent Api :(搬运官网案例)

BpmnModelInstance modelInstance = Bpmn.createProcess()
  .startEvent()
  .userTask()
  .id("question")
  .exclusiveGateway()
  .name("Everything fine?")
    .condition("yes", "#{fine}")
    .serviceTask()
    .userTask()
    .endEvent()
  .moveToLastGateway()
    .condition("no", "#{!fine}")
    .userTask()
    .connectTo("question")
  .done()

注意点:

节点id必须是合法的Xml NCName,且不能包含汉字、"-" 判断是否汉字用正则表达式,判断是否 Xml NCName 使用:com.sun.org.apache.xerces.internal.util.XMLChar#isValidNCName 注意此函数被idea提示为内部函数,jdk1.8以后不能保证存在

    private static void validXmlChar(String str) {
        //...汉字校验部分
        Assert.isTrue(XMLChar.isValidNCName(str) && !str.contains("-"), "\"" + str + "\" 必须是有效的 Xml NcName 且不包含 '-' ");
    }

部署生成的流程定义:

ProcessEngine 各种Service 的 Bean 声明在org.camunda.bpm.engine.spring.SpringProcessEngineServicesConfiguration
部署时候的配置参考org.camunda.bpm.container.impl.deployment.DeployProcessArchiveStep#performOperationStep

@Service
@RequiredArgsConstructor
public class ProcessReDeployService{
    private final RepositoryService repositoryService;
    private final SpringBootProcessApplication processApplication;
    /**
     * 以指定部署名部署,会生成最新版本的部署,并且激活旧版本的同名部署
     */
    public void deployProcessByName(String processArchiveName, String resourceName, BpmnModelInstance bpmnModelInstance) {
        getDeploymentByDefaultConfig(resourceName, bpmnModelInstance).name(processArchiveName).deploy();
    }
    /**
     * 在指定旧版部署id更新部署
     */
    public void deployProcessUpdateIncrementallyById(String oldDeploymentId, String resourceName, BpmnModelInstance bpmnModelInstance) {
        String processArchiveName = repositoryService.createDeploymentQuery().deploymentId(oldDeploymentId).singleResult().getName();
        getDeploymentByDefaultConfig(resourceName, bpmnModelInstance).name(processArchiveName).addDeploymentResources(oldDeploymentId).deploy();
    }
    /**
     * 在指定部署名更新部署,并指定需要COPY的其他部署的流程定义
     */
    public void deployProcessUpdateIncrementallyByName(String processArchiveName, String resourceName, BpmnModelInstance bpmnModelInstance) {
        Deployment deployment = Optional.ofNullable(repositoryService.createDeploymentQuery()
                .deploymentName(processArchiveName).orderByDeploymentTime().desc().listPage(0, 1))//查找同名最新部署
                .map(e -> e.get(0)).orElseThrow(() -> new RuntimeException(processArchiveName + "not found"));
        List<String> oldResourceNames = repositoryService.getDeploymentResourceNames(deployment.getId());
            oldResourceNames.remove(resourceName);
        getDeploymentByDefaultConfig(resourceName, bpmnModelInstance).name(deployment.getName()).addDeploymentResourcesByName(deployment.getId(),oldResourceNames).deploy();
    }
    private ProcessApplicationDeploymentBuilder getDeploymentByDefaultConfig(String resourceName, BpmnModelInstance bpmnModelInstance) {
        resourceName = validateResourceName(resourceName);
        return repositoryService.createDeployment(processApplication.getReference())
                .addModelInstance(resourceName, bpmnModelInstance)
                //开启重复资源过滤 、关闭 isDeployChangedOnly
                .enableDuplicateFiltering(false)
                .resumePreviousVersions()
                .resumePreviousVersionsBy(ResumePreviousBy.RESUME_BY_PROCESS_DEFINITION_KEY);
    }
    /**
     * 如果不以.bpmn结尾则加上.bpmn后缀
     */
    private String validateResourceName(String resourceName) {
        if (!StringUtil.hasAnySuffix(resourceName, BpmnDeployer.BPMN_RESOURCE_SUFFIXES)) {
            resourceName = resourceName.concat(BpmnDeployer.BPMN_RESOURCE_SUFFIXES[1]);
        }
        return resourceName;
    }
}

需要注意的点是:

  • 如果部署不指定name,则会生成name为NULL的部署
  • 如果部署时不开启资源过滤(.enableDuplicateFiltering(true/false)),则每次部署都是新部署(虽然以更新流程为目标时,也必然是新部署就是了)
  • 没有特别需要不必开启 isDeployChangedOnly (.enableDuplicateFiltering(true))
  • 尚不确定是否需要在部署新流程时包含旧流程定义(更新式部署),但是按照一般性思路,更新流程文件通常是修改jar包 Resources文件夹内的.bpmn文件,然后打包重新发布,最好模拟这个流程,在更新流程文件的部署时,包含除了需要修改的流程文件以外旧部署的其他流程文件(但是如果存在运行时修改或者创建流程定义的情况,就需要下面的方案👇)

- 在运行过程中添加或修改的流程,如果程序重新启动,就不会被包含在启动时部署的流程中(如果启动时不包含任何流程byte甚至不会执行部署),需要在流程启动时的部署中添加原有部署的资源:

我这里直接顶替掉SpringBootProcessApplication,这样@EnableProcessApplication注解也用不加了

@Configuration(proxyBeanMethods = false)
public class BpmProcessConfig extends SpringBootProcessApplication {

    @Value("${spring.application.name}")
    private String appName;

    @Override
    public void createDeployment(@Nullable String processArchiveName, DeploymentBuilder deploymentBuilder) {
        if(processArchiveName==null){
            processArchiveName =getName();
        }
        List<Deployment> deployments = processEngine.getRepositoryService().createDeploymentQuery().deploymentName(processArchiveName).orderByDeploymentTime().desc().listPage(0, 1);
        if(deployments!=null&&!deployments.isEmpty()){
            //额外部署配置 因为存在在运行期间的动态部署,所以初始部署时,从数据库拉去最新同名部署的资源,排除掉已有的部分,将运行deploymentBuilder = {ProcessApplicationDeploymentBuilderImpl@10932} 过程中部署的资源也包含在内
            //防止动态部署的流程资源在应用重启后丢失
            //但是如果部署时不添加本地资源,部署操作就会直接跳过——> DeployProcessArchiveStep#performOperationStep() 141行 —— /**if(!deploymentResourceNames.isEmpty()) {**/
            //DeployCmd执行部署操作时,如果部署的资源与最新版资源完全一致,就会去掉部署资源,部署资源为空时,就会将其确定为重复部署 重复部署就直接查询数据库数据然后注册 DeployCmd 159行 DeploymentHandler#determineDuplicateDeployment()
            //本来只要部署时啥也不添加资源就能重复部署,但是为了避免直接操作引擎侵入性太高,所以只能行此下策 ,先添加数据库的资源,再等部署时被筛选去掉,绕来绕去浪费性能(=_=)
            Deployment latestDeployment = deployments.get(0);
            if(latestDeployment!=null){
                List<Resource> deploymentResources = processEngine.getRepositoryService().getDeploymentResources(latestDeployment.getId());
                for (Resource deploymentResource : deploymentResources) {
                    deploymentBuilder.addInputStream(deploymentResource.getName(),new ByteArrayInputStream(deploymentResource.getBytes()));
                }
            }  
        }

    }

    @Override
    public String getName() {
        if (!StringUtils.hasText(appName)) {
            appName = applicationContext.getBeanNamesForAnnotation(SpringBootApplication.class)[0];
        }
        return appName;
    }
}

根据 DeployProcessArchiveStep、DeployCmd的源码,程序部署的流程是

  1. 程序启动,准备部署
  2. 查看部署是否含有资源(if(!deploymentResourceNames.isEmpty()))
    • 无资源直接打个日志,不部署了
  3. DeployCmd开始部署
  4. 下载部署资源的远程资源(DeployCmd#getResources())
  5. 从资源中解析需要部署的资源(DeployCmd#resolveResourcesToDeploy())
    • 如果全部资源都与数据库中最新同名部署资源全部一致,就判断为重复部署
    • 重复部署,根据部署名查找最新部署id(DefaultDeploymentHandler#determineDuplicateDeployment()
    • 根据id从数据库获取DeploymentEntity,将其注册
  6. 是新部署,往数据库插入数据,然后将其注册