责任链三剑客——事务日志监控,注解驱动拼拦截器
责任链三剑客——事务、日志、监控,注解驱动拼拦截器
文章目录
- 责任链三剑客——事务、日志、监控,注解驱动拼拦截器
-
- 一、问题:一个方法要挂多少横切关注点
- 二、责任链的核心:每个节点都有一个"下一个"
- 三、注解驱动拼链:doProxy 的运行时组装
- 四、noProxy:空壳的妙用
- 五、proxyFilter:控制哪些方法走代理
- 六、三个拦截器的具体实现
- 七、链的顺序为什么是 trans → log → monitor
- 八、这套设计跑了多少年
一、问题:一个方法要挂多少横切关注点
做业务系统,每个方法几乎都有同样的需求:要事务、要记日志、要监控耗时。最初的做法是在每个方法里手写:
public DataCenter savePerson(DataCenter dc, HttpServletRequest req, HttpServletResponse res) {
log.debug("调用savePerson,参数:" + dc.toJson());
long begin = System.currentTimeMillis();
try {
DBUtil.BeginTrans(null, false);
// 真正的业务逻辑
DBUtil.EndTrans();
} catch (Exception e) {
DBUtil.rollback();
throw e;
} finally {
long end = System.currentTimeMillis();
monitorDao.insert(begin, end, "savePerson", ...);
}
}
几十个方法,每个都复制这段模板。漏了事务回滚就是事故,漏了日志就查不到调用链路。更麻烦的是——改监控策略要改几十个方法。
解决办法是拦截器链:把事务、日志、监控拆成三个独立模块,用注解声明哪些方法需要哪些能力,框架在运行时自动拼成一条链。
二、责任链的核心:每个节点都有一个"下一个"
责任链模式的关键是一个接口和一个字段:
public interface Interceptor {
public Object invoke(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable;
}
每个具体的拦截器实现这个接口,同时持有下一个拦截器的引用:
public class logInterceptor implements Interceptor {
private Interceptor ins; // 指向下一个拦截器
public logInterceptor(Interceptor ins) {
this.ins = ins; // 构造时传入下一个
}
public Object invoke(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.debug("调用:" + method.getName()); // 自己的事
if (ins == null) {
return methodProxy.invokeSuper(o, args); // 链尾,执行真正的方法
} else {
return ins.invoke(o, method, args, methodProxy); // 交给下一个
}
}
}
三个拦截器,每个都按这个模式:
| 拦截器 | 做的事 |
|---|---|
transInterceptor |
开事务 → 调下一个 → 提交/回滚 |
logInterceptor |
记日志 → 调下一个 |
monitorInterceptor |
记开始时间 → 调下一个 → 记结束时间→入库 |
链的结构是这样的:
transInterceptor(最外层)
└─ logInterceptor(中间层)
└─ monitorInterceptor(最内层)
└─ invokeSuper(真正的方法)
调用顺序:事务开始 → 日志记录 → 监控计时 → 业务执行 → 监控入库 → 事务提交。每个拦截器只关心自己的事,完成后交给下一个。
三、注解驱动拼链:doProxy 的运行时组装
链不是写死的。它是根据方法上的注解在运行时动态拼出来的:
// doProxy.java - CGLIB的MethodInterceptor
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) {
Annotation[] annotions = method.getAnnotations();
Interceptor interceptor = null;
for (int i = 0; i < annotions.length; i++) {
if (annotions[i] instanceof Trans) {
// 第一个注解 → new transInterceptor(null)
// 后续注解 → new transInterceptor(上一个interceptor)
if (i == 0) {
interceptor = new transInterceptor(null);
} else {
interceptor = new transInterceptor(interceptor);
}
}
// Logger 和 monitoring 同理
}
// 链拼好了,从最外层开始调用
return interceptor.invoke(o, method, args, methodProxy);
}
这段代码的逻辑很精妙:先拼链的反方向——遍历注解,第一个被处理的注解创建的拦截器 ins 指向 null(链尾),第二个指向第一个,第三个指向第二个。最终返回的是最后一个创建的拦截器,即链头。
在业务方法上加注解:
@Trans // 需要事务
@Logger // 需要日志
@monitoring // 需要监控
public DataCenter savePerson(...) { ... }
三个注解,运行时自动拼成 trans → log → monitor → 业务方法 的链。不需要这些能力的方法不加注解,doProxy 里的 self 标志位为 false,直接 invokeSuper,零开销。
四、noProxy:空壳的妙用
不是所有方法都需要拦截。但 CGLIB 的 Enhancer 为每个方法都会走 Callback。不需要代理的方法怎么办?
public class noProxy implements MethodInterceptor {
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2); // 直接透传,什么都不做
}
}
noProxy 就是一个空壳代理——CGLIB 需要它存在,但它什么事都不做,直接调用父类方法。这就是 Null Object 模式在代理场景下的应用。
五、proxyFilter:控制哪些方法走代理
CGLIB 的 CallbackFilter 决定每个方法走哪个回调:
public class proxyFilter implements CallbackFilter {
private String filerList = ""; // 注解@aoppoint的filter属性,逗号分隔的方法名列表
public int accept(Method method) {
if (filerList == null) filerList = "";
if (!(filerList.indexOf(method.getName()) >= 0))
return 0; // 不在列表里 → 走 noProxy(透传)
return 1; // 在列表里 → 走 doProxy(拦截链)
}
}
这个 filter 的作用是缩小代理范围——不是所有方法都走拦截链,只有 filterList 里指定的方法才走。其他方法一律走 noProxy 透传。这样既保证了需要事务/日志/监控的方法被拦截,又保证了 getter/setter 之类的方法零开销。
六、三个拦截器的具体实现
transInterceptor —— 事务边界
public Object invoke(...) {
try {
DBUtil.BeginTrans(null, false); // 开启事务
if (ins == null) {
result = methodProxy.invokeSuper(o, args);
} else {
result = ins.invoke(o, method, args, methodProxy);
}
// 检查注解的readonly属性
Trans trans = method.getAnnotation(Trans.class);
if (trans.readonly()) {
DBUtil.rollback(); // 只读事务直接回滚
} else {
DBUtil.EndTrans(); // 提交
}
} catch (Exception e) {
DBUtil.rollback();
throw new Throwable(e.getMessage());
} finally {
DBUtil.rollback(); // 最终兜底
}
}
注意 finally 里的 rollback()——即使 EndTrans() 执行了,再调一次 rollback() 也不会有副作用。这是防御性编程,确保连接一定被释放。
logInterceptor —— 调用链路日志
public Object invoke(...) {
if (log.isDebugEnabled()) {
log.debug("调用类:" + o.getClass().getName() + ",方法:" + method.getName() + ",参数:[...]");
}
if (ins == null) {
result = methodProxy.invokeSuper(o, args);
} else {
result = ins.invoke(o, method, args, methodProxy);
}
return result;
}
日志拦截器只在 DEBUG 级别生效,生产环境不打印参数(参数里可能含敏感数据)。轻量级,对性能影响最小。
monitorInterceptor —— 性能监控入库
public Object invoke(...) {
long begin = System.currentTimeMillis();
try {
result = ins.invoke(o, method, args, methodProxy); // 或 invokeSuper
} finally {
long end = System.currentTimeMillis();
monitor dao = new monitor();
dao.setBeginTime(BigDecimal.valueOf(begin));
dao.setEndTime(BigDecimal.valueOf(end));
dao.setClassName(class_name);
dao.setMethodName(method_name);
DBUtil.SaveOne(monitorMapper.class, "insert", dao);
}
}
监控拦截器的 finally 块保证了无论业务成功还是失败,监控数据都会入库。而且 SaveOne 的异常被单独 catch 了,监控入库失败不会影响业务方法的正常返回。
七、链的顺序为什么是 trans → log → monitor
链的顺序不是随便排的,它有逻辑:
transInterceptor(最外层)
→ logInterceptor(中间层)
→ monitorInterceptor(最内层)
→ 业务方法
trans在最外层:因为事务要包裹所有操作。日志和监控都要在事务内执行——如果监控入库失败了,可以和业务数据一起回滚。反过来,如果监控在事务外,业务成功了但监控没记录,排查问题时找不到这条调用记录。
monitor在最内层:因为它的 finally 块无条件执行。即使业务抛异常、事务回滚,监控记录也要写进去——失败的操作更需要被监控到。
log在中间:它最轻量,不需要特殊位置,放中间不影响链的逻辑。
这是运行时顺序。构造链时的拼接顺序是反的——第一个注解创建 transInterceptor(null),第二个创建 logInterceptor(trans),第三个创建 monitorInterceptor(log)。最后返回 monitorInterceptor,它是最外层,它的 ins 指向 logInterceptor。
八、这套设计跑了多少年
这套拦截器链从2010年左右设计出来,到系统2023年下线,跑了十多年。中间加过新的拦截器(权限校验、数据脱敏),改过监控的入库策略,换过日志框架。但责任链的骨架从来没变过——Interceptor 接口 + ins 字段 + 注解驱动拼链。
为什么没变?因为这个设计恰好解决了政务系统最核心的一个矛盾:每个业务方法都需要横切能力,但不能让业务代码感知这些能力的存在。加一个注解就自动获得事务、日志、监控——业务代码只写业务逻辑,框架负责基础设施。
在 AOP 的概念普及之前,这套基于 CGLIB + 责任链的实现就是我们的"切面编程"。不是最优雅的,但足够可靠——可靠到运行了十几年没人提过要换。