系列文4轻量AOP落地CGLIB代理责任链搞定事务日志监控非科班野生程序员深耕政务信息化20年这套自研Java Web框架支撑过省级新农保、全国跨省医保结算等核心民生系统18年稳定运行至今。本系列拆解10个核心架构决策全是政务场景踩坑后的实用解法不求优雅但求落地愿同赛道朋友少走弯路也欢迎懂行大佬轻拍指正。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景事务管理、日志记录、性能监控——这些横切关注点不应该写在每个业务方法里。但加这些又不想引入 AspectJ 那套编译时织入或加载时织入的复杂机制。我没有用Spring整个框架只依赖自己的代码 MyBatis CGLIB。选CGLIB就是因为一个jar就能搞定代理不需要任何额外的基础设施。AspectJ连Spring都没有AspectJ更不可能引入。能不能用最简单的方式实现 AOP我的方案用CGLIB 代理 注解 责任链模式三个组件搞定1. 代理过滤器——proxyFilter.java这个类决定哪些方法需要代理哪些不需要packagecom.browise.core.proxy;importjava.lang.reflect.Method;importnet.sf.cglib.proxy.CallbackFilter;publicclassproxyFilterimplementsCallbackFilter{privateStringfilerList;publicvoidsetFilerList(StringfilerList){this.filerListfilerList;}Overridepublicintaccept(Methodarg0){if(filerListnull)filerList;if(!(filerList.indexOf(arg0.getName())0))return0;return1;}}逻辑很简单如果方法名在filerList里返回 1不代理否则返回 0代理。这里有个反向逻辑——filerList里列的是不需要代理的方法名。配合aoppoint(filter getQuery)注解使用意思是getQuery方法不需要代理不拦截其他方法都要。注意源码里的变量名就是filerList少了个 t保持原样。2. 什么都不做的代理——noProxy.javapackagecom.browise.core.proxy;importjava.lang.reflect.Method;importnet.sf.cglib.proxy.MethodInterceptor;importnet.sf.cglib.proxy.MethodProxy;publicclassnoProxyimplementsMethodInterceptor{OverridepublicObjectintercept(Objectarg0,Methodarg1,Object[]arg2,MethodProxyarg3)throwsThrowable{returnarg3.invokeSuper(arg0,arg2);}}这个拦截器什么也不做直接调用原方法。配合proxyFilter使用——被过滤掉的方法走这个拦截器相当于不做任何增强。3. 核心代理——doProxy.java这是 AOP 的核心负责根据方法上的注解组装责任链packagecom.browise.core.proxy;importjava.lang.annotation.Annotation;importjava.lang.reflect.Method;importcom.browise.core.annotation.Logger;importcom.browise.core.annotation.Trans;importcom.browise.core.annotation.monitoring;importcom.browise.core.interceptor.Interceptor;importcom.browise.core.interceptor.impl.logInterceptor;importcom.browise.core.interceptor.impl.monitorInterceptor;importcom.browise.core.interceptor.impl.transInterceptor;importnet.sf.cglib.proxy.MethodInterceptor;importnet.sf.cglib.proxy.MethodProxy;publicclassdoProxyimplementsMethodInterceptor{OverridepublicObjectintercept(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{booleanselffalse;Objecto1null;Annotation[]annotionsmethod.getAnnotations();Interceptorinterceptornull;//遍历注解数组if(annotions!null){for(inti0;iannotions.length;i){if(annotions[i]instanceofTrans||annotions[i]instanceofLogger||annotions[i]instanceofmonitoring){selftrue;}if(i0annotions[i]instanceofTrans){interceptornewtransInterceptor(null);}elseif(annotions[i]instanceofTrans){interceptornewtransInterceptor(interceptor);}if(i0annotions[i]instanceofLogger){interceptornewlogInterceptor(null);}elseif(annotions[i]instanceofLogger){interceptornewlogInterceptor(interceptor);}if(i0annotions[i]instanceofmonitoring){interceptornewmonitorInterceptor(null);}elseif(annotions[i]instanceofmonitoring){interceptornewmonitorInterceptor(interceptor);}}if(self){o1interceptor.invoke(o,method,args,methodProxy);}else{o1methodProxy.invokeSuper(o,args);}}else{o1methodProxy.invokeSuper(o,args);}returno1;}}责任链的工作方式这段代码的核心是根据方法上的注解动态组装责任链遍历方法上所有注解遇到Trans→ 创建transInterceptor事务拦截器遇到Logger→ 创建logInterceptor日志拦截器遇到monitoring→ 创建monitorInterceptor监控拦截器关键点每个新的拦截器都把当前的interceptor变量作为构造参数传入然后覆盖interceptor变量。所以最终interceptor指向的是最后一个注解对应的拦截器——它是最外层。以Trans Logger monitoring为例i0 Transi1 Loggeri2 monitoringi0: interceptor transInterceptor(null) i1: interceptor logInterceptor(transInterceptor) // 覆盖 i2: interceptor monitorInterceptor(logInterceptor) // 覆盖最终interceptor指向monitorInterceptor它是最外层。如果方法上有任何一个 AOP 注解self true就调用责任链的invoke()方法从最外层开始执行。如果没有 AOP 注解直接调用原方法。注意链条顺序取决于注解的声明顺序。谁写在最后谁就在最外层。责任链接口所有拦截器实现同一个Interceptor接口packagecom.browise.core.interceptor;publicinterfaceInterceptor{publicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable;}每个拦截器通过构造函数持有下一个拦截器的引用形成链条。以Trans Logger monitoring为例组装后的链条doProxy.interceptor → monitorInterceptor → logInterceptor → transInterceptor → 实际方法执行顺序monitorInterceptor最外层记录开始时间logInterceptor记录调用日志transInterceptor最内层开启事务 → 执行业务方法 → 提交/回滚回到monitorInterceptor计算耗时写入 monitor 表transInterceptor——事务拦截器packagecom.browise.core.interceptor.impl;publicclasstransInterceptorimplementsInterceptor{privateInterceptorins;publictransInterceptor(Interceptorins){this.insins;}OverridepublicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{Objectresultnull;try{DBUtil.BeginTrans(null,false);if(insnull){resultmethodProxy.invokeSuper(o,args);}else{resultins.invoke(o,method,args,methodProxy);}Transtransmethod.getAnnotation(Trans.class);if(trans.readonly()){DBUtil.rollback();}else{DBUtil.EndTrans();}}catch(Exceptione){DBUtil.rollback();thrownewThrowable(e.getMessage());}finally{DBUtil.rollback();}returnresult;}}事务拦截器在Trans Logger monitoring注解顺序下是最内层离业务方法最近。关键细节BeginTrans(null, false)开启事务false 表示非只读如果没有下一个拦截器直接调用原方法否则调用下一个拦截器支持只读事务Trans(readonlytrue)时执行完直接 rollback不持久化。这个在政务系统中很常用——很多查询方法需要事务环境比如延迟加载但不应该修改数据异常时 rollback 并抛出finally 里还有一次rollback()——即使正常提交了也会再调一次。这是兜底保险EndTrans()提交后连接归还连接池如果下一个业务获取到同一个连接可能会帮这个连接误提交未清理的事务状态。所以 finally 里无条件调一次 rollback确保连接干干净净地归还logInterceptor——日志拦截器packagecom.browise.core.interceptor.impl;publicclasslogInterceptorimplementsInterceptor{privateInterceptorins;privateLoglogLogFactory.getFactory().getInstance(logInterceptor.class);publiclogInterceptor(Interceptorins){this.insins;}OverridepublicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{Objectresultnull;StringBuffermsgnewStringBuffer();if(log.isDebugEnabled()){msg.append(调用类);msg.append(o.getClass().getName());msg.append(,调用方法);msg.append(method.getName());msg.append(,参数[);for(inti0;iargs.length;i){msg.append(args[i].toString());if(iargs.length-1)msg.append(,);}msg.append(]);}log.debug(msg.toString());if(insnull){resultmethodProxy.invokeSuper(o,args);}else{resultins.invoke(o,method,args,methodProxy);}returnresult;}}日志拦截器的特点只在 debug 级别开启时才拼接参数字符串——这是一个性能考虑。生产环境通常不开 debug这样参数的toString()不会被调用日志在方法调用前输出调用后没有再记一次——记录的是调用入口不是耗时日志内容包括类名、方法名、参数值monitorInterceptor——性能监控拦截器packagecom.browise.core.interceptor.impl;publicclassmonitorInterceptorimplementsInterceptor{privateInterceptorins;privateLoglogLogFactory.getFactory().getInstance(monitorInterceptor.class);publicmonitorInterceptor(Interceptorins){this.insins;}OverridepublicObjectinvoke(Objecto,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{Objectresultnull;StringBufferparamsnewStringBuffer();longbegin,end;beginSystem.currentTimeMillis();Stringclass_nameo.getClass().getName();Stringmethod_namemethod.getName();for(inti0;iargs.length;i){params.append(args[i].toString());if(iargs.length-1)params.append(,);}try{if(insnull){resultmethodProxy.invokeSuper(o,args);}else{resultins.invoke(o,method,args,methodProxy);}}catch(Throwablee){throwe;}finally{endSystem.currentTimeMillis();monitor daonewmonitor();dao.setBeginTime(BigDecimal.valueOf(begin));dao.setEndTime(BigDecimal.valueOf(end));dao.setClassName(class_name);dao.setMethodName(method_name);dao.setParams(params.toString());dao.setOp(AppContextContainer.getAppContext().getUser().getPsn_id());//保存监控异常不影响正常业务try{DBUtil.SaveOne(monitorMapper.class,insert,dao);}catch(Exceptione){log.error(保存监控错误信息为e.getMessage());}}returnresult;}}监控拦截器是三个里面最重的——它把监控数据持久化到数据库了不是简单的打日志。记录方法开始时间和结束时间毫秒级保存到monitor表类名、方法名、参数、开始时间、结束时间、操作员操作员从AppContextContainer获取系列文6的 ThreadLocal 上下文监控数据的保存不影响正常业务——用 try-catch 包住保存失败只记错误日志不抛异常放在finally里确保无论成功还是异常都能记录代理的创建回到BeanFactory代理是在实例化阶段创建的if(cl.isAnnotationPresent(aoppoint.class)!cl.isAnnotationPresent(responseMapping.class)){map.put(d.id(),createproxy(cl));}else{map.put(d.id(),cl.newInstance());}createproxy()方法用 CGLIB 的Enhancer创建代理对象同时设置doProxy和noProxy两个回调用proxyFilter做分发privateObjectcreateproxy(Class?cls){EnhancerenhancernewEnhancer();enhancer.setSuperclass(cls);aoppoint aop(aoppoint)cls.getAnnotation(aoppoint.class);proxyFilter filternewproxyFilter();filter.setFilerList(aop.filter());enhancer.setCallbacks(newCallback[]{newdoProxy(),newnoProxy()});enhancer.setCallbackFilter(filter);returnenhancer.create();}业务代码怎么写业务代码只需要加注解横切关注点完全不侵入bean(idorderService)responseMapping(key/order)aoppoint(filtergetQuery)publicclassOrderService{responseMapping(key/save)TransLoggermonitoringpublicDataCentersaveOrder(DataCenterdc){// 只写业务逻辑事务、日志、监控自动处理}responseMapping(key/query)publicDataCentergetQuery(DataCenterdc){// getQuery 在 filter 列表里不会被代理}}aoppoint(filter getQuery)表示这个类的getQuery方法不走代理。其他方法只要加了Trans/Logger/monitoring就会自动被代理增强。为什么不用 AspectJAspectJ 需要编译时字节码织入或加载时织入部署复杂CGLIB 是纯 Java 实现一个 jar 搞定我只需要方法级别的拦截不需要 AspectJ 那些高级的切点表达式注解驱动的方式对业务代码侵入最小决策原则用加注解的方式实现 AOP不需要 AspectJ 那套复杂的体系。事务、日志、监控这些是几乎所有业务方法都需要的横切关注点。用注解 代理的方式业务代码只需要在方法上加一行注解剩下的交给框架处理。回头看这三个拦截器的设计有几个点值得一提链条顺序由注解声明顺序决定——谁写在最后谁就是最外层。如果想让事务包住一切把Trans写在最后面monitoring Logger TranstransInterceptor 的 finally 兜底——无条件 rollback 确保连接归还连接池时没有残留事务防止被下一个业务误提交日志有性能意识——isDebugEnabled()判断避免了生产环境无意义的字符串拼接监控数据写库而非写日志——运维可以通过 SQL 查询哪个方法慢、谁调的比翻日志文件方便得多监控不影响业务——保存失败只记 error 日志不抛异常。不能因为监控功能拖垮业务你在项目中是怎么处理事务、日志这些横切关注点的用的什么方案欢迎评论区聊聊。系列导航- 上一篇[系列文3前后端彻底解耦统一入参解析前端只发JSON后端随意]- 下一篇[系列文5解决Java编译痛点ASM字节码直读100%获取方法参数名]作者许彰午| 非科班野生程序员深耕政务信息化20年标签#Java #AOP #CGLIB #代理模式 #责任链 #事务管理 #日志 #性能监控 #政务信息化 #技术复盘