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);

}

 

posted @ 2023-02-22 15:13  豆豆323  阅读(760)  评论(0编辑  收藏  举报