AOP与IOC的关系
AOP(面向切面编程)是一种编程设计思想,旨在通过拦截业务过程的切面,实现特定模块化的能力,降低业务逻辑之间的耦合度。这一思路在众多知名项目中都有实践。例如Spring的切点PointCut、gRPC的拦截器Interceptor、Dubbo的过滤器Filter。AOP只是一种概念,这种概念被应用在不同的场景下,产生了不同的实现。我们首先讨论比较具体的RPC场景,以gRPC为例。
图片摘自grpc.io
针对一次RPC过程,gRPC提供了可供用户扩展的Interceptor接口,方便开发者写入与业务相关的拦截逻辑。例如引入鉴权、服务发现、可观测等能力,在gRPC生态中存在很多基于Interceptor的扩展实现,可参考go-grpc-middlewa[1]。这些扩展实现归属于gRPC生态,限定于Client和Server两侧的概念,限定于RPC场景。
我们将具象的场景抽象化,参考Spring的做法。
Spring具备强大的依赖注入能力,在此基础之上,提供了适配与业务对象方法的AOP能力,可以通过定义切点,将拦截器封装在业务函数外部。这些“切面”、“切点”的概念,都是限定于Spring框架内,由其依赖注入(也就是IOC)能力所管理。
我想表达的观点是,AOP的概念需要结合具体场景落地,必须受到来自所集成生态的约束。我认为单独提AOP的概念,是不具备开发友好性和生产意义的,例如我可以按照面向过程编程的思路,写一连串的函数调用,也可以说这是实现了AOP,但其不具备可扩展性、可迁移性、更不具备通用性。这份约束是必要的,可强可弱,例如Spring生态的AOP,较弱的约束具备较大的可扩展性,但实现起来相对复杂,发者需要学习其生态的众多概念与API,再若Dubbo、gRPC生态的适配于RPC场景的AOP,开发者只需要实现接口并以单一的API注入即可,其能力相对局限。
上述“约束”在实际开发场景可以具象为依赖注入,也就是IOC。开发者需要使用的对象由生态所纳管、封装,无论是Dubbo的Invoker、还是Spring的Bean,IOC过程为AOP的实践提供了约束借口,提供了模型,提供了落地价值。
Go生态与AOP
AOP概念与语言无关,虽然我赞成使用AOP的最佳实践方案需要Java语言,但我不认为AOP是Java语言的专属。在我所熟悉的Go生态中,依然有较多基于AOP思路的优秀项目,这些项目的共性,也如我上一节所阐述的,都是结合特定生态,解决特定业务场景问题,其中解决问题的广度,取决于其IOC生态的约束力。IOC是基石,AOP是IOC生态的衍生物,一个不提供AOP的IOC生态可以做的很干净很清爽,而一个提供AOP能力的IOC生态,可以做的很包容很强大。
上个月我开源了IOC-golang[2]服务框架,专注于解决Go应用开发过程中的依赖注入问题。很多开发者把这个框架和Google开源的wi[3]框架做比较,认为没有wi清爽好用,这个问题的本质是两个生态的设计初衷不同。wi注重IOC而非AOP,因此开发者可以通过学习一些简单的概念和API,使用脚手架和代码生成能力,快速实现依赖注入,开发体验很好。IOC-golang注重基于IOC的AOP能力,并拥抱这一层的可扩展性,把AOP能力看作这一框架和其他IOC框架的差异点和价值点。
相比于解决具体问题的SDK,我们可以把依赖注入框架的IOC能力看作“弱约束的IOC场景”,通过两个框架差异点比较,抛出两个核心的问题:
Go生态在“弱约束IOC的场景”需不需要AOP?GO生态在“弱约束IOC的场景”的AOP可以用来做什么?
我的观点是:Go生态一定是需要AOP的,即使在“弱约束IOC场景”,依然可以使用AOP来做一些业务无关的事情,比如增强应用的运维可观测能力。由于语言特性,Go生态的AOP不能和Java划等号,Go不支持注解,限制了开发者使用编写业务语义AOP层的便利性,所以我认为Go的AOP并不适合处理业务逻辑,即使强行实现出来,也是反直觉的。我更接受把运维可观测能力赋予Go生态的AOP层,而开发者对于AOP是无感知的。
例如,对于任何接口的实现结构,都可以使用IOC-golang框架封装运维AOP层,从而让一个应用程序的所有对象都具备可观测能力。除此之外,我们也可以结合RPC场景、服务治理场景、故障注入场景,产生出更多“运维”领域的扩展思路。
IOC-golang的AOP原理
使用Go语言实现方法代理的思路有二,分别为通过反射实现接口代理,和基于Monkey补丁的函数指针交换。后者不依赖接口,可以针对任何结构的方法封装函数代理,需要侵入底层汇编代码,关闭编译优化,对于CPU架构有要求,并且在处理并发请求时会显著削弱性能。
前者的生产意义较大,依赖接口,也是本节所讨论的重点。
3.1IOC-golang的接口注入
在本框架开源的第一篇文章中有提到,IOC-golang在依赖注入的过程具备两个视角,结构提供者和结构使用者。框架接受来自结构提供者定义的结构,并按照结构使用者的要求把结构提供出来。结构提供者只需