当前位置: 首页 > news >正文

责任链三剑客——事务日志监控,注解驱动拼拦截器

责任链三剑客——事务、日志、监控,注解驱动拼拦截器

文章目录

  • 责任链三剑客——事务、日志、监控,注解驱动拼拦截器
    • 一、问题:一个方法要挂多少横切关注点
    • 二、责任链的核心:每个节点都有一个"下一个"
    • 三、注解驱动拼链:doProxy 的运行时组装
    • 四、noProxy:空壳的妙用
    • 五、proxyFilter:控制哪些方法走代理
    • 六、三个拦截器的具体实现
    • 七、链的顺序为什么是 trans → log → monitor
    • 八、这套设计跑了多少年

一、问题:一个方法要挂多少横切关注点

做业务系统,每个方法几乎都有同样的需求:要事务、要记日志、要监控耗时。最初的做法是在每个方法里手写:

publicDataCentersavePerson(DataCenterdc,HttpServletRequestreq,HttpServletResponseres){log.debug("调用savePerson,参数:"+dc.toJson());longbegin=System.currentTimeMillis();try{DBUtil.BeginTrans(null,false);// 真正的业务逻辑DBUtil.EndTrans();}catch(Exceptione){DBUtil.rollback();throwe;}finally{longend=System.currentTimeMillis();monitorDao.insert(begin,end,"savePerson",...);}}

几十个方法,每个都复制这段模板。漏了事务回滚就是事故,漏了日志就查不到调用链路。更麻烦的是——改监控策略要改几十个方法。

解决办法是拦截器链:把事务、日志、监控拆成三个独立模块,用注解声明哪些方法需要哪些能力,框架在运行时自动拼成一条链。

二、责任链的核心:每个节点都有一个"下一个"

责任链模式的关键是一个接口和一个字段:

publicinterfaceInterceptor{publicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable;}

每个具体的拦截器实现这个接口,同时持有下一个拦截器的引用:

publicclasslogInterceptorimplementsInterceptor{privateInterceptorins;// 指向下一个拦截器publiclogInterceptor(Interceptorins){this.ins=ins;// 构造时传入下一个}publicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{log.debug("调用:"+method.getName());// 自己的事if(ins==null){returnmethodProxy.invokeSuper(o,args);// 链尾,执行真正的方法}else{returnins.invoke(o,method,args,methodProxy);// 交给下一个}}}

三个拦截器,每个都按这个模式:

拦截器做的事
transInterceptor开事务 → 调下一个 → 提交/回滚
logInterceptor记日志 → 调下一个
monitorInterceptor记开始时间 → 调下一个 → 记结束时间→入库

链的结构是这样的:

transInterceptor(最外层) └─ logInterceptor(中间层) └─ monitorInterceptor(最内层) └─ invokeSuper(真正的方法)

调用顺序:事务开始 → 日志记录 → 监控计时 → 业务执行 → 监控入库 → 事务提交。每个拦截器只关心自己的事,完成后交给下一个。

三、注解驱动拼链:doProxy 的运行时组装

链不是写死的。它是根据方法上的注解在运行时动态拼出来的:

// doProxy.java - CGLIB的MethodInterceptorpublicObjectintercept(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy){Annotation[]annotions=method.getAnnotations();Interceptorinterceptor=null;for(inti=0;i<annotions.length;i++){if(annotions[i]instanceofTrans){// 第一个注解 → new transInterceptor(null)// 后续注解 → new transInterceptor(上一个interceptor)if(i==0){interceptor=newtransInterceptor(null);}else{interceptor=newtransInterceptor(interceptor);}}// Logger 和 monitoring 同理}// 链拼好了,从最外层开始调用returninterceptor.invoke(o,method,args,methodProxy);}

这段代码的逻辑很精妙:先拼链的反方向——遍历注解,第一个被处理的注解创建的拦截器ins指向null(链尾),第二个指向第一个,第三个指向第二个。最终返回的是最后一个创建的拦截器,即链头

在业务方法上加注解:

@Trans// 需要事务@Logger// 需要日志@monitoring// 需要监控publicDataCentersavePerson(...){...}

三个注解,运行时自动拼成trans → log → monitor → 业务方法的链。不需要这些能力的方法不加注解,doProxy里的self标志位为false,直接invokeSuper,零开销。

四、noProxy:空壳的妙用

不是所有方法都需要拦截。但 CGLIB 的Enhancer为每个方法都会走Callback。不需要代理的方法怎么办?

publicclassnoProxyimplementsMethodInterceptor{publicObjectintercept(Objectarg0,Methodarg1,Object[]arg2,MethodProxyarg3)throwsThrowable{returnarg3.invokeSuper(arg0,arg2);// 直接透传,什么都不做}}

noProxy就是一个空壳代理——CGLIB 需要它存在,但它什么事都不做,直接调用父类方法。这就是 Null Object 模式在代理场景下的应用。

五、proxyFilter:控制哪些方法走代理

CGLIB 的CallbackFilter决定每个方法走哪个回调:

publicclassproxyFilterimplementsCallbackFilter{privateStringfilerList="";// 注解@aoppoint的filter属性,逗号分隔的方法名列表publicintaccept(Methodmethod){if(filerList==null)filerList="";if(!(filerList.indexOf(method.getName())>=0))return0;// 不在列表里 → 走 noProxy(透传)return1;// 在列表里 → 走 doProxy(拦截链)}}

这个 filter 的作用是缩小代理范围——不是所有方法都走拦截链,只有 filterList 里指定的方法才走。其他方法一律走noProxy透传。这样既保证了需要事务/日志/监控的方法被拦截,又保证了 getter/setter 之类的方法零开销。

六、三个拦截器的具体实现

transInterceptor —— 事务边界

publicObjectinvoke(...){try{DBUtil.BeginTrans(null,false);// 开启事务if(ins==null){result=methodProxy.invokeSuper(o,args);}else{result=ins.invoke(o,method,args,methodProxy);}// 检查注解的readonly属性Transtrans=method.getAnnotation(Trans.class);if(trans.readonly()){DBUtil.rollback();// 只读事务直接回滚}else{DBUtil.EndTrans();// 提交}}catch(Exceptione){DBUtil.rollback();thrownewThrowable(e.getMessage());}finally{DBUtil.rollback();// 最终兜底}}

注意finally里的rollback()——即使EndTrans()执行了,再调一次rollback()也不会有副作用。这是防御性编程,确保连接一定被释放。

logInterceptor —— 调用链路日志

publicObjectinvoke(...){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);}returnresult;}

日志拦截器只在 DEBUG 级别生效,生产环境不打印参数(参数里可能含敏感数据)。轻量级,对性能影响最小。

monitorInterceptor —— 性能监控入库

publicObjectinvoke(...){longbegin=System.currentTimeMillis();try{result=ins.invoke(o,method,args,methodProxy);// 或 invokeSuper}finally{longend=System.currentTimeMillis();monitor dao=newmonitor();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 + 责任链的实现就是我们的"切面编程"。不是最优雅的,但足够可靠——可靠到运行了十几年没人提过要换。

http://www.jsqmd.com/news/907999/

相关文章:

  • SpeakFaster:基于大语言模型的AAC缩写扩展系统,为运动障碍者提升60%输入效率
  • 告别Putty!Tabby终端保姆级安装与SSH/SFTP配置全攻略(Windows版)
  • AI偏见如何被编码:从数据收集到算法设计的全链路审视与应对
  • 新手避坑指南:在Ubuntu 20.04 ROS Noetic下用Rviz和Gazebo调试激光雷达数据
  • Ubuntu 22.04重启后网卡‘消失’?别慌,5分钟搞定ens33和netplan配置
  • 给算法竞赛新手的团队协作手册:如何像一支职业队一样打ACM?
  • STM32物联网项目避坑指南:MQTT心跳包、串口资源与OneNET连接稳定性优化
  • 从电子琴仿真到多场景测试:详解 Quartus 13.0 下 ModelSim 多套 Testbench 的配置与管理实战
  • SQuId工具实战:多语言语音合成质量自动化评估指南
  • 基于NLU的COVID-19文献智能探索:从语义检索到知识聚合
  • Windows下YOLOv8训练保姆级教程:从数据集制作到模型推理(附避坑点)
  • SMUDebugTool:AMD Ryzen系统硬件调试的终极指南
  • AI时代网络安全范式转移:开发者如何应对生成式AI带来的攻防变革
  • 给数学恐惧症的程序员:用Python可视化柯西中值定理,理解参数方程与函数的关系
  • 基于Makey Makey与3D打印的脑瘫患者辅助开关设计与制作
  • 程序员平均对接一个AI平台用了多少小时?比如我用QQ大模型广场对接,deepseek-v4-flash,用了大约一天时间吧。 收到SSE数据还得人工解析
  • FreeRTOS任务通知的“隐藏玩法”:一个API模拟信号量、事件组甚至队列?
  • 出差党福音:用NPS+腾讯云轻量服务器,5分钟搞定远程家里游戏主机的内网穿透
  • 大语言模型安全实战:高级提示词注入攻击与纵深防御体系构建
  • 企业无线网络改造实录:用华为AC旁挂方案,搞定老旧交换机下的Wi-Fi覆盖
  • 保姆级教程:用PFC 7.0搞定岩土双轴压缩模拟(从建模到结果分析)
  • 别再死记硬背公式了!用Python+NumPy手把手实现状态空间方程的零阶保持法离散化
  • 别再傻傻分不清SIL和PL了!给工控安全新手的5分钟概念扫盲(附IEC61508/ISO13849-1对照表)
  • 基于规则引擎的古典诗歌生成器:从词库构建到格律控制的实践
  • springboot鹿邑县旅游网站99312(源码+文档)
  • Sigrity Power SI 2024提取S参数保姆级教程:从PCB导入到结果解读,新手避坑指南
  • 构建持续有效的反洗钱体系:从架构设计到实战运营
  • 从RS到T触发器:一张图搞定所有触发器互转原理(附74系列芯片实战接线)
  • 如何导出手机微信聊天记录到HTM格式,得到sqlite数据库文件?
  • Karate Club:一站式图机器学习算法库,80+算法统一接口快速验证