同一个框架里的两种拦截器——注解驱动 vs 数据库驱动
文章目录
- 同一个框架里的两种拦截器——注解驱动 vs 数据库驱动
-
- 一、同一个模式,两种实现
- 二、第二套拦截器:流程任务级的环绕式拦截
- 三、一个升级版:异步分流
- 四、两套拦截器放一起看
- 五、为什么流程级用数据库而不是注解
- 六、接口设计:before 和 after 为什么分开
- 七、passProcess 拦截器 vs doProxy 拦截链:一个上午10点的场景
- 八、结语
一、同一个模式,两种实现
之前写过责任链拦截器——doProxy 读注解拼链,transInterceptor、logInterceptor、monitorInterceptor 三个节点串成一条链。那是方法级的,控制粒度是"这个方法需不需要事务、日志、监控"。
但在同一个框架里,还有另一套拦截器。它们不是面向方法,而是面向流程任务节点。这套拦截器在 ProcessService.passProcess 里,工作方式完全不同。
两套拦截器放在一起看,才是完整的横切关注点解决方案。
二、第二套拦截器:流程任务级的环绕式拦截
passProcess 是工作流审批通过的核心方法。每个流程节点的审核人点"通过"时,走这个方法:
passProcess(taskid):
├── claim(taskid) // 拾取任务
├── 从DB查拦截器配置 // 根据流程定义+任务节点查表
├── 循环 before 列表 → beforeTrans() // 前置拦截
├── 保存表单数据 + 提交流程 // 核心业务
└── 循环 after 列表 → afterTrans() // 后置拦截
核心代码:
// 获取任务拦截器——从数据库查
task_interceptor intercptorobj = this.getinterceptor(taskid);
String beforeid = null, afterid = null;
if (intercptorobj != null) {
beforeid = intercptorobj.getBeforeId(); // 前置拦截器ID列表
afterid = intercptorobj.getAfterId(); // 后置拦截器ID列表
}
// 执行前置拦截器
if (beforeid != null && !"".equals(beforeid)) {
String[] beforeids = beforeid.split(",");
for (int i = 0; i < beforeids.length; i++) {
before beforeobj = (before) BeanFactory.getBean(beforeids[i]);
beforeobj.beforeTrans(center);
}
}
// … 保存表单 + 提交流程 …
// 执行后置拦截器
if (afterid != null && !"".equals(afterid)) {
String[] afterids = afterid.split(",");
for (int i = 0; i < afterids.length; i++) {
after afterobj = (after) BeanFactory.getBean(afterids[i]);
afterobj.afterTrans(center);
}
}
这套拦截器有几个特点:
配置放在数据库里。task_interceptor 表存储了流程定义Key、任务节点Key、前置拦截器ID列表、后置拦截器ID列表。修改一个节点要不要加拦截器,改一行记录就行,代码不用动。
调用方式是顺序循环。不是责任链的"串一串",而是最简单的 for 循环——先跑完所有前置,再跑业务,再跑完所有后置。这样做的好处是:前置之间彼此独立,一个拦截器失败不会影响后续拦截器的调用。
before 和 after 是两个分离的接口。它们不是同一个接口的不同方法,而是两个完全独立的接口。这意味着——同一个节点可以只配前置不配后置,反之亦然。比如"社会保险关系转出"节点的 after 里配一个"同步到税务系统",但不需要前置拦截。
三、一个升级版:异步分流
在 passProcessHK 里,after 拦截器还做了一个精妙的分流:
for (int i = 0; i < afterids.length; i++) {
after afterobj = (after) BeanFactory.getBean(afterids[i]);
if (isSpecific(intercptorobj.getTaskDefKey(), afterids[i])) {
// 特定节点+特定拦截器 → 异步线程
execTax exec = new execTax(afterobj, center);
new Thread(exec).start();
} else {
// 普通情况 → 同步执行
afterobj.afterTrans(center);
}
}
这里引入了一个 isSpecific 判断——查询 configSpecificDao 表,看当前节点是否配置了某个 after 拦截器的特殊处理。比如"同步税务"这个操作,在大部分节点是同步执行(流程必须等税务确认才能往下走),但在某些只读节点可以异步执行(后台慢慢推,不影响审批人点通过)。
同一个拦截器,同样的 after 接口,在 A 节点走同步、在 B 节点走异步——这不是代码决定的,是数据库里的 configSpecificDao 表决定的。
这就是数据库驱动配置的威力:拦截策略的变更不需要重新编译、不需要重启服务,改一条SQL就上线。
四、两套拦截器放一起看
| 粒度 | 方法级(同一个类的所有方法) | 流程任务级(某个流程的某个节点) |
| 配置方式 | 注解 @Trans @Logger @monitoring | 数据库 task_interceptor 表 |
| 调用模式 | 责任链(trans→log→monitor→invoke) | 顺序循环(逐个调 before/after) |
| 拦截器接口 | 统一的 Interceptor.invoke() | 分离的 before.beforeTrans() + after.afterTrans() |
| 顺序控制 | 链的拼装顺序决定执行顺序 | 循环顺序即执行顺序 |
| 变更成本 | 改代码、编译、部署 | 改数据库一行记录 |
| 适用场景 | 通用基础设施(事务、日志、监控) | 业务级横切(数据同步、消息推送、权限检查) |
这不是两套独立的方案,而是同一个模式在不同粒度的两种表现:
- 方法级用注解,因为"这个方法要不要事务"是框架能力,跟具体业务逻辑无关
- 流程级用数据库,因为"审批通过后要不要同步税务"是业务能力,跟具体流程设计有关
五、为什么流程级用数据库而不是注解
一个很自然的问题是:passProcess 里的拦截器,为什么不像 doProxy 一样用注解?在 ProcessService 的方法上加 @Before("taxSync")、@After("pushMessage") 不就行了吗?
不行。因为同一个 passProcess 方法处理所有流程的所有节点。审批请假、报销、资产申购、合同签批——几百个流程、几千个节点,都走过同一个 passProcess 方法。你在方法上加 @Before,那就是对所有流程生效,做不到"只在社保关系转移的审批节点触发同步税务"。
数据库驱动的拦截器解决了这个问题:绑定的单位不是方法,而是 (流程定义Key, 任务节点Key) 的组合。
— task_interceptor 表结构(简化)
key | taskDefKey | beforeid | afterid
————-|————–|—————|————
social_trans | hr_approve | checkLimit | notifySMS
social_trans | finance_pay | validateFund | syncTax, pushMessage
leave_apply | manager_ok | | notifySMS
同一个 passProcess 执行到 social_trans 流程的 finance_pay 节点时,触发 validateFund 前置 + syncTax、pushMessage 后置。执行到 leave_apply 流程的 manager_ok 节点时,只触发 notifySMS 后置。
这就是数据库配置的本质:把"在哪里触发什么"从代码中抽出来,变成数据。
六、接口设计:before 和 after 为什么分开
doProxy 的链式拦截器只有一个 Interceptor 接口。passProcess 却拆成了两个:
public interface before {
void beforeTrans(DataCenter center);
}
public interface after {
void afterTrans(DataCenter center);
}
为什么要分开?
因为前置和后置的责任边界不同。前置拦截器可以拒绝执行——比如 checkLimit 发现预算超了,抛异常,流程中止,用户看到错误提示。后置拦截器不应该拒绝执行——审批已经通过了,syncTax 同步失败应该重试,不应该让用户收到"审批失败了但其实是同步税务报错"这种混淆信息。
更实际的是:很多业务场景只需要后置不需要前置(比如审批通过后发短信通知),或者只需要前置不需要后置(比如审批前校验数据完整性)。拆成两个接口,数据库里配一个字段就行,不需要配了之后在代码里判断 beforeId 为空时跳过。
七、passProcess 拦截器 vs doProxy 拦截链:一个上午10点的场景
假设一个社保局的审批人员在上午10点通过了某人的"社保关系转移"申请。
doProxy 链做的事(通用基础设施):
- transInterceptor:开启事务
- logInterceptor:记录"调用 passProcess,insid=XX"
- monitorInterceptor:开始计时
passProcess 拦截器做的事(业务流程切面):
- before: validateTransfer → 校验是否符合转出条件
- before: checkArrears → 检查是否欠费
- 业务:保存表单、完成任务、流转到下一个节点
- after: syncNationalPlatform → 同步到国家医保平台
- after: sendSMS → 给申请人发"已受理"短信
两套拦截器的分工是明确的:
- doProxy 管的是"这个代码执行得对不对"——事务、日志、监控
- passProcess 管的是"这个业务发生时要联动什么"——校验、同步、通知
八、结语
同一个框架里有两种拦截器实现,不是设计冗余,是粒度需求不同。
方法级需求(“所有方法都需要事务”)用注解,因为它是编译时就确定的通用规则。流程级需求(“社保转移审批通过时要同步税务”)用数据库,因为它是随政策变化而变化的业务规则。
两套拦截器的接口设计也不同——方法级用统一的 Interceptor 接口做责任链,流程级拆成 before 和 after 做环绕式。这不是谁更优雅的问题,是每个粒度需要什么样的控制方式。
最重要的是这个认知:拦截器不只有一种实现方式。注解驱动的、责任链串接的、CGLIB 代理的——这些是互联网后端的主流答案。但政务系统里,真正需要灵活变更的是业务流程级别的横切逻辑,数据库驱动反而更合适。改一行记录就上线,不需要走编译部署流程。
知道什么时候该用哪种——这比会用其中任何一种都重要。



