admin管理员组

文章数量:1530842

2023年12月13日发(作者:)

工作流引擎详解!工作流开源框架ACtiviti的详细配置以及安装

和使用

创建ProcessEngine

Activiti流程引擎的配置文件是名为的XML文件.注意与使用Spring方式创建流程引擎是不一样的

使用sEngines类,获得ProcessEngine:

ProcessEngine processEngine = aultProcessEngine()

它会在classpath下搜索,并基于这个文件中的配置构建引擎

xmlns:xsi="/2001/XMLSchema-instance"

xsi:schemaLocation="/schema/beans /schema/beans/">

配置文件中使用的ProcessEngineConfiguration可以通过编程方式创建,可以配置不同的bean id

ProcessEngineConfigurationFromResourceDefault();

ProcessEngineConfigurationFromResource(String resource);

ProcessEngineConfigurationFromResource(String resource, String beanName); // 配置不同的bean id

ProcessEngineConfigurationFromInputStream(InputStream inputStream);

ProcessEngineConfigurationFromInputStream(InputStream inputStream, String beanName);

如果不使用配置文件进行配置,就会基于默认创建配置

XXX() 方法都会返回ProcessEngineConfiguration,后续可以调整成所需的对象. 在调用

buildProcessEngine()后, 就会创建一个ProcessEngine:

ProcessEngine processEngine = StandaloneInMemProcessEngineConfiguration()

.setDatabaseSchemaUpdate(_SCHEMA_UPDATE_FALSE)

.setJdbcUrl("jdbc:h2:mem:my-own-db;DB_CLOSE_DELAY=1000")

.setJobExecutorActivate(true)

.buildProcessEngine();

ProcessEngineConfiguration bean

必须包含一个id='processEngineConfiguration' 的bean

这个bean会用来构建ProcessEngine. 有多个类可以用来定义processEngineConfiguration. 这些类对应不同的环境,并设置了对应的默认值:

loneProcessEngineConfiguration: 单独运行的流程引擎.Activiti会自己处理事务.默认数据库只在引擎

启动时检测(如果没有Activiti的表或者表结构不正确就会抛出异常)

loneInMemProcessEngineConfiguration: 单元测试时的辅助类.Activiti会自己控制事务. 默认使用H2

内存数据库,数据库表会在引擎启动时创建,关闭时删除.使用它时,不需要其他配置(除非使用job执行器或邮件功能)

ProcessEngineConfiguration: 在Spring环境下使用流程引擎

cessEngineConfiguration: 单独运行流程引擎,并使用JTA事务

数据库配置

定义数据库配置参数

基于数据库配置参数定义数据库连接配置

jdbcUrl: 数据库的JDBC URL

jdbcDriver: 对应不同数据库类型的驱动

jdbcUsername: 连接数据库的用户名

jdbcPassword: 连接数据库的密码

基于JDBC参数配置的数据库连接 会使用默认的MyBatis连接池,配置MyBatis连接池:

jdbcMaxActiveConnections: 连接池中处于被使用状态的连接的最大值.默认为10

jdbcMaxIdleConnections: 连接池中处于空闲状态的连接的最大值

jdbcMaxCheckoutTime: 连接被取出使用的最长时间,超过时间会被强制回收. 默认为20000(20秒)

jdbcMaxWaitTime: 这是一个底层配置,让连接池可以在长时间无法获得连接时, 打印一条日志,并重新尝试获取一个连接.(避免因为

错误配置导致沉默的操作失败) 默认为20000(20秒)

使用urce配置

Activiti的发布包中没有这些类, 要把对应的类放到classpath下

...

无论使用JDBC还是DataSource,都可以设置下面的配置:

databaseType:

一般不用设置,因为可以自动通过数据库连接的元数据获取

只有自动检测失败时才需要设置.可能的值有:{h2,mysql,oracle,postgres,mssql,db2}

如果没使用默认的H2数据库就必须设置这项.这个配置会决定使用哪些创建/删除脚本和查询语句

databaseSchemaUpdate: 设置流程引擎启动和关闭时如何处理数据库表

false:默认, 检查数据库表的版本和依赖库的版本,如果版本不匹配就抛出异常

true: 构建流程引擎时,执行检查,如果需要就执行更新. 如果表不存在,就创建

create-drop: 构建流程引擎时创建数据库表,关闭流程引擎时删除这些表

JNDI数据库配置

在默认情况下,Activiti的数据库配置会放在web应用的WEB-INF/classes目录下的ties文件中. 这样做比较繁琐,因为要用户在每

次发布时,都修改Activiti源码中的ties并重新编译war文件,或者解压缩war文件,修改其中的ties

使用 JNDI(Java命名和目录接口) 来获取数据库连接,连接是由servlet容器管理的,可以在war部署外边管理配置. 与ties相比,它

也允许对连接进行更多的配置

JNDI的使用

Activiti Explorer和Activiti Rest应用从ties转换为使用JNDI数据库配置:

需要打开原始的Spring配置文件:

activiti-webapp-explorer/src/main/webapp/WEB-INF/

activiti-webapp-rest2/src/main/resources/

删除dbProperties和dataSource两个bean,然后添加如下bean:

我们需要添加包含了默认的H2配置的文件

如果已经有了JNDI配置,会覆盖这些配置.对应的配置文件activiti-webapp-explorer2/src/main/webapp/META-INF/:

name="jdbc/activitiDB"

type="urce"

scope="Shareable"

description="JDBC DataSource"

url="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=1000"

driverClassName=""

username="sa"

password=""

defaultAutoCommit="false"

initialSize="5"

maxWait="5000"

maxActive="120"

maxIdle="5"/>

如果是Activiti REST应用,则添加activiti-webapp-rest2/src/main/webapp/META-INF/:

name="jdbc/activitiDB"

type="urce"

scope="Shareable"

description="JDBC DataSource"

url="jdbc:h2:mem:activiti;DB_CLOSE_DELAY=-1"

driverClassName=""

username="sa"

password=""

defaultAutoCommit="false"

initialSize="5"

maxWait="5000"

maxActive="120"

maxIdle="5"/>

最后删除Activiti Explorer和Activiti Rest两个应用中不再使用的ties文件

JNDI的配置

JNDI数据库配置会因为使用的Servlet container不同而不同

Tomcat容器中的JNDI配置如下:

JNDI资源配置在CATALINA_BASE/conf/[enginename]/[hostname]/[warname].xml(对于Activiti Explorer来说,通常是在

CATALINA_BASE/conf/Catalina/localhost/) 当应用第一次发布时,会把这个文件从war中复制出来.所以如果这

个文件已经存在了,需要替换它.修改JNDI资源让应用连接mysql而不是H2:

name="jdbc/activitiDB"

type="urce"

description="JDBC DataSource"

url="jdbc:mysql://localhost:3306/activiti"

driverClassName=""

username="sa"

password=""

defaultAutoCommit="false"

initialSize="5"

maxWait="5000"

maxActive="120"

maxIdle="5"/>

Activiti支持的数据库

h2: 默认配置的数据库

mysql

oracle

postgres

db2

mssql

创建数据库表

创建数据库表的方法:

activiti-engine的jar放到classpath下

添加对应的数据库驱动

把Activiti配置文件()放到classpath下,指向你的数据库

执行DbSchemaCreate类的main方法

SQL DDL语句可以从Activiti下载页或Activiti发布目录里找到,在database子目录下.

脚本也包含在引擎的jar中:在org/activiti/db/create包下,drop目录里是删除语句

- SQL文件的命名方式如下:

[activiti.{db}.{create|drop}.{type}.sql]

type 是:

- engine:引擎执行的表,必须

- identity:包含用户,群组,用户与组之间的关系的表.这些表是可选的,只有使用引擎自带的默认身份管理时才需要

- history:包含历史和审计信息的表,可选的.历史级别设为none时不会使用. 注意这也会引用一些需要把数据保存到历史表中的功能

数据库表名理解

Activiti的表都以ACT_开头, 第二部分是表示表的用途的两个字母标识.用途和服务的API对应

ACT_RE_*: RE表示repository. 这个前缀的表包含了流程定义和流程静态资源

ACT_RU_*: RU表示runtime. 这些是运行时的表,包含流程实例,任务,变量,异步任务等运行中的数据. Activiti只在流程实例执行过

程中保存这些数据, 在流程结束时就会删除这些记录.这样运行时表可以一直很小速度很快

ACT_ID_*: ID 表示identity. 这些表包含身份信息. 比如用户,组等等

ACT_HI_*: HI 表示history. 这些表包含历史数据. 比如历史流程实例, 变量,任务等等

ACT_GE_*: 通用数据. 用于不同场景下

数据库升级

在执行更新之前要先使用数据库的备份功能备份数据库

默认情况下,每次构建流程引擎时都会进行版本检测.这一切都在应用启动或Activiti webapp启动时发生.如果Activiti发现数据库表的版本

与依赖库的版本不同,就会抛出异常

对配置文件进行配置来升级:

然后,把对应的数据库驱动放到classpath里.升级应用的Activiti依赖,启动一个新版本的Activiti指向包含旧版本的数据库,将

databaseSchemaUpdate设置为true,Activiti会自动将数据库表升级到新版本

当发现依赖和数据库表版本不通过时,也可以执行更新升级DDL语句

也可以执行数据库脚本,可以在Activiti下载页找到

启用Job执行器

JobExecutor是管理一系列线程的组件,可以触发定时器(包含后续的异步消息).

在单元测试场景下,很难使用多线程.因此API允许查询Job(JobQuery) 和执行Job

(eJob),

因此Job可以在单元测试中控制, 要避免与job执行器冲突,可以关闭它

默认,JobExecutor在流程引擎启动时就会激活. 如果不想在流程引擎启动后自动激活JobExecutor,可以设置

配置邮件服务器

Activiti支持在业务流程中发送邮件,可以在配置中配置邮件服务器

配置SMTP邮件服务器来发送邮件

配置历史存储

Activiti可以配置来定制历史存储信息

表达式和脚本暴露配置

默认情况下,和Spring配置文件中所有bean 都可以在表达式和脚本中使用

如果要限制配置文件中的bean的可见性,可以通过配置流程引擎配置的beans来配置

ProcessEngineConfiguration的beans是一个map.当指定了这个参数,只有包含这个map中的bean可以在表达式和脚本中使用.通过在

map中指定的名称来决定暴露的bean

配置部署缓存

因为流程定义的数据是不会改变的,为了避免每次使用访问数据库,所有流程定义在解析之后都会被缓存

默认情况下,不会限制这个缓存.如果想限制流程定义缓存,可以添加如下配置

这个配置会把默认的HashMap缓存替换成LRU缓存来提供限制. 这个配置的最佳值跟流程定义的总数有关,实际使用中会具体使用多少流程定

义也有关

也可以注入自定义的缓存实现,这个bean必须实现mentCache接口

类似的配置有knowledgeBaseCacheLimit和knowledgeBaseCache, 它们是配置规则缓存的.只有流程中使用规则任务时才用

日志

从Activiti 5.12开始,所有日志(activiti,spring,,mybatis等等)都转发给slf4j允许自定义日志实现

引入Maven依赖log4j实现,需要添加版本

4j

slf4j-log4j12

使用Maven的实例,忽略版本

4j

jcl-over-slf4j

映射诊断上下文

Activiti支持slf4j的MDC功能, 如下的基础信息会传递到日志中记录:

流程定义ID: mdcProcessDefinitionID

流程实例ID: mdcProcessInstanceID

分支ID: mdcexecutionId

默认不会记录这些信息,可以配置日志使用期望的格式来显示它们,扩展通常的日志信息. 比如,通过log4j配置定义会让日志显示上面的信

息:

sionPattern =ProcessDefinitionId=%X{mdcProcessDefinitionID}

executionId=%X{mdcExecutionId}mdcProcessInstanceID=%X{mdcProcessInstanceID} mdcBusinessKey=%X{mdcBusinessKey} %m%n"

当系统进行高风险任务,日志必须严格检查时,这个功能就非常有用,要使用日志分析的情况事件处理

Activiti中实现了一种事件机制,它允许在引擎触发事件时获得提醒

为对应的事件类型注册监听器,在这个类型的任何时间触发时都会收到提醒:

可以添加引擎范围的事件监听器,可以通过配置添加引擎范围的事件监听器在运行阶段使用API

添加event-listener到特定流程定义的BPMN XML中

所有分发的事件,都是tiEvent的子类.事件包含type,executionId,processInstanceId和

processDefinitionId. 对应的事件会包含事件发生时对应上下文的额外信息

事件监听器实现

实现事件监听器要实现tiEventListener.

下面监听器的实现会把所有监听到的事件打印到标准输出中,包括job执行的事件异常:

public class MyEventListener implements ActivitiEventListener {

@Override

public void onEvent(ActivitiEvent event) {

switch (e()) {

case JOB_EXECUTION_SUCCESS:

n("A job well done!");

break;

case JOB_EXECUTION_FAILURE:

n("A job ");

break;

default:

n("Event received: " + e());

}

}

@Override

public boolean isFailOnException() {

// The logic in the onEvent method of this listener is not critical, exceptions

// can be ignored if

return false;

}

}

isFailOnException(): 决定了当事件分发时onEvent(..) 方法抛出异常时的行为

返回false,会忽略异常

返回true,异常不会忽略,继续向上传播,迅速导致当前命令失败

当事件是一个API调用的一部分时(或其他事务性操作,比如job执行), 事务就会回滚

当事件监听器中的行为不是业务性时,建议返回false

activiti提供了一些基础的实现,实现了事件监听器的常用场景可以用来作为基类或监听器实现的样例

tityEventListener:

这个事件监听器的基类可以用来监听实体相关的事件,可以针对某一类型实体,也可以是全部实体

隐藏了类型检测,并提供了三个需要重写的方法:

onCreate(..)

onUpdate(..)

onDelete(..)

当实体创建,更新,或删除时调用

对于其他实体相关的事件,会调用onEntityEvent(..)

事件监听器的配置安装

把事件监听器配置到流程引擎配置中,会在流程引擎启动时激活,并在引擎启动过程中持续工作

eventListeners属性需要tiEventListener的队列

通常,我们可以声明一个内部的bean定义,或使用ref引用已定义的bean.下面的代码,向配置添加了一个事件监听器,任何事件触发

时都会提醒它,无论事件是什么类型:

...

为了监听特定类型的事件

可以使用typedEventListeners属性

它需要一个map参数

map的key是逗号分隔的事件名或单独的事件名

map的value是tiEventListener队列

下面的代码演示了向配置中添加一个事件监听器,可以监听job执行成功或失败:

...

分发事件的顺序是由监听器添加时的顺序决定的

首先,会调用所有普通的事件监听器(eventListeners属性),按照它们在list中的次序

然后,会调用所有对应类型的监听器(typedEventListeners属性),对应类型的事件被触发

运行阶段添加监听器

通过API:RuntimeService, 在运行阶段添加或删除额外的事件监听器:

/**

* Adds an event-listener which will be notified of ALL events by the dispatcher.

* @param listenerToAdd the listener to add

*/

void addEventListener(ActivitiEventListener listenerToAdd);

/**

* Adds an event-listener which will only be notified when an event occurs, which type is in the given types.

* @param listenerToAdd the listener to add

* @param types types of events the listener should be notified for

*/

void addEventListener(ActivitiEventListener listenerToAdd, types);

/**

* Removes the given listener from this dispatcher. The listener will no longer be notified,

* regardless of the type(s) it was registered for in the first place.

* @param listenerToRemove listener to remove

*/

void removeEventListener(ActivitiEventListener listenerToRemove);

运行阶段添加的监听器引擎重启后就消失

流程定义添加监听器

特定流程定义添加监听器:

监听器只会监听与这个流程定义相关的事件以及这个流程定义上发起的所有流程实例的事件

监听器实现:

可以使用全类名定义

引用实现了监听器接口的表达式

配置为抛出一个message,signal,error的BPMN事件

监听器执行自定义逻辑

下面代码为一个流程定义添加了两个监听器:

第一个监听器会接收所有类型的事件,它是通过全类名定义的

第二个监听器只接收作业成功或失败的事件,它使用了定义在流程引擎配置中的beans属性中的一个bean

...

对于实体相关的事件,也可以设置为针对某个流程定义的监听器,实现只监听发生在某个流程定义上的某个类型实体事件.下面的代码演示

了如何实现这种功能:

第一个例子:用于监听所有实体事件

第二个例子:用于监听特定类型的事件

...

entityType支持的值有:

attachment

comment

execution

identity-link

job

process-instance

process-definition

task

监听抛出BPMN事件

另一种处理事件的方法是抛出一个BPMN事件:

只针对与抛出一个activiti事件类型的BPMN事件, 抛出一个BPMN事件,在流程实例删除时,会导致一个错误

下面的代码演示了如何在流程实例中抛出一个signal,把signal抛出到外部流程(全局),在流程实例中抛出一个消息事件,在流程实例中抛出

一个错误事件.除了使用class或delegateExpression, 还使用了throwEvent属性,通过额外属性,指定了抛出事件的类型

如果需要声明额外的逻辑,是否抛出BPMN事件,可以扩展activiti提供的监听器类:

在子类中重写isValidEvent(ActivitiEvent event), 可以防止抛出BPMN事件.对应的类是:

eThrowingEventListener

ThrowingEventListenerTest

hrowingEventListener

流程定义监听器注意点

事件监听器只能声明在process元素中,作为extensionElements的子元素.监听器不能定义在流程的单个activity下

delegateExpression中的表达式无法访问execution上下文,这与其他表达式不同(比如gateway).它只能引用定义在流程引擎配置的

beans属性中声明的bean, 或者使用spring(未使用beans属性)中所有实现了监听器接口的spring-bean

使用监听器的class属性时,只会创建一个实例.监听器实现不会依赖成员变量,是多线程安全的

当一个非法的事件类型用在events属性或throwEvent中时,流程定义发布时就会抛出异常(会导致部署失败)

如果class或delegateExecution由问题:类不存在,不存在的bean引用,或代理类没有实现监听器接口

在流程启动时抛出异常

在第一个有效的流程定义事件被监听器接收时

所以要保证引用的类正确的放在classpath下,表达式也要引用一个有效的实例

通过API分发事件

Activiti我们提供了通过API使用事件机制的方法,允许触发定义在引擎中的任何自定义事件

建议只触发类型为CUSTOM的ActivitiEvents.可以通过RuntimeService触发事件:

/**

* Dispatches the given event to any listeners that are registered.

* @param event event to dispatch.

* * @throws ActivitiException if an exception occurs when dispatching the event or when the {@link ActivitiEventDispatcher}

* is disabled.

* @throws ActivitiIllegalArgumentException when the given event is not suitable for dispatching.

*/

void dispatchEvent(ActivitiEvent event);

支持的事件类型

引擎中每个事件类型都对应tiEventType中的一个枚举值

事件名称

ENGINE_CREATED

ENGINE_CLOSED

ENTITY_CREATED

ENTITY_INITIALIZED

ENTITY_UPDATED

ENTITY_DELETED

ENTITY_SUSPENDED

ENTITY_ACTIVATED

JOB_EXECUTION_SUCCESS

JOB_EXECUTION_FAILURE

事件描述

监听器监听的流程引擎已经创建,准备好接受API调用

监听器监听的流程引擎已经关闭,不再接受API调用

创建了一个新实体,实体包含在事件中

创建了一个新实体,初始化也完成了.如果这个实体的创建会包含子实体的创

建,这个事件会在子实体都创建/初始化完成后被触发,这是与

ENTITY_CREATED的区别

更新了已存在的实体,实体包含在事件中

删除了已存在的实体,实体包含在事件中

暂停了已存在的实体,实体包含在事件中.会被

ProcessDefinitions,ProcessInstances和Tasks抛出

激活了已存在的实体,实体包含在事件中.会被

ProcessDefinitions,ProcessInstances和Tasks抛出

作业执行成功,job包含在事件中

作业执行失败,作业和异常信息包含在事件中

事件类型

ActivitiEvent

ActivitiEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiEntityEvent

ActivitiExceptionEvent

ActivitiEntityEvent

ActivitiEntityEvent

JOB_RETRIES_DECREMENTED因为作业执行失败,导致重试次数减少.作业包含在事件中

TIMER_FIRED

JOB_CANCELED

ACTIVITY_STARTED

ACTIVITY_COMPLETED

ACTIVITY_SIGNALED

触发了定时器,job包含在事件中

取消了一个作业.事件包含取消的作业.作业可以通过API调用取消,任务完成后

ActivitiEntityEvent

对应的边界定时器也会取消,在新流程定义发布时也会取消

一个节点开始执行

一个节点成功结束

一个节点收到了一个信号

ActivitiActivityEvent

ActivitiActivityEvent

ActivitiSignalEvent

ActivitiMessageEvent

一个节点收到了一个消息.在节点收到消息之前触发,收到后,会触发

ACTIVITY_MESSAGE_RECEIVEDACTIVITY_SIGNAL或ACTIVITY_STARTED, 这会根据节点的类型:边界事

件,事件子流程开始事件

ACTIVITY_ERROR_RECEIVED

UNCAUGHT_BPMN_ERROR

ACTIVITY_COMPENSATE

VARIABLE_CREATED

VARIABLE_UPDATED

VARIABLE_DELETED

TASK_ASSIGNED

TASK_CREATED

TASK_COMPLETED

TASK_TIMEOUT

PROCESS_COMPLETED

MEMBERSHIP_CREATED

MEMBERSHIP_DELETED

MEMBERSHIPS_DELETED

一个节点收到了一个错误事件.在节点实际处理错误之前触发, 事件的

activityId对应着处理错误的节点.这个事件后续会是ACTIVITY_SIGNALLEDActivitiErrorEvent

或ACTIVITY_COMPLETE, 如果错误发送成功的话

抛出了未捕获的BPMN错误.流程没有提供针对这个错误的处理器.事件的

activityId为空

一个节点将要被补偿.事件包含了将要执行补偿的节点id

创建了一个变量.事件包含变量名,变量值和对应的分支或任务(如果存在)

更新了一个变量.事件包含变量名,变量值和对应的分支或任务(如果存在)

删除了一个变量.事件包含变量名,变量值和对应的分支或任务(如果存在)

任务被分配给了一个人员.事件包含任务

ActivitiErrorEvent

ActivitiActivityEvent

ActivitiVariableEvent

ActivitiVariableEvent

ActivitiVariableEvent

ActivitiEntityEvent

创建了新任务.它位于ENTITY_CREATE事件之后.当任务是由流程创建时,这

ActivitiEntityEvent

个事件会在TaskListener执行之前被执行

任务完成.它会在ENTITY_DELETE事件之前触发.当任务是流程一部分时,事

件会在流程继续运行之前, 后续事件将是ACTIVITY_COMPLETE,对应着完成

ActivitiEntityEvent

任务的节点

任务已超时.在TIMER_FIRED事件之后,会触发用户任务的超时事件,当这个

任务分配了一个定时器的时候

ActivitiEntityEvent

流程已结束.在最后一个节点的ACTIVITY_COMPLETED事件之后触发.当流

ActivitiEntityEvent

程到达的状态,没有任何后续连线时,流程就会结束

用户被添加到一个组里.事件包含了用户和组的id

用户被从一个组中删除.事件包含了用户和组的id

所有成员被从一个组中删除.在成员删除之前触发这个事件,所以他们都是可

以访问的.因为性能方面的考虑,不会为每个成员触发单独的

MEMBERSHIP_DELETED事件

ActivitiMembershipEvent

ActivitiMembershipEvent

ActivitiMembershipEvent

引擎内部所有ENTITY_* 事件都是与实体相关的,实体事件与实体的对应关系:

[ENTITY_CREATED],[ENTITY_INITIALIZED],[ENTITY_DELETED]:

Attachment

Comment

Deployment

Execution

Group

IdentityLink

Job

Model

ProcessDefinition

ProcessInstance

Task

User

ENTITY_UPDATED:

Attachment

Deployment

Execution

Group

IdentityLink

Job

Model

ProcessDefinition

ProcessInstance

Task

User

ENTITY_SUSPENDED, ENTITY_ACTIVATED:

ProcessDefinition

ProcessInstance

Execution

Task

注意

只有同一个流程引擎中的事件会发送给对应的监听器

如果有很多引擎在同一个数据库运行,事件只会发送给注册到对应引擎的监听器.其他引擎发生的事件不会发送给这个监听器,无论实际上

它们运行在同一个或不同的JVM中

对应的事件类型都包含对应的实体.根据类型或事件,这些实体不能再进行更新(比如,当实例以被删除).可能的话,使用事件提供的

EngineServices来以安全的方式来操作引擎.即使如此,也要小心的对事件对应的实体进行更新,操作

没有对应历史的实体事件,因为它们都有运行阶段的对应实体

本文标签: 事件流程配置监听器使用