不用 Spring手写一个 JavaWeb 框架 ③手写 AOP用 JDK 动态代理给方法装上增强插件系列连载中① 手写数据库连接池 → ② 手写 IoC 容器包扫描 三级缓存 →③ 手写 AOP本篇→ ④ 手写 MVC一个 Servlet 炼成 Spring MVC配套源码仓库github.com/JiaqiChen3518/video_stream前两篇我们解决了资源复用连接池和对象管理IoC 容器。现在对象都乖乖躺在容器里了但新的麻烦来了我想给所有 Service 方法加日志、给写操作加事务、给管理员接口做鉴权——这些代码和业务逻辑搅在一起每个方法里手动抄一遍这篇就手写一个 AOP 模块实现三个注解Log方法耗时日志、Transactional声明式事务、RequireRole角色鉴权。全程基于 JDK 动态代理也是第 ① 篇改写close()那个技巧的完全体。一、先看没有 AOP 的世界有多痛假设要给 Service 的每个方法记录耗时没有代理时只能这样publicVideogetVideoById(Longid){longstartSystem.currentTimeMillis();log.info(getVideoById 开始执行);try{VideovideovideoDao.findById(id);log.info(getVideoById 执行完成耗时 {}ms,System.currentTimeMillis()-start);returnvideo;}catch(Exceptione){log.error(getVideoById 执行异常,e);throwe;}}// 然后……每个方法复制粘贴一遍全项目 100 个方法日志、事务、鉴权这类代码有个共同点它们不属于任何一段业务逻辑却又横切在所有业务逻辑上——这就是横切关注点。AOP 的想法是把这类附加动作从业务方法里抽出来集中定义在方法调用前后由代理自动织入。业务代码回归纯粹增强逻辑一处定义、处处生效。二、60 秒复习代理模式第 ① 篇埋的伏笔回收第 ① 篇手写连接池时我们用 JDK 动态代理包装了Connection拦截close()改写成归还连接池。当时说过“代理模式会把’使用逻辑’和’管理逻辑’解耦”。AOP 本质上就是同一个套路的升级版——那次我们只拦了一个方法这次我们要拦截任意类的任意方法在调用前后插入自定义逻辑。JDK 动态代理三要素// 1. 接口代理只能以接口身份出场interfaceUserService{voidregister(Stringusername);}// 2. InvocationHandler代理收到任何方法调用都会转发给它的 invoke()classMyHandlerimplementsInvocationHandler{privatefinalObjecttarget;// 被代理的真实对象MyHandler(Objecttarget){this.targettarget;}OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{System.out.println(调用前可以做点什么);// ← 前置增强Objectresultmethod.invoke(target,args);// ← 调用真实方法System.out.println(调用后也可以做点什么);// ← 后置增强returnresult;}}// 3. Proxy创建代理实例UserServiceproxy(UserService)Proxy.newProxyInstance(UserService.class.getClassLoader(),newClass?[]{UserService.class},newMyHandler(newUserServiceImpl()));proxy.register(Tom);// 增强逻辑自动生效记住这三要素下面所有内容都是围绕在 invoke() 里写什么展开的。三、设计三个注解声明哪里要增强我们支持三种增强各用一个注解声明Target(ElementType.TYPE)// 打在类上这个类的所有方法都记录耗时日志Retention(RetentionPolicy.RUNTIME)publicinterfaceLog{}Target(ElementType.METHOD)// 打在方法上这个方法需要事务Retention(RetentionPolicy.RUNTIME)publicinterfaceTransactional{}Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceRequireRole{intvalue();// 需要的角色0-普通用户1-管理员}使用方式Log// 类级给所有方法记耗时日志ComponentpublicclassVideoServiceImplimplementsVideoService{Transactional// 方法级多步写操作包成一个事务publicvoidpublishWithStats(Videovideo){videoDao.insert(video);statsDao.incrPublishCount(video.getUserId());}RequireRole(1)// 方法级只有管理员能删视频publicvoiddeleteById(Longid){videoDao.delete(id);}}四、按需增强 多层代理包装4.1 不是所有 Bean 都值得代理创建代理有开销多一个对象、多一层反射调用所以第一步是按需判定检查这个 Bean 是否真的带增强注解一个都不带就直接返回原对象publicstaticObjectcreateProxy(Objectbean){booleanhasLogbean.getClass().isAnnotationPresent(Log.class);booleanhasTransactionalhasMethodAnnotation(bean.getClass(),Transactional.class);booleanhasRequireRolehasMethodAnnotation(bean.getClass(),RequireRole.class);if(!hasLog!hasTransactional!hasRequireRole){returnbean;// 无增强需求原样返回}// ……包代理见下文}这和 Spring 的思想一致Spring 也是先为每个 Bean 匹配一遍 Advisor匹配不到就不生成代理。4.2 多层代理像套娃一样包三层一个 Bean 可能同时需要三种增强。我们的做法是从里到外逐层包装每层代理只负责一件事Objectproxybean;if(hasRequireRole)proxycreateRoleCheckProxy(proxy);// 最内层先鉴权if(hasTransactional)proxycreateTransactionProxy(proxy);// 中间层再开事务if(hasLog)proxycreateLogProxy(proxy);// 最外层记日志returnproxy;最终的结构是Log代理 → Transaction代理 → RoleCheck代理 → 真实对象。方法被调用时从外到内依次穿过三层增强register() 被调用 → LogHandler: 打印开始执行记 startTime → TransactionHandler: 发现方法带 Transactionalbegin → RoleCheckHandler: 检查当前用户角色 要求不通过直接抛异常 → 真实方法执行 ← RoleCheck: 正常返回 ← Transaction: 正常commit抛 RuntimeExceptionrollback ← Log: 打印执行完成耗时 xx ms三层代理职责单一、互不干扰增删一种增强只需要加/减一层包装——这就是责任链 装饰器的混合体。4.3 剥洋葱多层代理怎么取到最内层的真身多层代理带来一个后续问题IoC 容器做字段注入时第 ② 篇的populateBean需要拿到原始对象来反射设置字段而不是某个中间层代理。写一个递归剥壳方法privatestaticabstractclassAbstractInvocationHandlerimplementsInvocationHandler{protectedfinalObjecttarget;/** 递归获取最内层的原始对象 */publicObjectgetRootTarget(){Objectcurrenttarget;while(currentinstanceofProxy){ObjectnextgetDirectTarget(current);// 取这一层代理的直接目标if(nextnull||nextcurrent)break;currentnext;}returncurrent;}}三层代理时getRootTarget()会连续剥三次最终拿到真正的VideoServiceImpl实例。第 ② 篇populateBean里那句AopProxyUtil.getTarget(bean)背后就是这段逻辑。五、三个调用处理器逐个拆解5.1 Log方法耗时统计privatestaticclassLogInvocationHandlerextendsAbstractInvocationHandler{OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{Stringnametarget.getClass().getSimpleName()::method.getName();log.info({} 开始执行,name);longstartSystem.currentTimeMillis();Objectresult;try{resultmethod.invoke(target,args);}catch(InvocationTargetExceptione){log.error({} 执行异常,name,e.getTargetException());throwe.getTargetException();// 关键解包抛原始异常}log.info({} 执行完成耗时 {}ms,name,System.currentTimeMillis()-start);returnresult;}}两个细节InvocationTargetException必须解包。method.invoke()反射调用时目标方法抛出的任何异常都会被包一层InvocationTargetException。如果原样上抛上层比如我们的全局异常过滤器拿到的异常类型全变了instanceof BusinessException判断直接失效。所以必须throw e.getTargetException()把原始异常还原。异常时不打耗时完成日志只打 error——日志语义清晰排查问题时不会被误导。5.2 Transactional从手动事务到声明式事务第 ① 篇的连接池就位后手写事务管理器就水到渠成了。核心是一个ThreadLocal——把数据库连接绑定到当前线程保证同一个线程里所有 DAO 操作用的是同一个连接publicclassTransactionManager{privatestaticfinalThreadLocalConnectionCONNECTION_HOLDERnewThreadLocal();publicstaticvoidbeginTransaction()throwsSQLException{ConnectionconnCONNECTION_HOLDER.get();if(connnull){// 本线程还没绑定向连接池借一个connConnectionPool.getInstance().getConnection();CONNECTION_HOLDER.set(conn);}conn.setAutoCommit(false);// 关闭自动提交 开启事务}publicstaticvoidcommit(){/* conn.commit()finally 里归还连接 */}publicstaticvoidrollback(){/* conn.rollback()finally 里归还连接 */}publicstaticConnectiongetCurrentConnection(){returnCONNECTION_HOLDER.get();// DAO 层取连接时优先用这个就能加入当前事务}}DAO 层取连接时优先取getCurrentConnection()——有事务就用事务的连接没有就走连接池正常借还。第 ① 篇的代理close()在这里又立一功commit()里那句connection.close()实际是把连接归还池子而不是物理关闭。有了手动事务声明式事务的代理就只需在调用前后包一下privatestaticclassTransactionInvocationHandlerextendsAbstractInvocationHandler{OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{booleantransactionalmethod.isAnnotationPresent(Transactional.class);try{if(transactional)TransactionManager.beginTransaction();Objectresultmethod.invoke(target,args);if(transactional)TransactionManager.commit();returnresult;}catch(InvocationTargetExceptione){Throwablecausee.getTargetException();if(transactionalcauseinstanceofRuntimeException){TransactionManager.rollback();// 运行时异常回滚}throwcause;}}}注意代理层只负责在方法边界上开关事务事务真正靠什么连接、怎么隔离全在 TransactionManager 里——横切逻辑和业务逻辑彻底分离这正是 AOP 的精髓。5.3 RequireRole方法级鉴权privatestaticclassRoleCheckInvocationHandlerextendsAbstractInvocationHandler{OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{RequireRolerequireRolemethod.getAnnotation(RequireRole.class);if(requireRole!null){IntegercurrentRoleUserContext.getRole();// 登录时从 JWT 解析进 ThreadLocalif(currentRolenull||currentRolerequireRole.value()){thrownewForbiddenException(权限不足);// 方法体根本不会被执行}}returnmethod.invoke(target,args);}}鉴权放在代理最内层第 4.2 节意味着权限不够时事务和日志代理的处理逻辑都不会被触发越权的写操作连事务都不会开启——顺序设计是有讲究的。六、照实写JDK 动态代理的一个致命局限以及 CGLIB 的存在意义实现到这一步我踩了一个很真实的坑必须诚实地讲。回看第 4.1 节我们的createProxy里其实藏着一个分支if(bean.getClass().getInterfaces().length0){log.warn({} 没有实现任何接口无法创建JDK动态代理将跳过AOP增强,...);returnbean;// ← AOP 静默失效}JDK 动态代理有一个硬性前提被代理的类必须实现接口。原理很简单——Proxy.newProxyInstance()生成的代理类本身就是implements UserService的一个子类它借接口的身份出场。没有接口它连扮演谁都不知道。后果是如果哪个程序员写了一个不实现接口的 ServiceTransactional、Log全部静默失效——不报错、不抛异常只是事务不生效、日志不出现。生产环境这种 bug 能让人排查到怀疑人生。CGLIB 就是为了解决这个问题而生。它不走实现接口路线而是在运行时用 ASM 字节码技术生成被代理类的子类重写父类方法在重写的方法里插入增强逻辑因为没有接口这个要求任何普通类包括没实现接口的都能被代理。// CGLIB 风格示意Enhancer 生成子类EnhancerenhancernewEnhancer();enhancer.setSuperclass(UserServiceImpl.class);// 以类为目标而非接口enhancer.setCallback((MethodInterceptor)(obj,method,args,proxy)-{System.out.println(前置增强);returnproxy.invokeSuper(obj,args);// 调用父类真实方法});UserServiceImplproxy(UserServiceImpl)enhancer.create();当然 CGLIB 也有自己的代价final类和方法无法被代理子类无法继承/重写且创建代理对象比 JDK 代理更重。所以 Spring 的策略是两者并存有接口默认 JDK 代理无接口或配置了proxyTargetClasstrue时走 CGLIB——理解了JDK 代理为什么需要接口才算真正理解了这个默认策略。我们 mini 框架选择了warn 并跳过这种简单粗暴的处理——写博客时我特意保留了这段代码因为它是对 JDK 代理局限最诚实的注脚。七、彩蛋级细节一个容易踩的注解查找坑代理还有个隐蔽的坑值得说。代理是以接口身份工作的invoke()收到的method参数是接口上的 Method 对象。如果Transactional只打在了实现类的方法上publicclassVideoServiceImplimplementsVideoService{Transactional// 打在实现类上publicvoidpublish(Videov){...}}那么method.isAnnotationPresent(Transactional.class)查的是接口方法——注解在实现类上这里返回false事务悄悄失效。这是 JDK 代理的经典陷阱Spring 内部用AnnotationUtils.findAnnotation()做合并查找来回避。手写版没有这个工具务实的做法是把方法级注解同时打在接口和实现类上或者统一只打一处并在文档里写清楚约定。八、对照 Spring我们写的是什么Spring 多写了什么我们的 AOP 本质是代理 注解识别。Spring AOP 的体系化在于多了三个抽象Spring 概念职责我们的对应物Advice通知增强逻辑本身前置/后置/环绕三个 InvocationHandlerPointcut切点用表达式声明哪些方法的哪些点execution(* com.x.*.*(..))注解扫描Log/TransactionalAdvisorAdvice Pointcut 的组装单元createProxy()里的三个 ifAopProxyJDK / CGLIB 两种代理创建策略只有 JDK 一种Spring 还解决了我们没解决的代理创建时机与循环依赖的协作第 ② 篇三级缓存里那个提前暴露代理的故事、方法自调用失效问题this.register()绕过了代理、以及基于AspectJ表达式的完整切面语言。但核心引擎就是这篇写的这些。九、面试快问快答Q1JDK 动态代理和 CGLIB 有什么区别JDK 动态代理CGLIB代理方式生成接口的实现类生成目标类的子类ASM 字节码前提必须实现接口无接口要求不能代理类只能代理接口方法final类/final方法性能创建快、调用走反射JDK 8 调用性能已追平创建重生成子类调用用 FastClass 索引加速Q2为什么方法自调用会导致 AOP 失效this.method()里的this是原始对象不是代理对象自然穿不过代理层的增强逻辑。解法注入自身代理、通过AopContext.currentProxy()获取当前代理、或避免自调用。Q3InvocationTargetException为什么要解包Method.invoke()会把目标方法抛出的所有异常包装成InvocationTargetException。不解包上层异常类型判断如全局异常处理器按BusinessException分类返回全部失效。Q4声明式事务为什么回滚的是 RuntimeException我们的实现里cause instanceof RuntimeException才回滚——这与 Spring 默认策略一致受检异常默认不回滚认为业务上可预期运行时异常代表系统异常必须回滚。Spring 可用rollbackFor自定义。Q5多层代理的调用顺序是怎么决定的由包装顺序决定后包的在外层先被穿过。我们把日志放最外层最先记录、最后收尾鉴权放最内层尽早拒绝、副作用最小。Spring 中用Order注解控制 Advisor 顺序思想相同。十、下篇预告系列收官到这里“对象怎么来”IoC、“方法怎么增强”AOP都有了。但还缺最后一块拼图HTTP 请求怎么找到对应的方法还记得第 ② 篇开篇那个坑吗——Tomcat 会为继承HttpServlet的类自动创建实例和 IoC 容器争夺 Bean 创建权。下一篇我们写本系列的终点手写 DispatcherServlet用一个 Servlet 接管所有请求揭秘为什么 Spring 的 Controller 是普通 POJO这个问题的完整答案。 本篇完整源码AopProxyUtil.java TransactionManager.java 注解定义如果这篇对你有帮助欢迎点赞收藏关注系列文章持续更新中
企业数字化 ERP 产品动态
相关推荐
金融服务全景解析:从五大板块到业务流程与数字化风控 1. 金融服务的真实版图:它远不止存取款那么简单很多人一听到"金融服务"这四个字,第一反应就是银行、存款、转账、信用卡。说实话,这种理解不能算错,但确实太窄了。我在这个行业里摸爬滚打了十几年,见过太多人… · 2026/9/26 7:20:34
700个AI Agent四小时拆光五道安全护栏:服务器安全防线失效全记录 凌晨两点十七分,监控大屏上最后一条护栏的状态从绿色跳成红色,我手里的咖啡差点洒在键盘上。700个AI Agent,不到四个小时,把我精心设计的五道安全护栏全部拆得干干净净。说实话,这个结果在意料之外,但回想起… · 2026/9/26 7:20:28
Ince-Gaussian光束生成涡旋阵列:VirtualLab Fusion仿真全解析 之前一直在VirtualLab Fusion里折腾结构光束仿真,总想着用现成的拉盖尔-高斯或厄米-高斯模式拼出涡旋阵列,结果不是对称性不理想,就是阵列排布太“正”,调参调到怀疑人生。后来换到Ince-Gaussian这一类解系,才意识到自… · 2026/9/26 7:58:01
Jev模型API接入与SDK集成实战:类型安全结构化输出测评 1. 这个模型到底是个什么东西Jev 模型最近在技术社区里刷屏刷得厉害,我身边好几个做 AI 应用的朋友都在群里问“这玩意儿到底怎么接”“跟其他模型比强在哪”。我花了大概三天时间,从官网文档到实际 API 调用,再到 SDK 集成,完整跑… · 2026/9/26 7:58:01
基于UniApp与Spring Boot的微信小程序问卷系统设计与实践 1. 项目背景与技术选型1.1 为什么会做一套小程序问卷系统去年接了一个企业内部的满意度调研需求,原本对方想用现成的第三方问卷平台,但聊下来发现几个问题:一是内部数据不能走外部服务,二是问卷题型比较特殊,需要嵌套逻… · 2026/9/26 7:58:01
UniApp微信小程序问卷系统开发:跨端渲染与跳题逻辑 去年团队要上线一个用户问卷,需求很直接:扫个码就能填、微信里直接打开,支持必答、跳题、单选多选填空,后台最好还能看统计。市面问卷平台大多能做到,但数据在别人那边,想二次定制也各种受限,干… · 2026/9/26 7:58:01
WorkBuddy搭配skill:HR如何用AI智能体封装简历初筛等重复工作 HR 这个岗位有个很尴尬的现实:每天处理的事情看起来都不难,但架不住量大、琐碎、还特别容易被追着问进度。招聘季筛简历筛到眼花,入离职手续一茬接一茬,员工问社保、问年假、问流程的消息永远回不完。我身边做 HR 的朋友ÿ… · 2026/9/26 7:58:01
Graspness:面向真实场景的可微分抓取置信度建模与6D位姿生成 简介:本资源是一套基于Graspness评分机制的机械臂视觉6自由度抓取完整实现方案,面向计算机、人工智能、机器人及电子信息等专业的本科生与研究生,适用于课程设计、毕业设计及机器人感知-操作一体化技术学习。项目采用Python为主开发语言&… · 2026/9/26 7:57:54
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46