flowable的流转与扩展
一、引言
我们在业务开发中,使用flowable的过程中,一般实际使用的,都是flowable提供给我们的一些门面服务。即下图中的servcie
对于任意一个service,比如runtimeService,我们查看它的实现类,会发现都是形如commandExecutor.execute(new XXCmd())的代码。点击查看commandExecutor类,会发现这下面是几是一条责任链。下面我们先从初始化开始分析这个责任链的构建。
二、初始化
整个初始化的过程,基本如上图。此处具体代码如下:
在初始化的过程中,initCommandExecutors()方法内,初始化了命令执行器。我们来看具体代码:
public void initCommandExecutors() { //初始化命令默认配置(主要设置事务类型) initDefaultCommandConfig(); //初始化schema命令配置 initSchemaCommandConfig(); // 初始化命令调用者 initCommandInvoker(); // 初始化命令拦截器 initCommandInterceptors(); // 初始化命令执行者 initCommandExecutor(); }
接下来看下initCommandInvoker方法
public void initCommandInterceptors() { if (commandInterceptors == null) { commandInterceptors = new ArrayList<>(); // 加入前置自定义拦截器 if (customPreCommandInterceptors != null) { commandInterceptors.addAll(customPreCommandInterceptors); } // 加入默认拦截器 commandInterceptors.addAll(getDefaultCommandInterceptors()); // 加入后置自定义拦截器 if (customPostCommandInterceptors != null) { commandInterceptors.addAll(customPostCommandInterceptors); } // 加入命令调用者的拦截器 commandInterceptors.add(commandInvoker); } }
显然,我们可以通过自定义customPreCommandInterceptors或者customPostCommandInterceptors,在flowable流程中,加入我们需要的自定义拦截器。下面我们看下默认拦截器的初始化:
public Collection<? extends CommandInterceptor> getDefaultCommandInterceptors() { if (defaultCommandInterceptors == null) { List<CommandInterceptor> interceptors = new ArrayList<>(); // 加入日志拦截器 interceptors.add(new LogInterceptor()); // 如果db是crdb,加入crdb重试拦截器 if (DATABASE_TYPE_COCKROACHDB.equals(databaseType)) { interceptors.add(new CrDbRetryInterceptor()); } // 加入事务拦截器 CommandInterceptor transactionInterceptor = createTransactionInterceptor(); if (transactionInterceptor != null) { interceptors.add(transactionInterceptor); } //加入命令上下文拦截器(用于命令上下文的环境依赖创建初始化工作) if (commandContextFactory != null) { String engineCfgKey = getEngineCfgKey(); CommandContextInterceptor commandContextInterceptor = new CommandContextInterceptor(commandContextFactory, classLoader, useClassForNameClassLoading, clock, objectMapper); engineConfigurations.put(engineCfgKey, this); commandContextInterceptor.setEngineCfgKey(engineCfgKey); commandContextInterceptor.setEngineConfigurations(engineConfigurations); interceptors.add(commandContextInterceptor); } // 加入事务上下文拦截器 if (transactionContextFactory != null) { interceptors.add(new TransactionContextInterceptor(transactionContextFactory)); } // 加入额外拦截器列表-实际有一个bpmn的可覆盖上下文拦截器 List<CommandInterceptor> additionalCommandInterceptors = getAdditionalDefaultCommandInterceptors(); if (additionalCommandInterceptors != null) { interceptors.addAll(additionalCommandInterceptors); } defaultCommandInterceptors = interceptors; } return defaultCommandInterceptors; }
到这里,其实拦截器虽然加好了,但是还没有形成责任链,即定义好每一个拦截器的next。flowable接下来在initCommandExecutor中,遍历了拦截器列表,进行了批量设置,代码如下:
public void initCommandExecutor() { if (commandExecutor == null) { // 循环设置next,并返回第一个拦截器 CommandInterceptor first = initInterceptorChain(commandInterceptors); // 设定命令拦截器 commandExecutor = new CommandExecutorImpl(getDefaultCommandConfig(), first); } } public CommandInterceptor initInterceptorChain(List<CommandInterceptor> chain) { if (chain == null || chain.isEmpty()) { throw new FlowableException("invalid command interceptor chain configuration: " + chain); } for (int i = 0; i < chain.size() - 1; i++) { chain.get(i).setNext(chain.get(i + 1)); } return chain.get(0); }
图中的命令执行者,其实就是上文中的commandExecutor。至此责任链已经在流程引擎启动阶段,注册到了各个我们日常使用的service。
三、流程流转过程
3.1命令执行器分析
上面已经说过,命令真正执行,实际都是调用commandExecutor的excute方法(即通过命令执行器执行),对于命令执行器,代码的具体流转如下
图中的节点推进器中的操作列表,实际每一步操作,大多是当前操作执行完成后,再去压入下一步操作。具体情况我们后续分析。且对于flowable的命令模式,实际并不是所有操作都有后续操作,例如AbstractQuery.query也会进入此命令执行器,但是显然此时不需要进行额外操作。
3.2 流程流转的分析
讲流程流转之前,我们先补充一些基本概念,即流程元素、操作与推进。
3.3.1 流程元素
先看下flowable的类图:
其中在flowable中,flowNode代表流程元素,它有以下子类:
- flownode代表流程节点,基本实现为事件(如开始事件/结束事件),网关(如排他网关,并行网关),活动三种,其中活动的主要实现为子进程和事件。
- squenceFlow代表顺序流。
- dataObject代表数据对象
一张流程图上的所有元素,都可以认为是流程元素。
先明确两个操作,即当前节点操作,与寻找下一条线的操作。对于操作与推进,分析如下:
3.3.2 操作与推进
操作
我们对flowable进行可视化编辑的时候,可以对每个节点添加边界事件,执行监听器等,这些组件的执行,都是通过操作来完成的。
对于flowable来讲,操作的基类为AbstractOperation。所有操作都会实现此类。他的实现列表如下:
这些实现中,在流程流转中最常用的为ContinueProcessOperation和TakeOutgoingSequenceFlowsOperation。分别代表继续流程操作和寻找顺序流操作(出线)。
在工作流引擎执行过程中,每经过一个流程元素,都需要执行该节点的继续流程操作。每个流程节点执行完成,都需要进行寻找顺序流操作,确定接下来的线的走向。
节点推进器
上面讲的操作,在flowable中,被节点操作推进器,即AbstractAgenda所持有。这个抽象类中,我们常用的实现类为DefaultFlowableEngineAgenda。AbstractAgenda实现了Agenda接口,而Agenda接口有继承了session。简单类图如下:
对于议程,我们的常用操作,是在Agenda中,具体代码如下:
public interface Agenda extends Session { /** * 返回操作计划是否为空 */ boolean isEmpty(); /** * 把下一个操作返回并从队列中移除 */ Runnable getNextOperation(); /** * 把操作加入队列 */ void planOperation(Runnable operation); /** * 加入一个异步执行的操作 */ <V> void planFutureOperation(CompletableFuture<V> future, BiConsumer<V, Throwable> completeAction); }
AbstractAgenda持有的对象如下:
public abstract class AbstractAgenda implements Agenda { // 命令上下文 protected CommandContext commandContext; // 操作列表 protected LinkedList<Runnable> operations = new LinkedList<>(); // 异步操作列表 protected List<ExecuteFutureActionOperation<?>> futureOperations = new ArrayList<>(); }
3.3.3流程流转分析
下面是一个简单的bpmn流程图的例子,我们接下来结合这张图,继续分析流程的流转
简要描述当流程经过流程节点,或者顺序流时,会执行的操作如下:
- 如果当前节点是流程节点,则可以执行当前节点操作。当前流程节点操作如果完成,就会执行寻找下一条线的操作。
- 如果当前节点本身就是顺序流(线),执行完成后只会进入顺序流指向节点的当前节点操作。
每一次流程流转中,工作流引擎都会一直尝试完成这两种操作。对于上面的流程图,如果是执行了开始流程与请假审批同意流程,那么经历过的操作顺序分别如下图:
我们可以发现,当流程满足条件能自动向后执行时,当前节点操作与寻找下一条线的操作,总是交替运行的。实际上,每个操作进行中,如果符合条件,都会向操作栈中,压入下一(多)个操作。这种压入有时是在操作自身中进行的,有时是通过行为(ActivityBehavior)进行的。
3.3 异步流程分析
在flowable的任务节点中,我们可以通过勾选异步,来实现当前操作异步处理。这块具体代码逻辑如下图:
四、自定义扩展
flowable通过对外提供一些spi以及自定义设置的方法,支持我们进行一些自定义扩展。下面简单讲一下id生成器与缓存的初始化与自定义扩展。
4.1 id生成器
flowable的id生成器,使用接口IdGenerator类,此类中只有一个方法,即getNextId方法,代码如下:
public interface IdGenerator { String getNextId(); }
在需要获取id时,flowable会调用此方法。我们可以通过实现此接口,并压入配置的方式,使用自己的id生成器。
flowable自行实现的全局id生成器,有两种,即DbIdGenerator与StrongUuidGenerator。默认为DbIdGenerator。spring自动化配置时,定义为StrongUuidGenerator。
4.1.1DbIdGenerator
先来聊下DbIdGenerator。实际上DbIdGenerator数据存储在数据库。每次取回后会在本地进行缓存一个区间段(2500)。然后更新数据库中的对应字段(act_ge_property表中,name_为next.dbid的字段)。全局自增,且不能区分业务。当前区间段用完会重新请求数据库。
这种id显然存在以下问题:
- 长度容易变动,且id单调递增,容易被识别出单量
- qps较高时,对数据库请求较多
4.1.2 StrongUuidGenerator
实际为uuid方案,不依赖数据库。
4.1.3 自定义
只要实现IdGenerator接口,并且写入配置即可。对于非spring环境示例代码如下:
public class SelfWithoutSpringTest { @Test public void testProcessEngine() { ProcessEngineConfiguration configuration = new StandaloneProcessEngineConfiguration(); //数据库配置 configuration.setJdbcDriver("com.mysql.jdbc.Driver"); configuration.setJdbcUsername("XXX"); configuration.setJdbcPassword("XXX"); configuration.setJdbcUrl("XXX"); configuration.setIdGenerator(new MyIdGenerator()); ProcessEngine processEngine = configuration.buildProcessEngine(); RuntimeService runtimeService = processEngine.getRuntimeService(); ProcessInstance processInstance = runtimeService.startProcessInstanceById("benAskForLeaveFlow:5:758ba816-acdd-11ed-8563-aa5b81408f73"); log.info(JSON.toJSONString(processInstance.getId())); } class MyIdGenerator implements IdGenerator { @Override public String getNextId() { return "ben-"+UUID.randomUUID().toString(); } } }
对于spring环境,实例代码如下
@Configuration public class flowableIdConfig { @Bean @Primary public IdGenerator primaryIdGenerator(){ return new PrimaryIdGenerator(); } @Bean @Process public IdGenerator processIdGenerator(){ return new ProcessIdGenerator(); } } public class PrimaryIdGenerator implements IdGenerator { public String getNextId() { return "primary-"+ UUID.randomUUID().toString(); } } public class ProcessIdGenerator implements IdGenerator { public String getNextId() { return "process-"+ UUID.randomUUID().toString(); } }
4.2 缓存
flowable运行中的缓存,大致可以分为两种,即流程定义缓存和各种实体缓存。
- 对流程定义(ProcessDefinition)这类数据的缓存,因为变更较少且访问频繁,将数据解析后常驻缓存在了进程中,且因每次部署时都是重新插入新的数据,所以不会存在有一致性的问题。
- 对于各数据实体的缓存,Flowable 设计了生命周期为一次命令的缓存,这类缓存能有效降低一次调用中相同数据对DB的多次查询,并随着CommandContext的销毁而销毁。
下面我们详细分析啊下流程定义的缓存
4.2.1缓存的初始化
在flowbale的的init中,下面的代码,主要用于初始化本地缓存
initProcessDefinitionCache();
initProcessDefinitionInfoCache();
initAppResourceCache();
initKnowledgeBaseCache();
其中除了initProcessDefinitionInfoCache方法,其余三个传入limitcount后,都是只是通过重写linkedHashmap的removeEldestEntry方法,来实现了LRU模式。initProcessDefinitionInfoCache还会对返回类型封装为ProcessDefinitionInfoCacheObject。以initProcessDefinitionCache为例,我们继续看下此方法的代码:
public void initProcessDefinitionCache() { // 流程定义缓存不存在,则初始化 if (processDefinitionCache == null) { if (processDefinitionCacheLimit <= 0) { // 初始化一个容量无限的缓存map processDefinitionCache = new DefaultDeploymentCache<>(); } else { // 初始化一个容量为limit的LRU的map processDefinitionCache = new DefaultDeploymentCache<>(processDefinitionCacheLimit); } } } public DefaultDeploymentCache() { this.cache = Collections.synchronizedMap(new HashMap<>()); } /** * Cache which has a hard limit: no more elements will be cached than the limit. */ public DefaultDeploymentCache(final int limit) { this.cache = Collections.synchronizedMap(new LinkedHashMap<String, T>(limit + 1, 0.75f, true) { // +1是不要的,因为要在删除旧数据前,把老数据加进来 // 0.75的负载因子是默认参数 private static final long serialVersionUID = 1L; @Override protected boolean removeEldestEntry(Map.Entry<String, T> eldest) { boolean removeEldest = size() > limit; if (removeEldest && LOGGER.isTraceEnabled()) { LOGGER.trace("Cache limit is reached, {} will be evicted", eldest.getKey()); } return removeEldest; } }); }
4.2.2、缓存的查询
对缓存进行查询时,就是简单的从map中获取数据,方法如下:
public T get(String id) { return cache.get(id); }
4.2.3、缓存的设定
设定有限制的LRU的map,demo如下:
@Test public void testProcessEngine() { ProcessEngineConfigurationImpl configuration = new StandaloneProcessEngineConfiguration(); //数据库配置 configuration.setJdbcDriver("com.mysql.jdbc.Driver"); configuration.setJdbcUsername("XXX"); configuration.setJdbcPassword("XXX"); configuration.setJdbcUrl("XXX"); configuration.setProcessDefinitionCacheLimit(1); ProcessEngine processEngine = configuration.buildProcessEngine(); RuntimeService runtimeService = processEngine.getRuntimeService(); ProcessInstance processInstance = runtimeService.startProcessInstanceById("benAskForLeaveFlow:5:758ba816-acdd-11ed-8563-aa5b81408f73"); ProcessInstance processInstance2 = runtimeService.startProcessInstanceById("benTestFlow:11:ce3839b1-a7af-11ed-b124-ca62e3fd2f34"); ProcessInstance processInstance3 = runtimeService.startProcessInstanceById("benAskForLeaveFlow:5:758ba816-acdd-11ed-8563-aa5b81408f73"); log.info(JSON.toJSONString(processInstance.getId())); }
流程启动过程中,在StartProcessInstanceCmd的execute方法内,会调用getProcessDefinition方法,这其中,又会调用deploymentCache.findDeployedProcessDefinitionById方法,尝试通过流程定义id,获取流程定义。此方法代码如下:
public ProcessDefinition findDeployedProcessDefinitionById(String processDefinitionId) { if (processDefinitionId == null) { throw new FlowableIllegalArgumentException("Invalid process definition id : null"); } // first try the cache ProcessDefinitionCacheEntry cacheEntry = processDefinitionCache.get(processDefinitionId); ProcessDefinition processDefinition = cacheEntry != null ? cacheEntry.getProcessDefinition() : null; if (processDefinition == null) { processDefinition = processDefinitionEntityManager.findById(processDefinitionId); if (processDefinition == null) { throw new FlowableObjectNotFoundException("no deployed process definition found with id '" + processDefinitionId + "'", ProcessDefinition.class); } processDefinition = resolveProcessDefinition(processDefinition).getProcessDefinition(); } return processDefinition; }
我们可以在上面代码中第七行加上断点进行debug。会发现上面业务代码中,每次启动流程时,map的容量都只有1,保留为最近使用,符合预期。
当然,我们也可以通过自行实现DeploymentCache类,把这份缓存替换为redis缓存,但是并不建议这么做,因为对于流程定义缓存,实际缓存对象为ProcessDefinitionCacheEntry。他持有的两个对象,即BpmnModel与Process,并没有实现序列化。此问题2017年在github已经提出,但是至今没有解决,详见https://github.com/flowable/flowable-engine/issues/481
五、并行网关流转的一些问题
5.1、Q:流程经过并行网关后,接下来会出现多个流程实例。看起来其中有一个流程实例是复用原有流程实例。这个复用原流程实例的实例,是如何确定的?
A:关于并行网关出线的时候,哪个线使用原流程,通过代码看是第一条线,具体代码在org.flowable.engine.impl.agenda.TakeOutgoingSequenceFlowsOperation#leaveFlowNode方法内。
// 获取所有出线流程 List<ExecutionEntity> outgoingExecutions = new ArrayList<>(flowNode.getOutgoingFlows().size()); // 获取第一个顺序流 SequenceFlow sequenceFlow = outgoingSequenceFlows.get(0); // 复用第一个当前流程实例 execution.setCurrentFlowElement(sequenceFlow); execution.setActive(false); outgoingExecutions.add(execution); // 为其他流程实例出线 if (outgoingSequenceFlows.size() > 1) { for (int i = 1; i < outgoingSequenceFlows.size(); i++) { ExecutionEntity parent = execution.getParentId() != null ? execution.getParent() : execution; ExecutionEntity outgoingExecutionEntity = processEngineConfiguration.getExecutionEntityManager().createChildExecution(parent); SequenceFlow outgoingSequenceFlow = outgoingSequenceFlows.get(i); outgoingExecutionEntity.setActive(false); outgoingExecutionEntity.setCurrentFlowElement(outgoingSequenceFlow); executionEntityManager.insert(outgoingExecutionEntity); outgoingExecutions.add(outgoingExecutionEntity); } }
第一个线的概念,是在于flowable会把bpmn解析为一个Process对象,其中FlowNode持有一个outgoingFlows对象。对于出线列表对象中的第一个元素,即第一条线。 对于同一个bpnm文件,生成的Process应该用于是一致的,所以谁使用原流程不是随机的,而是固定的。
5.2、Q:并行网关如何决定继续向下流转?
A:关于并行网关如何收线决定往下走,根据代码分析,是会比较这个并行网关进入的线的条数,和完成的条数是否一致(注意下,这里只看当前网关的进入线数量,而非上一个网关出线数量。因为两个数量可能不一致)。 这块还是涉及到之前说的当前节点执行类和出线类。当前节点执行类(ContinueProcessOperation)发现当前节点为并行网关时,经过一系列操作,会调用并行网关操作行为尝试出线,即ParallelGatewayActivityBehavior.execute。关键代码就在这个方法中,简要罗列如下:
// 获取已完成实例 Collection<ExecutionEntity> joinedExecutions = executionEntityManager.findInactiveExecutionsByActivityIdAndProcessInstanceId(execution.getCurrentActivityId(), execution.getProcessInstanceId()); if (multiInstanceExecution != null) { joinedExecutions = cleanJoinedExecutions(joinedExecutions, multiInstanceExecution); } // 获取当前并行网关入线数量 int nbrOfExecutionsToJoin = parallelGateway.getIncomingFlows().size(); // 获取已完成实例 int nbrOfExecutionsCurrentlyJoined = joinedExecutions.size(); // Fork // Is needed to set the endTime for all historic activity joins CommandContextUtil.getActivityInstanceEntityManager().recordActivityEnd((ExecutionEntity) execution, null); // 如果当前入线数量=已完成数量。那么出线 if (nbrOfExecutionsCurrentlyJoined == nbrOfExecutionsToJoin) { // 省略 // 出线操作 CommandContextUtil.getAgenda().planTakeOutgoingSequenceFlowsOperation((ExecutionEntity) execution, false); }