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

发布时间:2026/5/29 6:22:06

责任链三剑客——事务日志监控,注解驱动拼拦截器 责任链三剑客——事务、日志、监控注解驱动拼拦截器文章目录责任链三剑客——事务、日志、监控注解驱动拼拦截器一、问题一个方法要挂多少横切关注点二、责任链的核心每个节点都有一个下一个三、注解驱动拼链doProxy 的运行时组装四、noProxy空壳的妙用五、proxyFilter控制哪些方法走代理六、三个拦截器的具体实现七、链的顺序为什么是 trans → log → monitor八、这套设计跑了多少年一、问题一个方法要挂多少横切关注点做业务系统每个方法几乎都有同样的需求要事务、要记日志、要监控耗时。最初的做法是在每个方法里手写publicDataCentersavePerson(DataCenterdc,HttpServletRequestreq,HttpServletResponseres){log.debug(调用savePerson参数dc.toJson());longbeginSystem.currentTimeMillis();try{DBUtil.BeginTrans(null,false);// 真正的业务逻辑DBUtil.EndTrans();}catch(Exceptione){DBUtil.rollback();throwe;}finally{longendSystem.currentTimeMillis();monitorDao.insert(begin,end,savePerson,...);}}几十个方法每个都复制这段模板。漏了事务回滚就是事故漏了日志就查不到调用链路。更麻烦的是——改监控策略要改几十个方法。解决办法是拦截器链把事务、日志、监控拆成三个独立模块用注解声明哪些方法需要哪些能力框架在运行时自动拼成一条链。二、责任链的核心每个节点都有一个下一个责任链模式的关键是一个接口和一个字段publicinterfaceInterceptor{publicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable;}每个具体的拦截器实现这个接口同时持有下一个拦截器的引用publicclasslogInterceptorimplementsInterceptor{privateInterceptorins;// 指向下一个拦截器publiclogInterceptor(Interceptorins){this.insins;// 构造时传入下一个}publicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{log.debug(调用method.getName());// 自己的事if(insnull){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[]annotionsmethod.getAnnotations();Interceptorinterceptornull;for(inti0;iannotions.length;i){if(annotions[i]instanceofTrans){// 第一个注解 → new transInterceptor(null)// 后续注解 → new transInterceptor(上一个interceptor)if(i0){interceptornewtransInterceptor(null);}else{interceptornewtransInterceptor(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(filerListnull)filerList;if(!(filerList.indexOf(method.getName())0))return0;// 不在列表里 → 走 noProxy透传return1;// 在列表里 → 走 doProxy拦截链}}这个 filter 的作用是缩小代理范围——不是所有方法都走拦截链只有 filterList 里指定的方法才走。其他方法一律走noProxy透传。这样既保证了需要事务/日志/监控的方法被拦截又保证了 getter/setter 之类的方法零开销。六、三个拦截器的具体实现transInterceptor —— 事务边界publicObjectinvoke(...){try{DBUtil.BeginTrans(null,false);// 开启事务if(insnull){resultmethodProxy.invokeSuper(o,args);}else{resultins.invoke(o,method,args,methodProxy);}// 检查注解的readonly属性Transtransmethod.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(insnull){resultmethodProxy.invokeSuper(o,args);}else{resultins.invoke(o,method,args,methodProxy);}returnresult;}日志拦截器只在 DEBUG 级别生效生产环境不打印参数参数里可能含敏感数据。轻量级对性能影响最小。monitorInterceptor —— 性能监控入库publicObjectinvoke(...){longbeginSystem.currentTimeMillis();try{resultins.invoke(o,method,args,methodProxy);// 或 invokeSuper}finally{longendSystem.currentTimeMillis();monitor daonewmonitor();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 责任链的实现就是我们的切面编程。不是最优雅的但足够可靠——可靠到运行了十几年没人提过要换。

相关新闻