数据库配拦截器,不用改BPMN
非科班野生程序员,深耕政务信息化20年,这套自研Java Web框架支撑过省级社保、跨省医保结算等核心民生系统。政务审批流程里,有些事必须在审批前后做------保存前校验数据、审批后通知申请人、完成后同步到税务系统。Activiti有Listener,但Listener写死在BPMN里,改一次要重新部署整个流程。这篇拆解怎么用数据库配置+BeanFactory动态实例化实现运行时可改的拦截器链。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。
背景
Activiti提供了 ExecutionListener 和 TaskListener,可以在流程事件(启动、完成、签字)时触发自定义逻辑。问题是:
- Listener写死在BPMN里------改一个Listener要改BPMN XML、重新部署
- 运行中的实例怎么办?------重新部署后,已经在跑的实例可能还用旧的Listener
- 政务场景的拦截器经常变------今天要求保存前校验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"
}
一个流程的一个步骤对应一行配置。beforeId 和 afterId 都是逗号分隔的字符串,支持配置多个拦截器,按顺序执行。
拦截器接口
两个接口,极其简单:
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?
- Listener在BPMN里------改了要重新部署,影响运行中的实例
- Listener的生命周期绑定流程定义------同一个步骤在不同版本的流程定义里可能需要不同的Listener
- 政务场景下拦截器经常变------校验规则改了、通知方式改了、外部系统接口改了
为什么数据库配置更好?
- 改配置不改BPMN------数据库改一行,立即生效
- 不影响运行中的实例------每个实例启动时实时读配置
- 支持逗号分隔的链式调用------一个步骤配多个拦截器,按顺序执行
- 配合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 #政务信息化