流程引擎和规则引擎在企业级平台的系统中,是双子星,占有非常重要的地位。市面上有非常多**"light-weight"**的流程引擎组件,虽然声明着轻量化,但是流程引擎这个话题一出现,其实就是一个古典、严肃并且不那么"轻"的话题,今天我们带着实践来扒开一个开源流程引擎的实现、部署、以及使用技巧,希望可以帮到正在选型的你。
欢迎关注微信公众号「小爱同学的企服技术笔记」,获取完整细节文章。
本篇主要和大家探讨在企业内审批协同、商家招商入驻、app开发上架自检部署、交易正逆向等典型重流程串行场景上:
1、为什么要引入流程引擎,对研发以及业务规模化后的效率有什么提高
2、怎么在市面纷繁众多的开源以及国产定制BPMN Flow系统选型
3、如何源码级别修改、SDK级别包装、私有化部署一套开源流程引擎
问题一:为什么要引入流程引擎, 解决什么问题?
无论一个系统的复杂度高低或是承载的业务量大小,新引入一个深度内嵌式的组件,都是需要深思熟虑的,因为这不仅是对现有的的系统进行一次"重新装修"、"梳理线路",更加会对日后的功能迭代、架构升级、性能提高带来时间成本与历史包袱,一定要慎之又慎。
以我们公司典型的商业化系统的场景为例,商家侧的业务流不同于传统的电商商家模式,电商模式的商家的最终目的是为了在开展合作、签订一系列协议合同之后完成商品上架并促成交易,但是钉钉上有ISV(中小SaaS独立开发者)、销售渠道商、硬件提供商、内容培训商等等不同属性、合作目的、合作方式完全不同的商家类型。对于他们的入驻、合作、商业化变现运转等等业务行为系统业务流程设计是需要case by case单独考虑的。通过粗暴硬编码、使用运行时服务编排或者是责任链模式来编排商家生命周期中的细节流程时,逻辑串联非常分散没有上帝视角的管控,并且后续的拓展性也是捉襟见肘的。
因此,我们的目光放到了流程引擎上,期望引入一个流程管理、编排、灵活拆装节点的底层组件,解决当前场景的问题。这也是流程引擎普遍适合的场景,需要有流程管控编排能力,需要极大程度复用一些不变化的节点。
引入流程引擎这种底层组件,绝对要评估好是不是非上不可。
1、引入之后的开发效率绝对没有硬编码更快,会涉及流程设计、绘制、节点间的编排调试等等额外的纯技术工作,业务代码一行也省不了。**这部分额外的研发成本,会在后续的迭代和二次拓展中帮助研发更节省时间,如果只是一次性使用,没有必要引入。
**2、底层引擎的稳定直接决定上层业务的稳定,流程实例、流程数据、流程集群等纯技术数据与模块也需要长期投入时间成本进行维护。
问题二:选型中间遇到的困难、反复
BPMN是BPM以及workflow的建模语言标准之一,也是流程引擎在业界的公认标准,流程引擎也不是一个小众技术点,所以可供选择的就特别多,这里挑选了开源、阿里巴巴集团内,产品化程度高、轻量化等多种流程引擎做了一个对比。
open-source | 集团内部 | |||||
Activiti | JBPM | SmartEngine | tbbpm | 菜鸟星云 | 自建方案 | |
遵循BMPN规范 | 遵循最新BPMN2.0 | 遵循最新BPMN2.0 | 遵循最新BPMN2.0 | 不支持 | 遵循最新BPMN2.0 | 遵循最新BPMN2.0 |
支持并行、多网关分支流程 | 支持 | 支持 | 支持 | 不支持 | 支持 | 支持 |
存储方案灵活性 | 支持多种DB存储,选择较多,流程数据本地存储 | 仅支持自家生态内的存储 | 关系型DB、doc型mangodb等 | 不支持数据本地存储 | 流程数据本地存储,流程定义集中存储 | - |
支持流程编排 | 主要面向工作流应用,流程运行过程需要持久化,纯流程编排场景运行性能较差 | 主要面向工作流应用,流程运行过程需要持久化,纯流程编排场景运行性能较差 | 支持工作流和流程编排两种模式 | 支持工作流和流程编排两种模式 | 支持工作流和流程编排两种模式 | - |
拓展、业务灵活定制性 | 生态较好,已经有比较好的落地生态,执行期的节点扩展不够优雅,分布式情况下需要改造 | 自身对历史版本不兼容,灵活性兼容性较差,分布式情况下需要改造 | 对ICBU国际贸易业务做了定制 | 扩展性较好,流程节点支持集团中间件,特殊业务场景不能自行开发 | 支持多种扩展方式,扩展性好,流程节点集团中间件,特殊业务场景不能自行开发 | - |
流程设计器 | 支持符合BPMN规范的任意编辑器,自身提供一个功能比较完善的编辑器 | 支持可视化编辑器 | 支持符合BPMN规范的任意编辑器,自身有一个简单版本的可视化编辑器 | 有可视化编辑器 | 有比较完备的流程编辑器,支持流程版本、流程变更审核发布等 | - |
API风格 | api设计合理整体模块,职责分开清晰 | api设计比较晦涩 | 基本与Activiti一致,剪裁轻量版本 | api比较少 | 目前提供的api比较少 | - |
整体复杂度,适配难度 | 内部复杂度较高,适配成本高 | 适配成本较高 | 需要共建交流,特殊功能需要排期支持,流程问题可自行排查 | 需要共建交流,特殊功能需要排期支持,流程问题需要协助排查 | 需要共建交流,特殊功能需要排期支持,流程问题需要协助排查 | - |
学习成本 | 社区成熟,文档较多 | 社区成熟,文档较多 | 文档较少 | 文档较少,比较靠依赖看代码来摸索使用 | 文档比较齐全,有答疑 | - |
总体结论 | 开源,文档较多,可任意扩展定制功能,但需要自行部署修改,成本比较高 | 开源,文档较多,可任意扩展定制功能,但是项目比较大,学习成本比较高,需要自行部署修改,成本比较高 | 轻量级,功能完备性略差,离产品化还有点距离,有不少场景需要自行定制开发,成本比较高 | 文档少,自定义bpm规范,未能产品化,学习成本高,部分流程功能不满足需求 | 文档比较齐全,接入简单,功能满足需求,产品化较好,但是团队不稳定,原创团队成员大部分已经离职或转岗,比较担心项目前景 | 比较满足业务需要,技术风险可控,但是实现完备的bpmn流程引擎成本非常高,且有如此多的情况下,重复搭建一套不太有必要 |
读者可以根据自己的实际需求,来参考斟酌选型,对于笔者遇到的实际情况,我们会做以下的选型考虑:
**1、**符合标准的BPMN2.0规范,标准的规范可以让我们后续的增强改造有的放矢,可以对接一些开源组件,不至于后续难以维护。**特别是流程设计器有了很多开源选择。
****2、**支持到流程运行时纯内存的服务编排,为原责任链模式的代码增加分支灵活性,并且也要支持流程暂停中断、外部触发流程唤起等,可持久化在关系型存储内。
**3、**尽量light-weight,不要夹杂太多特别上层产品化的模块在内核内,尽量整体的架构是core-plugin插拔模式的,让我们可以简单梳理清楚源码的模块。
**4、**一定是去中心化存储模式的,可以单一本地部署,而不是走服务化提供API式管理的,因为流程引擎的内嵌我们必须要对于紧急状况发生时有足够的掌控能力,如果整个引擎细节对于我们是个黑盒,那用户出现问题时的恢复时效是无法承诺保障的。
**5、**有能用的流程编辑器、有简单的流程管理可视化界面是加分项最好。
综合上述的仔细对比调研结合我们的诉求,开源的Activiti、JBPM虽然是业界标杆,但是对于我们来说太”厚重了“,自行修改的成本较高,之后发现物流巨头菜鸟的星云其实是产品化程度最高,功能支持最多,也有广泛的物流业务场景使用,得到过实践的验证。但是作为纯服务化的方式,只能通过API进行黑盒使用,基于团队从2020开始不太稳定,我们也放弃了。
辗转反侧,最后我们选择了出生于阿里巴巴国际业务的smart-engine,基本都满足我们的诉求,源码简单,虽然只提供了一套特别精简的core包,但是定制改造成本低,支持单一本地部署,虽然文档不多,但是已经贡献给了开源社区,社区活跃度尚可。特别是作者帝奇本人的答疑响应速度特别高,在github上的迭代频率和速度也有目共睹,因此我们信任并慎重选择了它。
这个流程引擎提供了一套类似汽车发动机的核心模块,当然也需要很多产品化包装。
对于smart-engine的介绍,可以参考github,也欢迎你进行一定的Pull Request贡献:
github.com/alibaba/Sma…
其实对于系统重构中的新组件选型我们的经验的还很有限,本着对系统和团队负责的原则,一定是要磨刀不误砍柴工,精心对比列出对比点,并且找经验多、踩过坑的高年级同学把关,并且寻找已经使用过的同学或者外网用户虚心讨教,对于组件的源码要花一定的时间去摸排一番之后,才可以做下决定,这个过程本身就是煎熬、犹豫、反复推到重来的过程,这些成本下足了,才不会事后埋坑和留下遗憾。
这似乎也是生活中的道理,人生第一次投资、买房、买HIFI耳机、入坑第一台相机等等,踏入一个你有强烈需求和欲望的领域,但是却没有任何认知的情况下,我们只有花足时间、不断摸排学习、讨教,在一个个问题击破后渐渐建立起这个未知世界的棱棱角角,最后做出自己理智的决定。
深入进入一个流程引擎内部:
**一些BPMN的基本知识:
**BPMN(Business Process Model and Notation)是由世界官方标准组织制定的具有规范和通用性的工作流图形表示法,已经纳入ISO 19510。所以我们也按照BPMN 2.0的通用表示法来介绍一些基本的元素和组件,具体的细节网上有非常多,可以深入了解:en.wikipedia.org/wiki/Busine…
**
**BPMN 2.0 最基本元素
_BPMN 2.0全部的图形表示元素
_
实战:引擎代码独立部署、业务应用集成内嵌:
简单先梳理SE流程引擎以及我们需要的引擎层服务架构,先完成底层的引擎部署,再与我们的实际业务代码相结合。
虽然是部署core代码,但是也要清晰确立基本的流程引擎架构
独立部署与源码修改定制过程:
首先我们将smart-engine引擎源码 fork了一个稳定版本分支,脱离开了开源公有版本的迭代,如果后续有特别大的bug fix,我们会主动跟进进行merge,这样一来我们可以进行源码改造并且进行独立部署,遇到一些小问题我们也可以第一时间介入自己搞定fix,因为商业化系统的用户不能忍受开源的pr fix速度。在实际的应用部署中不一定需要有实体虚拟机的应用,也可以创建应用模块,单独的一个代码库也可以使用aone进行部署发布,控制feature branch和真正的线上master版本。这里使用正常的maven版本库进行版本控制,而不是简单自己进行deploy jar包进行管控的好处可以避免多人开发和版本混乱时,可以清晰地知道当前的stable版本是哪个,并严格以应用级别做到发布控制。
系统分层说明:
代码部署在业务应用内,日常可以灵活适配业务的部分(1-3)
1、产品形态层: 在提供流程的主体功能上,主要支持到纯内存的流程编排模式与我们强烈需要的工作流模式(支持流程节点暂停、唤起、持久化)。
**2、服务层:**目前实践和进展来看,在process(流程)、task(任务)、node(节点)这三个模型上的操作是最多的,所以在实际运行应用的应用内,对这三个重点模型进行了比较优雅的产品化服务接口包装,集合在processEngineService中,供业务系统内部调用。
**3、拓展层:**主要是一些贴合业务的实际挂载功能,留出一些业务产品化需求开发的空间。比如在商家系统的日常的运营中,会经常使用到钉钉的审批,那在实际某个流程运行到某一个节点的时候,希望转接一个钉钉BPM审批出去到端上。一些的流程节点小功能、异步操作等等,安排在这一层进行触发。
**代码部署在独立smart-engine应用内,需要固化的真实引擎代码(4-5)
**4、引擎层:
- 流程定义解析:
流程定义文件与解析:在应用resource包下专门建一个存放本业务应用专门的bpmn文件包,单独的流程bpmn.xml文件包含了该流程的名称定义、版本号、适配标准等等,该文件可以在图形化的流程编辑平台进行序列化和反序列化的解析,灵活编辑。
- 模型拓展:
此处如果对于标准的BPMN的节点标签不满足使用,我们可以在xml中自己定义标签,在引擎parse过程中加入对于这种自定义标签的解析即可。
- 流程执行运行:
smartengine启动:这里使用smartengine推荐的启动当时,我们将SM初始化放在应用启动的生命周期尾部,作为一个系统容器的bean管理。流程运行时:流程开始、向下传递运行、暂停、重新唤起、结束,这些运行时动作,统一都在SM的core层面对外包装了更加贴合业务应用的参数和特性,例如常用的orgId、uid,做了默认的参数设置。
- 节点业务逻辑:
对于运行时节点内的业务逻辑包装,我们定义了一个注解@ProcessTask来定点标注运行的method,并且与流程定义文件一样,统一放在一个process包中进行管理
-
运行时节点间出入参转换
对于运行时节点间入参、出参传递转换包装了专门的****.DefaultTaskParamConverter这个类进行统一参数的转换
-
暂停节点上下文恢复
对于暂停的节点,SM本身是没有做暂停节点的入参出参持久化的,就会导致一旦流程暂停,暂停节点的上下文会全部丢失,这点我们设计了附属的com.****.*****.core.process.CustomVariablePersister类进行运行时或暂停时节点上下文参数的存放。
5、数据层:数据层对于需要持久化的数据,我们基本根据原本的DDL设计,增加了流程节点变量表,部署在业务应用内或者是单体引擎应用内皆可。
重要的几个存储表解释:
se_activity_instance 存储整个 bpmn 数据,整个流程所有节点元数据
se_execution_instance 实际执行的节点列表
se_process_instance 每次产生的一次实例流程
se_variable_instance 记录流程变量,供暂停唤起时,历史出入参记忆
**
启动运行,从一个复杂逆向流开始示例
**我们从一个稍稍复杂的商家结束合作逆向流程来看,改造后的smartengine在一个业务流程生命周期中的运行期细节。
简易的伪流程+代码梳理示意
绘制真实BPMN流程图
以下的系统生命周期讲解,跟随着图中红线部门的分支流程进行。
1、梳理业务流程、编排好对应流程图、启动流程引擎触发流程
对应如何编排实际的业务流程如上图所示,不再赘述,SmartEngine提供了一个流程编辑器,可以参考使用,也可是使用开源BPMN2.0标准的编辑器。
基础的smartengine随应用一起启动,初始化,并进行扫描资源文件包下的流程定义文件,生成流程实例于引擎中
/**
* proces engine工厂类,默认是smartengine
*/
@Component
public class ProcessEngineFactoryBean implements FactoryBean<SmartEngine>, InitializingBean {
@javax.annotation.Resource
SequenceIdGenerator sequenceIdGenerator;
SmartEngine smartEngine;
InstanceAccessor defaultInstanceAccessService = new DefaultInstanceAccessor();
@Override
public void afterPropertiesSet() throws Exception {
//随应用一起一一个bean方式启动,基础地进行smartengine的初始化
ProcessEngineConfiguration processEngineConfiguration = new DefaultProcessEngineConfiguration();
processEngineConfiguration.setIdGenerator(sequenceIdGenerator);
processEngineConfiguration.setVariablePersister(new CustomVariablePersister());
processEngineConfiguration.setInstanceAccessor(new CustomInstanceAccessService());
processEngineConfiguration.setTaskAssigneeDispatcher(new PartnerTaskAssigneeDispatcher());
smartEngine = new DefaultSmartEngine();
smartEngine.init(processEngineConfiguration);
deployProcessDefinition();
}
private void deployProcessDefinition() {
RepositoryCommandService repositoryCommandService = smartEngine
.getRepositoryCommandService();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
String processLocation = BizEnvironment.getBusinessUnitProperties().getProcessLocation();
if (StringUtils.isBlank(processLocation)) {
throw new PartnerConfigException(ErrorCode.CONFIG_EXCEPTION_MISSING_PROCESS_PACKAGE, "please config processLocation in application.properties");
}
//这里进行基础的扫描bpmn流程定义包内的文件,进行流程文件解析,实例化一个process进引擎内
Resource[] resources = resolver.getResources(
processLocation);
for (Resource resource : resources) {
InputStream inputStream = resource.getInputStream();
repositoryCommandService.deploy(inputStream);
IOUtil.closeQuietly(inputStream);
}
} catch (Exception e) {
throw new EngineException(e);
}
}
}
2、流程触发、非暂停业务节点的流转
当外部上层接口触发smartEngine core包内的接口ProcessCommandService.start()时,流程便会进行触发,此处我们做了上层产品化的包装,让外层调用更加简易,返回更加,清晰并且提供尽可能多的流程与节点信息在返回ProcessInstance中。
public class ProcessInstance implements Entity {
private String processInstanceId; //流程实例id 唯一代表某一时间开启的流程实例
private String processDefKey; //流程模板key 绑定唯一的流程定义文件
private String bizUniqueId; //流程模板唯一业务定义号
private String version; //流程模板版本 如1.0.0
private String status; //流程当前运行状态 running\complete\break等
private Date startTime; //流程启动时间
private List<InstanceVariable> instanceVariableList; //当前运行时节点变量表
private List<ProcessInstanceBizData> processInstanceBizDataList; //当前流程自身拓展数据
}
public ProcessInstance createInstance(String processDefId, String version, Map<String, Object> request) {
//流程开始出发运行
com.alibaba.smart.framework.engine.model.instance.ProcessInstance processInstance = smartEngine.getProcessCommandService()
.start(processDefId, version, request);
List<ProcessInstanceBizData> bizDataList = createViewModelBizData(request, processInstance);
//组装完整的流程信息,方便在任何时刻都可以从返回清晰地获取该流程整体和细节的状态
return ProcessInstanceConverter.toProcessInstanceWithVariable(processInstance,
smartEngine.getVariableQueryService(),bizDataList);
}
此时流程运行至引擎层进入smart-engine core代码
//com.alibaba.smart.framework.engine.service.command.impl.DefaultProcessCommandService
public ProcessInstance start(String processDefinitionId, String processDefinitionVersion, Map<String, Object> request, Map<String, Object> response) {
//真正内存中初始化一个processInstance
ProcessInstance processInstance = processInstanceFactory.create( processEngineConfiguration, processDefinitionId,processDefinitionVersion, request);
//创建core内运行时上下文
ExecutionContext executionContext = this.instanceContextFactory.create(processEngineConfiguration, processInstance,
request, response, null);
//建立一个process virtual machine,为运行的process instance建立一个虚拟容器环境
PvmProcessInstance pvmProcessInstance = new DefaultPvmProcessInstance();
try {
//持久化一次流程实例基本信息
tryInsertProcessInstanceIfNeedLock(processEngineConfiguration, processInstance);
//*****开始进入流程节点运行阶段
processInstance = pvmProcessInstance.start(executionContext);
processInstance = CommonServiceHelper.insertAndPersist(processInstance, request, processEngineConfiguration);
return processInstance;
} finally {
LockStrategy lockStrategy = processEngineConfiguration.getLockStrategy();
if (null != lockStrategy) {
lockStrategy.unLock(processInstance.getInstanceId());
}
}
}
processInstance = pvmProcessInstance.start(executionContext);之后会进入execute方法内,进行触发javaDelegate运行实际的流程节点业务代码,这里也会控制节点拓展任务已经流程的暂停与否
//com.alibaba.smart.framework.engine.pvm.impl.DefaultPvmActivity
public void execute(ExecutionContext context) {
//开始触发TaskInvoker运行@ProcessTask标注方法
this.getBehavior().execute(context,this);
//触发@ProcessTask标注方法后挂载的逻辑,此处本实例没有运行到
fireEvent(context,PvmEventConstant.ACTIVITY_EXECUTE);
//如果当前实例是人工节点(或者需要暂停),则此处会终端跳出到上层进行持久化操作
if (context.isNeedPause()) {
// break;
return;
}
fireEvent(context,PvmEventConstant.ACTIVITY_END);
this.getBehavior().leave(context, this);
}
我们自己实现了一个javaDelegation来作为自定义的taskInvoker,这里面是到实际流程业务代码的最后一道大门,可以做一些入参出参的转换控制、hook、拼装等等,并且我们可以控制我们自定义的异常处理
上一节点的出参我们定义为OUT_RES,暂时可以定义为一个String,多参数可以为Object,从context中获取到,并转换传入本节点中
/**
* 默认taskinvoker
*/
public class ProcessTaskInvoker implements JavaDelegation {
private static final Logger logger = LoggerFactory.getLogger(ProcessTaskInvoker.class);
private final String code;
private final Object target;
private final Method method;
private TaskParamConverter taskParamConverter;
public ProcessTaskInvoker(String code, Object target, Method method, TaskParamConverter taskParamConverter) {
this.code = code;
this.target = target;
this.method = method;
this.taskParamConverter = taskParamConverter;
}
@Override
public void execute(ExecutionContext executionContext) {
Object result = this.invoke(executionContext);
if (result == null) {
return;
}
//上一节点的出参,本节点的入参,用OUT_RES统一命名
String resultName = Optional.ofNullable(ActivityHelper.getProperty(executionContext.getProcessDefinition(),
executionContext.getActivityInstance().getProcessDefinitionActivityId(),
ProcessPropertyEnum.OUT_RES.getPropName()))
.orElse(executionContext.getExecutionInstance().getProcessDefinitionActivityId() + ":" + ProcessPropertyEnum.OUT_RES.getPropName());
executionContext.getRequest().put(resultName, result);
if (result instanceof ViewModel) {
executionContext.getRequest().put(ProcessPropertyEnum.VIEW_MODEL.getPropName(), JSON.toJSONString(result));
}
}
public Object invoke(ExecutionContext executionContext) {
try {
ReflectionUtils.makeAccessible(this.method);
//将当前节点上下文中的入参hook转换出来,传入实际的@TaskProcess方法内
Object[] args = taskParamConverter.convert(this.method, executionContext);
//反射触发下文的@TaskProcess方法
return this.method.invoke(this.target, args);
} catch (InvocationTargetException|IllegalAccessException ex) {
logger.error("ProcessTaskInvoker invoke InvocationTargetException.on class:{},method:{},activityId:{},code:{},processId:{},version:{}",target.getClass().getName(),
method.getName(),Optional.ofNullable(executionContext.getActivityInstance()).map(ActivityInstance::getProcessDefinitionActivityId).orElse(null),
code,Optional.ofNullable(executionContext.getProcessDefinition()).map(ProcessDefinition::getId).orElse(null),
Optional.ofNullable(executionContext.getProcessDefinition()).map(ProcessDefinition::getVersion).orElse(null),
ex);
if (ex instanceof InvocationTargetException) {
ReflectionUtils.rethrowRuntimeException(((InvocationTargetException)ex).getTargetException());
} else {
throw new UndeclaredThrowableException(ex);
}
} catch (Exception e) {
logger.error("ProcessTaskInvoker invoke exception.on class:{},method:{},activityId:{},code:{},processId:{},version:{}",target.getClass().getName(),
method.getName(),executionContext.getActivityInstance().getProcessDefinitionActivityId(),
code,executionContext.getProcessDefinition().getId(),executionContext.getProcessDefinition().getVersion(),e);
throw e;
}
return null;
}
}
之后进入节点的运行阶段,因为在流程实例初始化的时候,已经装载好我们上述所说的**@ProcessTask标注的方法,这里的几个基本参数会贯穿流程整个运行过程中,不用特意像OUT_RES做特殊入出参转换处理**
//ProcessDefinition processDefinition 流程定义,流程基本信息,方便业务逻辑中获取一些基本参数
//String processDefinitionActivityId 流程节点定义Id
//String processInstanceId 流程实例Id
//String bizCode 租户类型
//Long orgId 组织名
//这些默认字段都可以在流程启动时注入context内
//outRes 从上一节点传递下来的结果数据,可以用在下游节点或者是网关类节点上作为判断
//一个标准节点的代码,标注@ProcessTask与流程定义文件绑定
@ProcessTask(code = "reverse.setPartnerSatuasEnding")
//入参
public Integer setPartnerSatuasEnding(ProcessDefinition processDefinition,
String processDefinitionActivityId,String processInstanceId, String bizCode, Long orgId, String outRes) {
ProviderTypeEnum providerTypeEnum = ProviderTypeEnum.getProviderTypeEnumByName(bizCode);
new StartEndingCooperationCmd(providerTypeEnum,orgId,outRes).execute();
CLEAR_PARTNER_JOB.warn("ReverseActivity.setPartnerSatuasEnding -> orgId:{} bizCode:{} setPartnerStatusEnding finished",orgId,bizCode);
//出参
return ProviderTypeEnum.getProviderTypeEnumByName(bizCode).getValue();
}
3、遇到需要暂停的节点、暂停持久化与重新唤起流程
实际出发运行和前后节点的交互传参与第二步中一致,仅仅是 //com.alibaba.smart.framework.engine.pvm.impl.DefaultPvmActivity中会进行context.isNeedPause()的判断然后暂停退出
这里一致运行的节点群和遇到暂停的节点不同的是:
从start()开始触发processInstance之后,会持续触发jump()操作,如上图中开始到”发起重分配CRM商机metaq“这个暂停人工节点前,是不会触发到context.isNeedPause(),可以类似为这是个递归栈,还没有到返回的时候。当遇到暂停或者是流程的end结束节点的时候,这个递归栈则开始第一次return,开始一次次出栈操作。之后便会进行之前完成的节点和流程当前状态的持久化。主要是触发
//com.alibaba.smart.framework.engine.service.command.impl.DefaultProcessCommandService
processInstance = CommonServiceHelper.insertAndPersist(processInstance, request, processEngineConfiguration);
**重新唤起流程本质与jump()无异,提供了signal()方法进行外部触发,从存储中获取当前暂停节点的上下文,然后继续推进
**
//com.alibaba.smart.framework.engine.pvm.impl.DefaultPvmProcessInstance
public ProcessInstance signal(PvmActivity pvmActivity, ExecutionContext executionContext) {
//NOTATION: execute EXECUTE method
pvmActivity.execute( executionContext);
return executionContext.getProcessInstance();
}
4、遭遇判断网关
一般的网关有并行网关与排他网关,这里的逻辑处理主要是会对outRes返回的结果进行先全部条件的计算,而非优先走某一支路,如上图中的”检测工单订单是否完结“之后的排他网关,不会因为返回为false,而在运行时直接跳过true的判断,而是都会计算,然后在选择结果分支进行流转,这里可以详看gateway部分的源码。
至此,一个完整的多分支、带有暂停的工作流就结束了它的生命周期。
欢迎关注微信公众号「小爱同学的企服技术笔记」,获取完整细节文章。
一些参与SE引擎贡献的点:
流程引擎是否该给流程节点内业务逻辑的异常进行兜底?
smartengine的设计逻辑,为了保证性能,持久化操作只会在暂停节点或者流程结束时进行,那么如果当一个流程节点在运行过程中发生异常中断,那么smartengine也不会做任何异常处理,会导致流程实例数据在上一次暂停后的数据全部丢失,必须从上一次暂停后的节点重新开始,如果该流程之前无暂停节点(未持久化过)那么整个流程实例的运行时数据便会全部丢失。
这方面调研了一下业界的做法:
1、流程定义者自己定义好异常处理节点,本质上我认为这是提供一个类似exception节点的拓展,出现异常逻辑自动跳入指定类型的exception节点内进行处理,当然也有强制的Exception父类兜底节点作为最后的兜底。
参考 www.tinygroup.org/docs/ac6ee3…
2、Activiti在最新版本中也使用了在流程最后或暂停时进行持久化的方式,
(参考juejin.cn/post/684490…).
在Activiti对于异常的处理方式上,对于第一种,多出了"回滚"的选择,是回滚而不是smartengine上的直接"丢弃"。其余的异常兜底方案上与一致。
这里基于日常的线上情况来看,我们尝试了这样的方法来解决这个高频的问题。
首先为什么是高频的,因为节点内的业务逻辑千奇百怪,你无法要求每一个业务逻辑开发者都保证节点内不出异常,所以这部分流程引擎就应该做好兜底。
解法:
1、首先我们的smartEngine处于我们要进行二次定制的需要,本身fork了最近的版本,并且独立部署了一个应用,此应用不需要架设真正的带机器应用,但是部署迭代需要在aone内进行,控制好master分支的稳定,并且定期merge 开源版本。
2、我们遇到的实际问题如下
3、改造的方案是在节点执行过程中如果出现Exception,执行部分会进行主动catch,预留出可以自编辑的handle处理部分,并且为了保护整个流程正常,会做出暂停在node1(上一正常节点)并持久化所有上下文数据,待流程重试或介入排查。