# Messenger

# 概述

复杂业务流程的运转往往需要业务系统的协助、通知业务系统流程运转的进展,流程平台通过事件驱动机制和业务系统建立关联来实现以上目的。 在流程运转的某些环节,流程平台会触发对业务系统的调用,这些环节称为事件,而业务系统中接收、处理并响应事件的模块称为Messenger。

# 接口标准

流程平台定义了一个REST接口标准来支持事件调用,业务系统可以通过任意的技术框架来实现这个接口。如果是Java开发,可以使用 InfoPlus SDK (opens new window) 来简化开发过程。

  • 接口地址: 业务系统自定义,并通过流程编辑器进行配置

  • 调用方法: POST

  • 请求头参数:

    参数名称 描述
    Authorization HTTP基本认证头 (opens new window) 。其中用户名为流程代码(包含租户域名的全局标识,如 workflow_code@sjtu.edu.cn),密码为流程密钥
  • 请求体编码: application/x-www-form-urlencoded

  • 请求体参数:

    参数名称 描述
    event 事件类型,参考事件
    data json, 参考SDK的InfoPlusEvent对象
  • 响应体编码: application/json

  • 响应体: 参考SDK的InfoPlusResponse对象

# 配置接口

  • 流程开发者可以在流程编辑器中定义业务系统Messenger的访问地址,并勾选要触发的事件,参考:

    • 流程支持为生产环节和测试环境定义不同的Messenger地址,也支持为同一环境定义多个Messenger地址。 为同一事件定义多个Messenger通常是错误的,开发者需要意识到平台不保障多个Messenger的调用顺序,因此也不保障是否触发调用。
    • 某些事件允许设置是阻塞调用还是非阻塞调用,未允许配置的都是阻塞调用。 阻塞调用时流程平台等待Messenger的处理结果,并根据结果决定是否允许事件发生,是否调整表单数据(不是所有事件均支持修改表单数据); 非阻塞调用时流程平台不等待Messenger处理结果,并假设允许事件发生且不需要调整表单数据。
    • 未勾选的事件表示无需通知业务系统,允许事件发生且不需要调整表单数据。
  • 开发过程中需要的表单数据模型,可通过IDE来生成,参考:

# 事件

流程平台支持的事件涵盖了流程运转的全生命周期。

流程平台和业务系统是两个独立的系统,没有分布式事务来保障两者数据的强一致,因此会存在系统异常而导致两个系统数据不一致的情况。

为了将这种情况最小化,流程平台尽可能的将一些涉及到表单数据持久化的事件分为发生前和发生后定义为两个事件,如INSTANCE_STARTING和INSTANCE_STARTED分别代表流程发起前和发起后。

一般发生前的事件在表单数据持久化到数据库之前触发,Messenger即可以阻止其发生,也可以通过响应数据对表单数据进行修改。 发生后的事件在表单数据持久化到数据库之后触发,此时除了事务提交之外的所有数据库操作均已完成,Messenger可以假设事件已经确定完成,此时虽然仍可以通过返回错误来迫使事务回滚从而阻止事件发生,但不可以对表单数据再进行调整。

并非所有事件可以阻止,也并非所有事件均区分发生前后,具体请参考下表,理解调用时序对正确使用事件非常关键。

# 事件列表

事件 触发时机 可修改表单数据 表单数据用途 典型用例 调用时序
INSTANCE_STARTING 流程发起前 持久化 检查是否可发起
表单数据初始化
查看
INSTANCE_STARTED 发起成功后 业务系统保存流程流水号等信息 查看
INSTANCE_COMPLETED 流程结束后 业务系统保存表单数据 查看
INSTANCE_KILLING 流程将被终止 检查是否允许终止 查看
INSTANCE_KILLED 流程被终止以后 业务系统同步状态
处理业务系统相关的逻辑
查看
INSTANCE_EXPIRING 流程级超时 超时时自动办理、终止、延时
INSTANCE_SAVING 管理员修改 持久化 合法性检查 查看
INSTANCE_SAVED 管理员修改后 业务系统保存表单数据 查看
INSTANCE_PRINTING 管理员打印 前端显示 打印时数据预处理 查看
INSTANCE_EXPORTING 管理员导出 生成文件 导出时数据预处理 查看
INSTANCE_RENDERING 欢迎页渲染 前端显示 渲染前数据预处理 查看
INSTANCE_COMPENSATION 手工触发 业务系统同步表单数据
STEP_EXPIRING 步骤/实例即将超时 干预超时行为:延期/忽略/终止
STEP_RENDERING 页面即将渲染时 前端显示 修改表单界面数据
追加代码表内容配合Select使用
查看
STEP_PRINTING 用户打印 前端显示 打印时数据预处理 查看
STEP_EXPORTING 用户导出 生成文件 导出时数据预处理 查看
STEP_WITHDRAWING 撤回前 业务系统判断是否允许撤回 查看
STEP_WITHDRAWN 撤回后 业务系统同步状态 查看
ACTION_CLICKING 动作点击以后
选步骤之前
执行流程 验证表单数据
注:请在ACTION_DOING做相同验证
查看
ACTION_SAVING 即将保存草稿 持久化 验证表单数据 查看
ACTION_SAVED 存草稿成功后 业务系统保存表单数据 查看
ACTION_DOING 步骤办理前 持久化 验证表单数据
更新表单数据
查看
ACTION_DONE 步骤办理后 业务系统保存表单数据
处理业务逻辑
查看
FIELD_CHANGING 表单字段变化 前端显示 引起界面其他字段联动。
注:启用需设置字段的Event属性
查看
FIELD_SUGGESTING 外部代码表Suggest时 前端显示 提供外部代码表数据 查看

# 关于表单数据修改和持久化

  • ACTION_CLICKING事件对表单数据的修改虽然在流程执行完成计算后续步骤后即废弃,但是它会影响后续步骤的计算结果。因此开发者需要保障ACTION_CLICKING和ACTION_DOING对表单数据修改的一致性,以免出现提示用户的后续步骤和实际提交后不符的情况。 极端情况下,这种不一致导致实际后续步骤存在必须指定执行人但未在ACTION_CLICKING后计算出,流程执行将失败。
  • STEP_RENDERING、FIELD_CHANGING、FIELD_SUGGESTING这类可修改表单数据但用于前端显示的事件,Messenger对表单数据的修改只影响前端表单显示,没有持久化到数据库中,真正的持久化将在后续保存或提交时进行。
    • 流程平台对此类事件带过来的数据和用户手工输入的一视同仁,如果需要持久化必须有足够权限
    • 重复节如果权限不足,会报FORM_DATA_UNAUTHORIZED异常;字段无权限会被忽略,导致的问题不易发现
    • 在表单提交时,如有必要,需要对带过来的数据做合法性检查或者二次赋值
  • ACTION_DOING, ACTION_SAVING等事件对表单数据的修改不受步骤权限的控制。

# 使用SDK

如前所述,Messenger开发的本质是提供一个符合接口标准的HTTP服务入口,接收流程平台的事件调用,处理并给出响应。

对于Java开发,流程平台提供了SDK来简化开发的过程,SDK处理了服务入口、认证头校验、事件分发、表单数据和Java实体转换、外部代码表缓存等一系列工作, 让开发者可以专注到业务逻辑实现本身。SDK以Maven构建库 (opens new window)形式提供,可以直接在Maven项目中引用。

    <dependency>
        <groupId>sjtu</groupId>
        <artifactId>sjtu-infoplus-atk</artifactId>
        <version>1.1-SNAPSHOT</version>
    </dependency>
1
2
3
4
5

由于并未发布到中央仓库,因此您还需要在pom.xml中添加以下配置以便能下载到

    <repositories>
        <repository>
            <id>public-snapshots</id>
            <name>SJTU Snapshots Repository</name>
            <url>https://maven.dev.sjtu.edu.cn/content/groups/public-snapshots/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>
1
2
3
4
5
6
7
8
9
10
11

SDK实现事件接收的基本框架如下图

SDK通过SubscriberServlet来提供HTTP调用入口,接收来自流程平台的事件调用。

SubscriberServlet向InfoPlusApplication查询注册的Messenger信息,确定可以处理事件的Messenger,根据事件类型调用Messenger的对应方法。

在外部代码表的支持方面,SDK设计了共享外部代码表的机制。 如果需要的外部代码表没有被Messenger直接提供,Messenger将会向InfoPlusApplication查询是否为共享外部代码表,如果是的话将转由共享外部代码表来提供支持。 设计允许多个流程共用一个外部代码表,如果业务系统存在多个流程,共用代码表通常是合理的情况。

# SDK配置

  • 入口配置

    入口配置是配置SDK提供HTTP服务入口的地址,由于SDK使用Servlet来提供HTTP服务,因此需要在web.xml中定义Servlet监听的url。 以下是一段通常的配置,它定义了SubscriberServlet在/infoplus_subscriber上提供服务。

    <servlet>
        <servlet-name>SubscriberServlet</servlet-name>
        <servlet-class>edu.sjtu.infoplus.applicationToolkit.SubscriberServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SubscriberServlet</servlet-name>
        <url-pattern>/infoplus_subscriber</url-pattern>
    </servlet-mapping>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    如果项目最终部署的服务器是host,部署的路径是context,则实际提供服务的url将是http://host/context/infoplus_subscriber。 这个地址就是在配置接口时需要填写的生成环境或者开发环境地址。

  • Messenger和外部代码表配置

    为了让流程平台的事件能正确分发,Messenger需要注册到InfoPlusApplication;外部代码表如果采用共享方式,则代码表构造器也需要注册到InfoPlusApplication。

    InfoPlusApplication设计为单例,可以通过静态方法InfoPlusApplication.defaultApplication()获取到唯一实例。 Messenger和代码表构造器也是单例,注册过程既可以通过springframework的配置文件进行,也可以通过代码方式进行。

    • springframework配置文件配置

      <bean id="infoPlusService" class="edu.sjtu.infoplus.applicationToolkit.InfoPlusService">
          <property name="engineUrl" value="https://form.sjtu.edu.cn/infoplus"/>
          <property name="debug" value="true"/>
      </bean>
      
      <bean id="application" class="edu.sjtu.infoplus.applicationToolkit.InfoPlusApplication" factory-method="defaultApplication">
          <property name="service" ref="infoPlusService"/>
          <property name="domain" value="sjtu.edu.cn"/>
          <property name="messengers">
              <list>
                  <bean class="workflow.messenger.SampleMessenger">
                      <property name="workflow" value="workflow_code"/>
                      <property name="secret" value="workflow_secret"/>
                      <property name="codeTables">
                          <list>
                              <value>private_code_table_code</value>
                          </list>
                      </property>
                  </bean>
              </list>
          </property>
          <property name="sharedCodeTableBuilders">
              <list>
                  <bean class="workflow.codeTable.SampleSharedCodeTableBuilder">
                  </bean>
              </list>
          </property>
      </bean>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
    • 代码方式配置

      import edu.sjtu.infoplus.applicationToolkit.*;
      
      ...
      
      InfoPlusService infoPlusService = new InfoPlusService();
      infoPlusService.setEngineUrl("https://form.sjtu.edu.cn/infoplus");
      infoPlusService.setDebug(true);
      
      InfoPlusApplication application = InfoPlusApplication.defaultApplication();
      application.setService(infoPlusService);
      application.setDomain("sjtu.edu.cn");
      
      SampleMessenger sampleMessenger = new SampleMessenger();
      sampleMessenger.setWorkflow("workflow_code");
      sampleMessenger.setSerect("workflow_secret");
      sampleMessenger.setCodeTables(Arrays.asList("private_code_table_code"));
      
      SampleSharedCodeTableBuilder codeTableBuilder = new SampleSharedCodeTableBuilder();
      
      application.setMessengers(Arrays.asList(sampleMessenger));
      application.setSharedCodeTableBuilders(Arrays.asList(codeTableBuilder));
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21

      以上两种方式均定义了一个messenger,一个非共享的代码表和一个共享的代码表。 配置过程中的各个属性含义,请参考JavaDoc

      以上配置中的InfoPlusService并不是必须的,但是通过以上配置建立了InfoPlusService和InfoPlusApplication的关系。

      Messenger接收流程平台的事件调整并处理,而InfoPlusService提供了对流程平台的API接口主动调用的封装。 尽管Messenger中一般不需要调用流程平台API接口,但是业务系统经常会有诸如代码发起流程、代码执行流程的需求, 此时就需要使用InfoPlusService提供的方法来完成。

      流程平台的API接口使用令牌保护,需要使用流程的代码和密钥来获取令牌。InfoPlusService内部实现了令牌的获取, 但需要获取流程的密钥才可能工作,此时独立创建的InfoPlusService对象是无法工作的。

# Messenger实现

所有的Messenger均应该继承AbstractMessenger,通过重载对应的事件方法来实现对事件的处理。 下表列出了流程平台事件和AbstractMessenger方法的对应关系,所有方法均接收唯一的入参,InfoPlusEvent,均返回InfoPlusResponse对象。

流程平台事件 AbstractMessenger方法 使用方式
INSTANCE_STARTING onInstanceStarting 重载此方法
INSTANCE_STARTED onInstanceStarted 重载此方法
INSTANCE_COMPLETED onInstanceCompleted 重载此方法
INSTANCE_KILLING onInstanceKilling 重载此方法
INSTANCE_KILLED onInstanceKilled 重载此方法
INSTANCE_EXPIRING onInstanceExpiring 重载此方法
INSTANCE_SAVING onInstanceSaving 重载此方法
INSTANCE_SAVED onInstanceSaved 重载此方法
INSTANCE_PRINTING onInstancePrinting 重载此方法
INSTANCE_EXPORTING onInstanceExporting 重载此方法
INSTANCE_RENDERING onInstanceRendering 重载此方法
INSTANCE_COMPENSATION onInstanceCompensation 重载此方法
STEP_EXPIRING onStepExpiring 根据步骤名增加方法
onStep{StepCode}Expiring
STEP_RENDERING onStepRendering 根据步骤名增加方法
onStep{StepCode}Rendering
STEP_PRINTING onStepPrinting 根据步骤名增加方法
onStep{StepCode}Printing
STEP_EXPORTING onStepExporting 根据步骤名增加方法
onStep{StepCode}Exporting
STEP_WITHDRAWING onStepWithdrawing 根据步骤名和动作名增加方法
onStep{StepCode}Action{ActionCode}Withdrawing
STEP_WITHDRAWN onStepWithdrawn 根据步骤名和动作名增加方法
onStep{StepCode}Action{ActionCode}Withdrawn
ACTION_CLICKING onActionClicking 根据步骤名和动作名增加方法
onStep{StepCode}Action{ActionCode}Clicking
ACTION_SAVING onActionSaving 重载此方法
ACTION_SAVED onActionSaved 重载此方法
ACTION_DOING onActionDoing 根据步骤名和动作名增加方法
onStep{StepCode}Action{ActionCode}Doing
ACTION_DONE onActionDone 根据步骤名和动作名增加方法
onStep{StepCode}Action{ActionCode}Done
FIELD_CHANGING onFieldChanging 重载此方法
FIELD_SUGGESTING onFieldSuggesting 重载方法 getSuggestionData

一般的事件处理包括以下几个环节:

1、 从入参InfoPlusEvent获取流程的信息。 常见的包括:

要获取信息 示例代码(e为入参InfoPlusEvent对象实例)
Bean格式表单数据 T form = e.toBean(T.class);
当前用户 InfoPlusUser user = e.getUser();
流水号 long entryId = e.getStep().getInstance().getEntryId();
主流程流水号 long entryId = e.getStep().getInstance().getEntryIdTop();
动作执行的备注 String remark = e.getStep().getRemark();

2、 业务逻辑处理,如数据检查、数据入库、数据修改等

3、返回事件处理结果。

处理结果 示例代码(e为入参InfoPlusEvent对象实例)
错误 return new InfoPlusResponse(true, true, "金额不能大于1000元");
成功,不修改表单数据 return new InfoPlusResponse();
成功,修改表单数据 return new InfoPlusResponse(e, form);
成功,修改表单数据,并设置其他变量 return new InfoPlusResponse(e, form).addAttribute("tag1", "value1");

# 外部代码表实现

外部代码表指流程平台中类型为External的代码表,外部代码表的数据未定义在平台中,当需要使用代码表数据时(如用户在表单界面的suggest控件中开始输入)流程平台通过FIELD_SUGGESTING事件向Messenger请求数据。

出于性能和用户体验考虑,SDK对外部代码表做了特殊支持,Messenger在提供外部代表表的支持时不需要重载onFieldSuggesting方法,而应该根据外部代码表的使用范围决定实现为共享外部代码表或者是非共享外部代码表。

非共享外部代码表仅对Messenger对应的流程提供支持,Messenger通过重载getSuggestionData方法来实现。 共享外部代码表对注册到InfoPlusApplication的所有Messenger对应的流程提供支持,共享外部代码表通过向InfoPlusApplication注册一个CachableDataBuilder<String, List<? extend CodeItem>>接口的实现类(以下简称构建器)来实现。

下表说明了两种实现方式的过程:

非共享外部代码表 共享外部代码表
申明支持的外部代码表的代码 调用AbstractMessenger.registerCodeTable(String codeTable)方法 实现构建器的getKey()方法,
调用InfoPlusApplication.registerSharedCodeTableBuilder()注册构建器
设置外部代码表数据缓存时间 调用AbstractMessenger.setTimeout(long timeout)方法 实现构建器的getTimeoutMillis()方法
设置是否预加载外部代码表数据 调用AbstractMessenger.setPreloadCodeTables(boolean preload)方法 构建器申明实现PreloadRequired接口
提供外部外码表的数据 重载AbstractMessenger.getSuggestionData(String codeTable)方法 实现构建器的buildCachableData()方法
主动失效缓存 调用AbstractMessenger.invalidateCodeTable(String codeTable)方法 调用InfoPlusApplication.getSharedCodeTables().invalidate(String codeTable)方法

无论共享还是非共享,在代码表数据的提供上均要求一次性提供数据全集,以List<? extend CodeItem>的形式返回,这是出于性能的考虑,便于SDK对数据进行缓存。

在缓存时间和是否预加载的设置上,同一个Messenger的所有非共享外部代码表共用同样的设置,而共享外部代码表允许每个构建器单独定义自己的策略。

一个外部代码表可以既注册为非共享外部代码表又注册为共享外部代码表,这种情况下注册了非共享外部代码表的Messenger使用非共享外部代码表的实现,其他Messenger使用共享外部代码表的实现。 这事实上成为两个独立的外部代码表,相互不影响。

如果SDK设计的外部代码表方法无法满足实际需求,比如数据全集过于庞大,可以重载onFieldSuggesting方法来提供自己的实现,但开发者需要仔细考虑如何满足性能要求。 通常在这样做之前,请思考是否根据已有的表单数据,可以供选择的代码项数量是否是固定且少量的。如果是的话,可以考虑在STEP_RENDERING或者FIELD_CHANGING事件的响应结果中提供符合条件的代码表数据,供当前页面作为内部代码表来使用。

# Java Doc

SDK的更新多信息请参考SDK Java Doc

代码示例