说说在 jBPM 工作流中如何实现【回退】功能

回退,指的是用户主动回退到当前任务的上一流程节点(上一步骤)。

想象这样一种场景,当前用户接收任务后,发现这个任务不该由他办理或者这个任务存在严重的业务问题,这时就需要回退给上一步的办理者重新办理。

解决方案如下:

  1. 识别 “需要具有回退能力” 的任务。
  2. 为上述任务设计处理回退逻辑的监听器。
  3. 回退监听器接收一个参数,用于指定回退目的地的活动 ID。之所以这样设计是因为,回退的出发地与目的地可能相隔着多个活动。这个参数也可以设计为流程变量,这样可以在流程运行时动态算出。
  4. 回退监听器动态创建一条转移路径,指向回退目的地的活动。
  5. 定义【回退任务】API。
  6. 如果回退操作造成业务 “损失”,那么就必须在【回退任务】API 中予以补偿;如果需要清除 “历史痕迹”,那么在【回退任务】API 还需要删除相关的历史记录。

假设有这样一个流程:


jPDL:

<?xml version="1.0" encoding="UTF-8"?>

<process name="Rollback" xmlns="http://jbpm.org/4.4/jpdl">
   <start g="207,195,48,48" name="start1">
      <transition to="申请"/>
   </start>
   <task assignee="Jack" g="290,191,92,52" name="申请">
      <transition to="审批"/>
   </task>
   <task assignee="Deniro" g="420,189,92,52" name="审批">
        <!-- 领导审批活动,定义具有退回【申请】活动的监听器-->
        <on event="start">
            <event-listener class="net.deniro.jbpm.test.RollbackListener">
                <field name="rollbackTo">
                    <string value="申请"/>
                </field>
            </event-listener>
        </on>
      <transition to="end1"/>
   </task>
   <end g="550,191,48,48" name="end1"/>
</process>

RollbackListener 是定义的回退监听器,它会根据注入的 rollbackTo 参数值来动态生成一条通向回退目的地的转移路径。它定义如下:

ublic class RollbackListener implements org.jbpm.api.listener.EventListener {

    static Logger logger = Logger.getLogger(RollbackListener.class);

    private static ProcessEngine processEngine = Configuration.getProcessEngine();

    /**
     * 退回的目的地
     */
    private String rollbackTo;

    /**
     * 增加退回路径
     *
     * @param execution
     * @throws Exception
     */
    @Override
    public void notify(EventListenerExecution execution) throws Exception {


        //获取流程定义对象
        ProcessInstance processInstance = execution.getProcessInstance();

        String processDefinitionId = processInstance.getProcessDefinitionId();
        ProcessDefinitionImpl processDefinition = (ProcessDefinitionImpl) processEngine
                .getRepositoryService().createProcessDefinitionQuery()
                .processDefinitionId(processDefinitionId).uniqueResult();

        //获取退回目的地活动的定义对象
        ActivityImpl toActivityImpl = processDefinition.findActivity(rollbackTo);
        if (toActivityImpl == null) {//退回目的地不存在(流程定义错误),则记录日志
            String msg = "在此 " + processDefinitionId + "流程中不存在 " + rollbackTo+ "活动";
            logger.error(msg);
            throw new Exception(msg);
        }

        //获取当前活动的定义对象
        ActivityImpl fromActivityImpl=((ExecutionImpl)execution).getActivity();

        //建立退回路径
        TransitionImpl transition=fromActivityImpl.createOutgoingTransition();
        transition.setName(fromActivityImpl.getName()+" 回退 "+rollbackTo);
        transition.setDestination(toActivityImpl);

    }
}

最后,编写回退任务 API:

public class TaskRollbackService {

    /**
     * 流程引擎
     */
    private static final ProcessEngine processEngine= Configuration.getProcessEngine();

    /**
     * 任务服务
     */
    private static final TaskService taskService=processEngine.getTaskService();

    /**
     * 退回任务
     * @param taskId 当前任务 ID
     * @param rollbackToActName 要退回的活动名称
     */
    public void completeTaskRollback(String taskId,String rollbackToActName){
        Task task=taskService.getTask(taskId);
        taskService.completeTask(task.getId(),task.getActivityName()+" 回退 " +
                ""+rollbackToActName);//完成任务

        //清除历史痕迹或补偿业务损失
    }
}

这里使用流程引擎与任务服务来实现回退;如果使用 jBPM4 的命令模式,则调用会更加优雅,而且还会受到事务的控制。

单元测试:

//发起流程实例
ProcessInstance processInstance = executionService.startProcessInstanceByKey("Rollback");

String instanceId = processInstance.getId();//实例 ID

//正常办理第一个任务
Task applyTask = taskService.findPersonalTasks("Jack").get(0);
taskService.completeTask(applyTask.getId());

//断言已到达第二个任务
processInstance = executionService.findProcessInstanceById(instanceId);
assertTrue(processInstance.isActive("审批"));

//假设在【审批】活动中未通过,则执行【回退】操作
Task auditTask = taskService.findPersonalTasks("Deniro").get(0);
TaskRollbackService rollbackService = new TaskRollbackService();
rollbackService.completeTaskRollback(auditTask.getId(), "申请");

//断言当前已在【申请】活动
processInstance = executionService.findProcessInstanceById(instanceId);
assertTrue(processInstance.isActive("申请"));

//重新【申请】
Task applyTask2 = taskService.findPersonalTasks("Jack").get(0);
taskService.completeTask(applyTask2.getId());

//【审批】通过
Task auditTask2 = taskService.findPersonalTasks("Deniro").get(0);
taskService.completeTask(auditTask2.getId());

//断言流程结束
assertProcessInstanceEnded(processInstance);

//断言流程实例已成为历史
HistoryProcessInstance historyProcessInstance = historyService
        .createHistoryProcessInstanceQuery().processInstanceId(instanceId)
        .uniqueResult();
assertNotNull(historyProcessInstance);

这里还有一些需要优化的地方:

1、TaskRollbackService 的 completeTaskRollback(String taskId,String rollbackToActName),定义了两个参数;其实 rollbackToActName,即需要回退的活动名称已经在流程定义的回退监听器中设定过了,所以其实这里可以省略。注意:这种省略适用于只存在一条回退转移路径的情况。如果存在多条回退转移路径,就需要调用者明确指定具体的回退目的地参数啦O(∩_∩)O~

2、使用 jBPM4 的命令模式来实现 completeTaskRollback 逻辑,会受到事务控制,这对于清除历史痕迹与实现业务补偿的场景来说很重要。自定义命令代码模板如下:

//使用  Configuration.getProcessEngine() 的 execute() 方法来执行自定义命令
Configuration.getProcessEngine().execute(new Command<Void>() {
    @Override
    public Void execute(Environment environment) throws Exception {
        //通过 Environment 对象,可以取得任意的流程引擎服务类
        HistoryService historyService = environment.get(HistoryService.class);
                ...
        return null;
    }
});
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,423评论 25 707
  • 2016-04-01 华杉 孟子一句“不动心”,掀起了《孟子》全书的高潮,也奠定了陆九渊、王阳明心学的源头。孔子说...
    郁萍阅读 410评论 0 0
  • 坐在奶奶的床边,和她老人家聊天,不一会儿,奶奶费力地把手从厚重的被子里抽将出来。 握着她那瘦小起皱但非常温热的手,...
    苗学光阅读 517评论 0 0
  • 《挪威的森林》 ――村上春树 死并非生的对立面, 而作为生的一部分永存。 也只有死去...
    姑娘和狗阅读 214评论 0 0