# 数据库配拦截器,不用改BPMN

数据库配拦截器,不用改BPMN

非科班野生程序员,深耕政务信息化20年,这套自研Java Web框架支撑过省级社保、跨省医保结算等核心民生系统。政务审批流程里,有些事必须在审批前后做------保存前校验数据、审批后通知申请人、完成后同步到税务系统。Activiti有Listener,但Listener写死在BPMN里,改一次要重新部署整个流程。这篇拆解怎么用数据库配置+BeanFactory动态实例化实现运行时可改的拦截器链。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


背景

Activiti提供了 ExecutionListenerTaskListener,可以在流程事件(启动、完成、签字)时触发自定义逻辑。问题是:

  1. Listener写死在BPMN里------改一个Listener要改BPMN XML、重新部署
  2. 运行中的实例怎么办?------重新部署后,已经在跑的实例可能还用旧的Listener
  3. 政务场景的拦截器经常变------今天要求保存前校验A字段,明天改成了校验B字段

能不能不改BPMN、不重新部署,在数据库里配一下就让拦截器生效?


核心思路

每个流程步骤可以配置前置拦截器(beforeId)和后置拦截器(afterId),存在数据库的 task_interceptor 表里。运行时通过框架的 BeanFactory.getBean() 动态实例化拦截器对象,调用其 beforeTrans()afterTrans() 方法。

复制代码
前端提交审批
    ↓
passProcess() 开始
    ↓
查 task_interceptor 表 → 拿到 beforeId / afterId
    ↓
前置拦截器(beforeId 逗号分隔,逐个执行)
    ├── beforeobj1.beforeTrans(center)  → 数据校验
    ├── beforeobj2.beforeTrans(center)  → 特殊业务处理
    ↓
保存表单数据 + 提交流程
    ↓
后置拦截器(afterId 逗号分隔,逐个执行)
    ├── afterobj1.afterTrans(center)    → 发通知
    └── afterobj2.afterTrans(center)    → 异步同步税务(new Thread)

数据模型

java 复制代码
// task_interceptor 表对应的实体
public class task_interceptor {
    private String key;         // 流程定义ID,如 "leave:1:12345"
    private String taskDefKey;  // 任务步骤ID,如 "usertask1"
    private String beforeId;    // 前置拦截器ID,逗号分隔,如 "checkData,calcFee"
    private String afterId;     // 后置拦截器ID,逗号分隔,如 "sendMsg,syncTax"
}

一个流程的一个步骤对应一行配置。beforeIdafterId 都是逗号分隔的字符串,支持配置多个拦截器,按顺序执行。


拦截器接口

两个接口,极其简单:

java 复制代码
// 前置拦截器接口
public interface before {
    public void beforeTrans(DataCenter dt) throws utilException;
}

// 后置拦截器接口
public interface after {
    public void afterTrans(DataCenter dt) throws utilException;
}

实现类用框架的 @bean 注解注册到 BeanFactory

java 复制代码
@bean(id = "createMatterName")
public class createMatterName implements before {
    @Override
    public void beforeTrans(DataCenter center) throws utilException {
        // 自定义逻辑
    }
}

加载拦截器配置

java 复制代码
public task_interceptor getinterceptor(String taskid) throws utilException {
    // 1. 从Activiti拿当前任务的流程定义ID和步骤ID
    Task thistask = taskService.createTaskQuery()
        .taskId(taskid).list().get(0);
    String taskDefKey = thistask.getTaskDefinitionKey();
    String key = thistask.getProcessDefinitionId();

    // 2. 用复合主键查配置表
    task_interceptor dao = new task_interceptor();
    dao.setKey(key);
    dao.setTaskDefKey(taskDefKey);
    Object[] params = { dao };
    List<task_interceptor> list = (List<task_interceptor>) 
        DBUtil.getDao(task_interceptorMapper.class, "selectByPrimaryKey", params);

    if (list != null && list.size() == 1) {
        return list.get(0);
    } else {
        return null;  // 没配置拦截器,正常走
    }
}

用 Activiti 的任务ID反查流程定义ID和步骤ID,再去 task_interceptor 表找配置。


执行拦截器链

在 passProcess() 中执行

passProcess() 是前端调用的主要审批提交入口。拦截器在"保存表单数据"的前后执行:

java 复制代码
public void passProcess(DataCenter center, ...) throws utilException {
    String taskid = (String) center.getParameters("taskid");
    // ...

    // 1. 拾取任务
    this.claim(taskid, userid);

    // 2. 加载拦截器配置
    task_interceptor intercptorobj = this.getinterceptor(taskid);
    String beforeid = null, afterid = null;
    if (intercptorobj != null) {
        beforeid = intercptorobj.getBeforeId();
        afterid = intercptorobj.getAfterId();
    }

    // 3. 执行前置拦截器
    if (beforeid != null && !"".equals(beforeid) && !"null".equals(beforeid)) {
        String[] beforeids = beforeid.split(",");
        for (int i = 0; i < beforeids.length; i++) {
            before beforeobj = (before) BeanFactory.getBean(beforeids[i]);
            if (beforeobj == null)
                throw new utilException("获取拦截器失败!", -9999);
            beforeobj.beforeTrans(center);
        }
    }

    // 4. 保存业务数据
    CommonSQLcontrol sqlcl = (CommonSQLcontrol) BeanFactory.getBean("CommonSQL");
    sqlcl.CommonSaveofbyseach(dc, request, response);

    // 5. 计算下一步处理人 + 提交流程
    // ...

    // 6. 执行后置拦截器
    if (afterid != null && !"".equals(afterid) && !"null".equals(afterid)) {
        String[] afterids = afterid.split(",");
        for (int i = 0; i < afterids.length; i++) {
            after afterobj = (after) BeanFactory.getBean(afterids[i]);
            if (afterobj == null)
                throw new utilException("获取拦截器失败!", -9999);
            afterobj.afterTrans(center);
        }
    }
}

关键:BeanFactory.getBean(beforeids[i])

拦截器的ID是字符串,运行时才确定。BeanFactory 维护了一个 ConcurrentHashMap<String, Object>,启动时扫描所有 @bean 注解的类并实例化。getBean() 直接从Map里取------不需要 Class.forName(),不需要反射,不需要Spring。

7个调用点,同一套逻辑

拦截器不只在一个地方执行。以下所有方法都包含相同的拦截器执行逻辑:

方法 场景 前置 后置
passProcess() 正常审批提交
passProcessHK() 含税务同步的审批 ✓(含异步)
passProcessNo() 不保存表单的审批
passProcessNo2() 批量审批
endInstance() 结束流程实例
suspendsave() 暂存草稿
flowControl.finish() 手动办结

异步后置拦截器

有些后置操作很耗时------比如同步数据到税务系统。如果同步阻塞主流程,审批人要等十几秒才能看到"提交成功"。

passProcessHK() 中对特定拦截器做了异步处理:

java 复制代码
// 调用后置拦截器
if (afterid != null && !"".equals(afterid)) {
    String[] afterids = afterid.split(",");
    for (int i = 0; i < afterids.length; i++) {
        after afterobj = (after) BeanFactory.getBean(afterids[i]);
        if (afterobj == null)
            throw new utilException("获取拦截器失败!", -9999);
        if (isSpecific(intercptorobj.getTaskDefKey(), afterids[i])) {
            // 税务同步拦截器 → 异步执行
            execTax exec = new execTax(afterobj, center);
            new Thread(exec).start();
        } else {
            // 其他拦截器 → 同步执行
            afterobj.afterTrans(center);
        }
    }
}

execTax 是一个 Runnable

java 复制代码
public class execTax implements Runnable {
    private after afterobj;
    private DataCenter datacenter;

    public execTax(after afterobj, DataCenter datacenter) {
        this.afterobj = afterobj;
        this.datacenter = datacenter;
    }

    @Override
    public void run() {
        try {
            afterobj.afterTrans(datacenter);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

isSpecific() 方法查一张配置表 configSpecificDao,判断某个步骤的某个拦截器是否需要异步。不是硬编码哪个拦截器异步,而是配置化的------以后新增需要异步的拦截器,加配置就行。


前端配置界面

管理员在 otherSet.jsp 里选择一个流程步骤,点击"拦截器设置"按钮,打开 editljq.jsp

复制代码
┌──────────────────────────────────┐
│         拦截器配置                 │
│                                   │
│  流程id:      leave:1:12345      │
│  环节id:      usertask1          │
│                                   │
│  前置拦截器id: checkData,calcFee  │
│  后置拦截器id: sendMsg           │
│                                   │
│           [保存]                  │
└──────────────────────────────────┘

流程ID和环节ID只读(自动从树节点带出),管理员只需填写拦截器的bean ID。保存到 task_interceptor 表,立即生效------不需要重新部署流程,不需要重启服务。


为什么这样做

为什么不用Activiti的Listener?

  1. Listener在BPMN里------改了要重新部署,影响运行中的实例
  2. Listener的生命周期绑定流程定义------同一个步骤在不同版本的流程定义里可能需要不同的Listener
  3. 政务场景下拦截器经常变------校验规则改了、通知方式改了、外部系统接口改了

为什么数据库配置更好?

  1. 改配置不改BPMN------数据库改一行,立即生效
  2. 不影响运行中的实例------每个实例启动时实时读配置
  3. 支持逗号分隔的链式调用------一个步骤配多个拦截器,按顺序执行
  4. 配合BeanFactory动态实例化------不需要硬编码class名,bean ID是字符串

为什么不直接用Spring AOP?

框架没有用Spring。BeanFactory 是自己手写的IOC容器(300行代码),拦截器是它管理的一个bean类型。不需要AOP的代理和织入------直接 getBean() + 接口调用,足够了。


决策原则

把"做什么"从BPMN里拿出来,放到数据库里。

BPMN定义的是流程的"骨架"------先A再B再C。但A之前做什么、B之后做什么,这些"肉"不应该写死在骨架上。数据库配置让拦截器成为流程的外挂件------随时加、随时改、随时撤。


你的项目里审批前后的扩展逻辑是怎么做的?是用Listener还是拦截器还是其他方案?欢迎评论区聊聊。


系列导航:


作者:许彰午 | 非科班野生程序员,深耕政务信息化20年

标签: #Java #Activiti #工作流 #拦截器 #BeanFactory #政务信息化

相关推荐
lv__pf1 小时前
redis【msb 2026金三银四redis上】
数据库·redis·缓存
布莱克6052 小时前
理解数据库聚簇索引:原理、优势与适用场景
数据库·mysql
Nturmoils3 小时前
只面对一张表:KingbaseES 超表如何简化海量时序数据管理
数据库
starrocks_stella4 小时前
StarRocks 如何查询 Paimon 半结构化数据?Variant、Shredding 与 SQL 实践
数据库
这个DBA有点耶4 小时前
MySQL迁移实战:从mysqldump到专业工具的完整选型指南
数据库·mysql·dba
小白说大模型4 小时前
LLM集成数据库的幻觉治理:当AI给出的SQL建议是错的
数据库·人工智能·sql·oracle·重构·开源
zcmodeltech5 小时前
工程机械与矿山机械沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的露天开采-井下掘进-智慧矿山全场景联动方案
数据库·stm32·单片机·嵌入式硬件·制造·多分类
曹牧5 小时前
Oracle:空值排序
数据库·oracle