searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

手把手写一个简易 AOP 框架:理解代理链与织入过程

2026-06-30 18:41:04
0
0

一、先搞清楚 AOP 到底在干什么

AOP 的全称是面向切面编程。它的核心思想是:把那些横跨多个方法的通用逻辑(比如日志记录、性能统计、权限校验)从业务代码中抽离出来,单独管理。

这些通用逻辑叫做"切面",需要被切入的方法叫做"连接点",切入的时机(方法执行前、执行后、异常时)叫做"通知"。

举个例子:你有十个业务方法,每个方法执行前都要校验用户权限。如果不用 AOP,你得在十个方法里各写一遍校验逻辑。用了 AOP,你只需要定义一个权限校验的切面,然后声明这十个方法都需要被这个切面处理,框架会自动帮你把校验逻辑插入到这十个方法的执行流程中。

这个"自动插入"的过程,就叫做"织入"。

理解了这三个概念——切面、连接点、通知,你就理解了 AOP 的全部精髓。剩下的事情,就是怎么把它实现出来。


二、一切的基础:代理模式

AOP 的底层实现,百分之百依赖代理模式。不管是 Spring AOP 还是 AspectJ,最终都是通过代理对象来实现的。

代理模式的思路很简单:你不直接调用目标对象的方法,而是调用一个代理对象。代理对象在调用目标方法之前或之后,插入你想要的额外逻辑。

最原始的做法是静态代理:为每个目标类手动写一个代理类,代理类里调用目标类的方法,并在前后加上额外逻辑。这种方式能用,但太笨了。每加一个目标类就要写一个代理类,维护成本极高。

所以我们需要动态代理:在运行时根据目标对象动态生成代理类。这样无论有多少个目标对象,都只需要一套代理生成逻辑。

动态代理有两种主流实现方式。一种是基于接口的代理:要求目标对象必须实现某个接口,代理对象也实现同一个接口,调用时通过接口方法转发。另一种是基于继承的代理:直接继承目标类,重写所有方法,在方法内部调用父类的原始方法,并在前后插入额外逻辑。

对于我们的简易 AOP 框架来说,基于接口的代理更通用,因为它不要求目标类有父类可以继承。但基于继承的代理更灵活,因为它可以代理没有实现任何接口的类。实际实现时,可以根据情况选择,或者两者都支持。


三、代理链:多个切面如何串联

理解了单个代理,接下来要解决的问题是:如果有多个切面,怎么办?

比如一个方法同时需要日志记录、权限校验、性能统计三个切面。这三个切面的执行顺序是怎样的?它们是怎么组合在一起的?

答案是代理链。

想象一下洋葱的结构。最外层是第一个切面的代理,它里面包着第二个切面的代理,第二个里面包着第三个,最核心才是目标对象本身。

当你调用这个方法时,请求先进入最外层代理。最外层代理执行自己的前置逻辑(比如记录开始时间),然后把调用转发给下一层代理。下一层代理再执行自己的前置逻辑(比如校验权限),然后继续转发。层层传递,直到到达最核心的目标对象,执行真正的业务逻辑。

业务逻辑执行完后,返回值开始往回走。每一层代理在收到返回值后,执行自己的后置逻辑(比如记录结束时间、计算耗时),然后把返回值继续往外传递,直到最外层代理把最终结果返回给调用者。

这就是代理链的工作原理。每个切面对应代理链中的一环,多个切面按顺序串联,形成一个责任链。

那么顺序怎么定?通常的规则是:在目标方法执行前,切面按声明的顺序依次执行前置逻辑;在目标方法执行后,切面按相反的顺序依次执行后置逻辑。这种设计保证了逻辑的对称性,也符合大多数人的直觉。


四、织入过程:框架如何知道该代理谁

代理链解决了"怎么执行"的问题,但还有一个更关键的问题:框架怎么知道哪个方法需要被哪些切面处理?

这就是织入要解决的核心问题。

织入的本质是一种匹配过程。你需要定义一套规则,用来描述"什么条件下,什么切面应该应用到什么方法上"。

最常见的规则是基于方法签名的匹配。比如你可以声明:所有包名以 service 开头的类中,所有以 get 开头的方法,都需要被日志切面处理。框架在启动时,会扫描所有的类,找到符合条件的方法,然后为这些方法创建代理对象。

另一种规则是基于注解的匹配。你在方法上加一个自定义注解,框架扫描到这个注解后,就知道这个方法需要被对应的切面处理。这种方式更精确,也更常用。

还有一种是基于正则表达式的匹配。你可以用通配符来描述方法名的模式,框架根据正则表达式来判断哪些方法需要被代理。

不管用哪种规则,织入过程的步骤都是一样的:

第一步,扫描。框架在启动时扫描所有的类,收集需要被代理的目标对象和方法信息。

第二步,匹配。根据预设的规则,判断每个方法应该被哪些切面处理,以及这些切面的执行顺序。

第三步,生成代理。为每个需要被代理的目标对象,按照切面的顺序生成代理链。如果有三个切面,就生成三层嵌套的代理对象。

第四步,替换。把原始的目标对象替换成代理对象。这样,后续所有对这个对象的调用,都会经过代理链,从而触发切面逻辑。

这四步走完,织入就完成了。


五、完整流程串起来看一遍

现在我们把整个流程串起来,看看一个方法调用从开始到结束,在我们的简易 AOP 框架里到底经历了什么。

假设有一个用户服务类,其中有一个获取用户信息的方法。这个方法上标注了需要日志记录和权限校验两个切面,日志在前,权限在后。

框架启动时,扫描到这个方法符合匹配规则,于是为它创建了一个代理对象。这个代理对象内部维护了一个切面列表,按顺序存放着日志切面和权限切面的处理器。

当外部调用获取用户信息这个方法时,实际上调用的是代理对象。

代理对象收到调用后,开始遍历切面列表。首先执行日志切面的前置逻辑:记录方法名、参数、开始时间。然后把调用转发给下一层——权限切面的代理。

权限切面的代理收到调用后,执行自己的前置逻辑:检查当前用户是否有权限访问这个方法。如果没有权限,直接抛出异常,后续流程终止。如果有权限,继续把调用转发给最内层——目标对象的真实方法。

目标对象执行真正的业务逻辑:查询数据库,组装用户信息,返回结果。

返回值开始往回走。先经过权限切面的代理,执行后置逻辑:记录权限校验通过的日志。然后经过日志切面的代理,执行后置逻辑:记录结束时间、计算耗时、输出完整日志。

最终,代理对象把结果返回给调用者。调用者完全感知不到中间发生了这一切,它只知道自己调用了一个方法,拿到了一个结果。但实际上,这个方法已经被两个切面增强过了。


六、几个关键设计决策

在实现这个简易框架的过程中,有几个设计决策值得仔细思考。

第一个决策:代理是在启动时生成,还是在运行时生成?

启动时生成的好处是性能好,运行时不需要再做反射和字节码操作。缺点是灵活性差,运行时无法动态修改代理关系。运行时生成的好处是灵活,可以根据条件动态决定是否代理。缺点是性能开销大。

对于简易框架来说,启动时生成是更合理的选择。先把所有需要代理的对象找出来,一次性生成好代理,后续直接用。

第二个决策:切面的执行顺序如何确定?

最简单的方式是按照切面被注册的顺序来执行。先注册的先执行前置逻辑,后执行后置逻辑。这种方式简单直接,但不够灵活。更好的方式是允许用户显式指定优先级,数字越小优先级越高,先执行。

第三个决策:异常处理怎么做?

当某个切面的前置逻辑抛出异常时,整个调用链应该立即终止,异常直接抛给调用者。后置逻辑不应该被执行,因为前置逻辑已经失败了。这个规则看起来简单,但很多初学者会忽略,导致异常被吞掉或者逻辑混乱。

第四个决策:如何避免代理链过深导致性能问题?

每多一层代理,就多一次方法调用和一次逻辑判断。如果一个方法被十个切面处理,就意味着要经过十层代理。这对性能的影响是实实在在的。

解决办法有两个:一是控制切面的数量,不要滥用;二是在框架内部做优化,比如把多个切面的前置逻辑合并成一个方法,减少调用层数。但这种优化会增加框架的复杂度,对于简易框架来说,先不考虑。


七、写完之后的收获

当你真正从零实现了一个 AOP 框架,你会对很多以前模糊的概念有全新的认识。

你会明白 Spring AOP 里的那些注解,本质上就是在告诉框架:我要在哪里织入什么切面。你会明白为什么 Spring AOP 只能代理接口方法或者非 final 方法——因为它底层用的就是动态代理。你也会明白 AspectJ 为什么更高级——因为它在编译期就把切面逻辑织入了字节码,不需要运行时代理。

更重要的是,你会理解代理链和织入这两个核心概念不是什么黑魔法,就是一层一层的对象包裹,加上一套规则匹配。

AOP 的价值不在于技术本身有多复杂,而在于它提供了一种思考方式:把横切关注点从纵切逻辑中剥离出来,让每个模块只关注自己该做的事。这种分离,才是 AOP 真正的力量。


写在最后

自己写一遍 AOP 框架,不是为了在生产环境中替代 Spring AOP。而是为了在你下次使用注解的时候,心里清楚地知道:这行代码背后,有一条代理链正在为你工作。

这种"知道为什么"的感觉,比"知道怎么用"要踏实得多。

0条评论
0 / 1000
c****t
948文章数
1粉丝数
c****t
948 文章 | 1 粉丝
原创

手把手写一个简易 AOP 框架:理解代理链与织入过程

2026-06-30 18:41:04
0
0

一、先搞清楚 AOP 到底在干什么

AOP 的全称是面向切面编程。它的核心思想是:把那些横跨多个方法的通用逻辑(比如日志记录、性能统计、权限校验)从业务代码中抽离出来,单独管理。

这些通用逻辑叫做"切面",需要被切入的方法叫做"连接点",切入的时机(方法执行前、执行后、异常时)叫做"通知"。

举个例子:你有十个业务方法,每个方法执行前都要校验用户权限。如果不用 AOP,你得在十个方法里各写一遍校验逻辑。用了 AOP,你只需要定义一个权限校验的切面,然后声明这十个方法都需要被这个切面处理,框架会自动帮你把校验逻辑插入到这十个方法的执行流程中。

这个"自动插入"的过程,就叫做"织入"。

理解了这三个概念——切面、连接点、通知,你就理解了 AOP 的全部精髓。剩下的事情,就是怎么把它实现出来。


二、一切的基础:代理模式

AOP 的底层实现,百分之百依赖代理模式。不管是 Spring AOP 还是 AspectJ,最终都是通过代理对象来实现的。

代理模式的思路很简单:你不直接调用目标对象的方法,而是调用一个代理对象。代理对象在调用目标方法之前或之后,插入你想要的额外逻辑。

最原始的做法是静态代理:为每个目标类手动写一个代理类,代理类里调用目标类的方法,并在前后加上额外逻辑。这种方式能用,但太笨了。每加一个目标类就要写一个代理类,维护成本极高。

所以我们需要动态代理:在运行时根据目标对象动态生成代理类。这样无论有多少个目标对象,都只需要一套代理生成逻辑。

动态代理有两种主流实现方式。一种是基于接口的代理:要求目标对象必须实现某个接口,代理对象也实现同一个接口,调用时通过接口方法转发。另一种是基于继承的代理:直接继承目标类,重写所有方法,在方法内部调用父类的原始方法,并在前后插入额外逻辑。

对于我们的简易 AOP 框架来说,基于接口的代理更通用,因为它不要求目标类有父类可以继承。但基于继承的代理更灵活,因为它可以代理没有实现任何接口的类。实际实现时,可以根据情况选择,或者两者都支持。


三、代理链:多个切面如何串联

理解了单个代理,接下来要解决的问题是:如果有多个切面,怎么办?

比如一个方法同时需要日志记录、权限校验、性能统计三个切面。这三个切面的执行顺序是怎样的?它们是怎么组合在一起的?

答案是代理链。

想象一下洋葱的结构。最外层是第一个切面的代理,它里面包着第二个切面的代理,第二个里面包着第三个,最核心才是目标对象本身。

当你调用这个方法时,请求先进入最外层代理。最外层代理执行自己的前置逻辑(比如记录开始时间),然后把调用转发给下一层代理。下一层代理再执行自己的前置逻辑(比如校验权限),然后继续转发。层层传递,直到到达最核心的目标对象,执行真正的业务逻辑。

业务逻辑执行完后,返回值开始往回走。每一层代理在收到返回值后,执行自己的后置逻辑(比如记录结束时间、计算耗时),然后把返回值继续往外传递,直到最外层代理把最终结果返回给调用者。

这就是代理链的工作原理。每个切面对应代理链中的一环,多个切面按顺序串联,形成一个责任链。

那么顺序怎么定?通常的规则是:在目标方法执行前,切面按声明的顺序依次执行前置逻辑;在目标方法执行后,切面按相反的顺序依次执行后置逻辑。这种设计保证了逻辑的对称性,也符合大多数人的直觉。


四、织入过程:框架如何知道该代理谁

代理链解决了"怎么执行"的问题,但还有一个更关键的问题:框架怎么知道哪个方法需要被哪些切面处理?

这就是织入要解决的核心问题。

织入的本质是一种匹配过程。你需要定义一套规则,用来描述"什么条件下,什么切面应该应用到什么方法上"。

最常见的规则是基于方法签名的匹配。比如你可以声明:所有包名以 service 开头的类中,所有以 get 开头的方法,都需要被日志切面处理。框架在启动时,会扫描所有的类,找到符合条件的方法,然后为这些方法创建代理对象。

另一种规则是基于注解的匹配。你在方法上加一个自定义注解,框架扫描到这个注解后,就知道这个方法需要被对应的切面处理。这种方式更精确,也更常用。

还有一种是基于正则表达式的匹配。你可以用通配符来描述方法名的模式,框架根据正则表达式来判断哪些方法需要被代理。

不管用哪种规则,织入过程的步骤都是一样的:

第一步,扫描。框架在启动时扫描所有的类,收集需要被代理的目标对象和方法信息。

第二步,匹配。根据预设的规则,判断每个方法应该被哪些切面处理,以及这些切面的执行顺序。

第三步,生成代理。为每个需要被代理的目标对象,按照切面的顺序生成代理链。如果有三个切面,就生成三层嵌套的代理对象。

第四步,替换。把原始的目标对象替换成代理对象。这样,后续所有对这个对象的调用,都会经过代理链,从而触发切面逻辑。

这四步走完,织入就完成了。


五、完整流程串起来看一遍

现在我们把整个流程串起来,看看一个方法调用从开始到结束,在我们的简易 AOP 框架里到底经历了什么。

假设有一个用户服务类,其中有一个获取用户信息的方法。这个方法上标注了需要日志记录和权限校验两个切面,日志在前,权限在后。

框架启动时,扫描到这个方法符合匹配规则,于是为它创建了一个代理对象。这个代理对象内部维护了一个切面列表,按顺序存放着日志切面和权限切面的处理器。

当外部调用获取用户信息这个方法时,实际上调用的是代理对象。

代理对象收到调用后,开始遍历切面列表。首先执行日志切面的前置逻辑:记录方法名、参数、开始时间。然后把调用转发给下一层——权限切面的代理。

权限切面的代理收到调用后,执行自己的前置逻辑:检查当前用户是否有权限访问这个方法。如果没有权限,直接抛出异常,后续流程终止。如果有权限,继续把调用转发给最内层——目标对象的真实方法。

目标对象执行真正的业务逻辑:查询数据库,组装用户信息,返回结果。

返回值开始往回走。先经过权限切面的代理,执行后置逻辑:记录权限校验通过的日志。然后经过日志切面的代理,执行后置逻辑:记录结束时间、计算耗时、输出完整日志。

最终,代理对象把结果返回给调用者。调用者完全感知不到中间发生了这一切,它只知道自己调用了一个方法,拿到了一个结果。但实际上,这个方法已经被两个切面增强过了。


六、几个关键设计决策

在实现这个简易框架的过程中,有几个设计决策值得仔细思考。

第一个决策:代理是在启动时生成,还是在运行时生成?

启动时生成的好处是性能好,运行时不需要再做反射和字节码操作。缺点是灵活性差,运行时无法动态修改代理关系。运行时生成的好处是灵活,可以根据条件动态决定是否代理。缺点是性能开销大。

对于简易框架来说,启动时生成是更合理的选择。先把所有需要代理的对象找出来,一次性生成好代理,后续直接用。

第二个决策:切面的执行顺序如何确定?

最简单的方式是按照切面被注册的顺序来执行。先注册的先执行前置逻辑,后执行后置逻辑。这种方式简单直接,但不够灵活。更好的方式是允许用户显式指定优先级,数字越小优先级越高,先执行。

第三个决策:异常处理怎么做?

当某个切面的前置逻辑抛出异常时,整个调用链应该立即终止,异常直接抛给调用者。后置逻辑不应该被执行,因为前置逻辑已经失败了。这个规则看起来简单,但很多初学者会忽略,导致异常被吞掉或者逻辑混乱。

第四个决策:如何避免代理链过深导致性能问题?

每多一层代理,就多一次方法调用和一次逻辑判断。如果一个方法被十个切面处理,就意味着要经过十层代理。这对性能的影响是实实在在的。

解决办法有两个:一是控制切面的数量,不要滥用;二是在框架内部做优化,比如把多个切面的前置逻辑合并成一个方法,减少调用层数。但这种优化会增加框架的复杂度,对于简易框架来说,先不考虑。


七、写完之后的收获

当你真正从零实现了一个 AOP 框架,你会对很多以前模糊的概念有全新的认识。

你会明白 Spring AOP 里的那些注解,本质上就是在告诉框架:我要在哪里织入什么切面。你会明白为什么 Spring AOP 只能代理接口方法或者非 final 方法——因为它底层用的就是动态代理。你也会明白 AspectJ 为什么更高级——因为它在编译期就把切面逻辑织入了字节码,不需要运行时代理。

更重要的是,你会理解代理链和织入这两个核心概念不是什么黑魔法,就是一层一层的对象包裹,加上一套规则匹配。

AOP 的价值不在于技术本身有多复杂,而在于它提供了一种思考方式:把横切关注点从纵切逻辑中剥离出来,让每个模块只关注自己该做的事。这种分离,才是 AOP 真正的力量。


写在最后

自己写一遍 AOP 框架,不是为了在生产环境中替代 Spring AOP。而是为了在你下次使用注解的时候,心里清楚地知道:这行代码背后,有一条代理链正在为你工作。

这种"知道为什么"的感觉,比"知道怎么用"要踏实得多。

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0