作者|李志新(季枫)AOP与IOC的关系AOP(Aspect-OrientedProgramming)是一种编程设计思想,旨在通过拦截业务流程的方面来实现特定模块优化和降低业务逻辑之间耦合的能力。这个想法已经在许多知名项目中得到实践。比如Spring的PointCut,gRPC的Interceptor,Dubbo的Filter。AOP只是一个概念,应用于不同的场景,产生不同的实现。我们先讨论更具体的RPC场景,以gRPC为例。图片取自grpc.io对于一个RPC进程,gRPC提供了Interceptor接口,用户可以扩展,方便开发者编写业务相关的拦截逻辑。比如引入鉴权、服务发现、可观察性等能力,gRPC生态中有很多基于Interceptor的扩展实现。作为参考,go-grpc-middleware[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生态和AOPAOP概念与语言无关,虽然我同意使用AOP的最佳实践解决方案需要Java语言,但我不认为AOP是Java语言独有的。在我熟悉的Go生态中,还是有很多基于AOP思想的优秀项目。这些项目的共性,我在上一节中解释过,就是结合特定的生态来解决特定的业务场景问题。广度取决于其IOC生态的约束力。IOC是基石,AOP是IOC生态的衍生品。一个不提供AOP的IOC生态可以很干净清爽,而一个提供AOP能力的IOC生态可以很包容很强大。上个月开源了IOC-golang[2]服务框架,重点解决Go应用开发过程中的依赖注入问题。许多开发者将此框架与谷歌开源的wire[3]框架进行比较,认为不如wire清爽易用。这个问题的本质是两个生态的设计初衷不同。Wire专注于IOC而不是AOP,因此开发者可以通过学习一些简单的概念和API,使用脚手架和代码生成能力,快速实现依赖注入,开发体验非常好。IOC-golang专注于基于IOC的AOP能力,拥抱这一层的可扩展性,将AOP能力视为本框架与其他IOC框架的区别和价值点。相对于解决特定问题的SDK,我们可以将依赖注入框架的IOC能力看成是一种“弱约束的IOC场景”。通过对比两个框架的差异,提出了两个核心问题:Go生态在“弱约束IOC场景”下是否需要AOP?GO生态的AOP在“弱约束IOC场景”中可以用来做什么?我的观点是:Go生态必须要求AOP。即使在“弱约束IOC场景”下,AOP依然可以用来做一些业务相关的事情,比如增强应用运维的可观察性。由于语言特性,Go生态的AOP不能等同于Java。Go不支持注解,限制了开发者编写业务语义AOP层的便利性。因此,我认为Go的AOP不适合处理业务逻辑,即使是强制出来的,也是违反直觉的。我更愿意赋予Go生态的AOP层运维的可观察性,而开发者对AOP是无意识的。比如对于任何一个接口的实现结构,都可以使用IOC-golang框架来封装运维AOP层,从而可以观察到一个应用的所有对象。此外,我们还可以结合RPC场景、服务治理场景、故障注入场景,在“运维”领域产生更多的扩展思路。IOC-golang的AOP原理使用Go语言实现方法代理。有两种思路,一种是通过反射实现接口代理,另一种是基于Monkeypatch的函数指针交换。后者不依赖于接口,可以为任意结构的方法封装函数代理。需要侵入底层汇编代码,关闭编译优化。它对CPU架构有要求,在处理并发请求时会明显削弱性能。前者生产意义更大,依赖于接口,这也是本节的重点。3.1IOC-golang的接口注入在本框架开源的第一篇文章中提到,IOC-golang在依赖注入过程中有两个视角,结构提供者和结构使用者。框架接受结构提供者定义的结构,并根据结构使用者的要求提供结构。结构提供者只需要关注结构本体,不需要关注结构实现了哪些接口。结构体用户需要关心结构体的注入和使用:是否注入到接口中?注入指针?是通过API获取的吗?还是通过标签注入获取?通过标签注入依赖对象//+ioc:autowire=true//+ioc:autowire:type=singletontypeAppstruct{//将实现注入结构指针ServiceStruct*ServiceStruct`singleton:""`//将实现注入接口ServiceImplService`singleton:"main.ServiceImpl1"`}App的ServiceStruct字段是指向特定结构的指针。字段本身已经可以定位到期望注入的结构,所以不需要在标签中指定期望注入的结构名称。对于这种注入结构体指针的字段,通过接口代理注入是无法提供AOP能力的,只能通过上面提到的monkeypatch方案,不推荐。App的ServiceImpl字段是一个名为Service的接口,期望注入的结构体指针是main.ServiceImpl。本质上是一个从结构到接口的断言逻辑。尽管框架可以验证接口的实现,但是结构的用户仍然需要确保注入的接口实现了方法。对于这种注入接口的方式,IOC-golang框架会自动为main.ServiceImpl结构体创建一个代理,并将代理结构体注入到ServiceImpl字段中,所以这个接口字段具有AOP能力。因此,ioc建议开发者面向接口编程,而不是直接依赖具体的结构体。除了AOP能力,面向接口编程还会提高go代码的可读性、单元测试能力、模块解耦程度。IOC-golang框架的开发者可以通过API获取结构体指针,调用自动加载模型(如单例)的GetImpl方法可以获取结构体指针。funcGetServiceStructSingleton()(*ServiceStruct,error){i,err:=singleton.GetImpl("main.ServiceStruct",nil)iferr!=nil{returnnil,err}impl:=i.(*ServiceStruct)返回impl,nil}建议使用IOC-golang框架的开发者通过API获取接口对象。通过调用自动加载模型(如单例)的GetImplWithProxy方法,可以获得代理结构,并将该结构断言为接口使用。这个接口不是structprovider手动创建的,而是iocli自动生成的“struct-specificinterface”,下面会解释。funcGetServiceStructIOCInterfaceSingleton()(ServiceStructIOCInterface,error){i,err:=singleton.GetImplWithProxy("main.ServiceStruct",nil)iferr!=nil{returnnil,err}impl:=i.(ServiceStructIOCInterface)返回impl,nil}这两个通过API获取对象的方法可以通过iocli工具自动生成。注意,这些代码的作用是方便开发者调用API,减少代码量。ioc自动加载的逻辑核心不是由工具生成的。这是与wire提供的依赖注入实现思路的区别之一,也是很多开发者误解的一点。IOC-golang的结构特定接口。通过上面的介绍,我们知道IOC-golang框架推荐的AOP注入方式是强依赖接口的。但是,开发者需要为自己的所有结构编写一个匹配的接口,这会消耗大量的时间。因此iocli工具可以自动生成特定于结构的接口,减少开发者编写的代码量。例如,一个名为ServiceImpl的结构包含一个GetHelloString方法。//+ioc:autowire=true//+ioc:autowire:type=singletontypeServiceImplstruct{}func(s*ServiceImpl)GetHelloString(namestring)string{returnfmt.Sprintf("这是ServiceImpl1,你好%s",name)}执行iocligen命令时,会在当前目录下生成一个代码zz_generated.ioc.go,里面包含结构体的“专属接口”:typeServiceImplIOCInterfaceinterface{GetHelloString(namestring)string}name独占接口是$(Structurename)IOCInterface,独占接口包含了该结构的所有方法。专用接口的目的有两个:减少开发人员的工作量,方便通过API直接访问代理结构,方便作为字段直接注入。structure-specific接口可以直接定位到structureID,所以在注入dedicatedinterface时,标签不需要显式指定结构类型://+ioc:autowire=true//+ioc:autowire:type=singletontypeAppstruct{//注入ServiceImplStructure-specificinterface,不需要在labelServiceOwnInterfaceServiceImplIOCInterface`singleton:""`}中指定结构ID`singleton:""`}因此,如果你发现一个现有的go项目,其中使用了结构指针,我们建议更换它具有特定于结构的接口,框架默认注入代理;对于已经使用接口的字段,我们建议直接通过标签注入结构体,框架默认也会注入到代理中。按照这个模型开发的所有项目对象都将具备运维能力。3.2代理生成和注入上一节提到的“注入接口”的对象,都是框架默认封装为代理的,具有运维能力,其中提到iocli会为所有结构生成“专属接口”.在本节中,我们将解释框架如何封装代理层并将其注入到接口中。上面提到的代理结构体的代码生成和注册,在生成的zz.generated.ioc.go代码中包含了结构体的具体接口,也包含了结构体代理的定义。还是以上面提到的ServiceImpl结构体为例,它生成的代理结构体如下:name)}代理结构体命名为$(structurename)_以小写字母开头,实现了“结构体特定接口”的所有方法,并将所有方法调用委托给$(methodname)_的方法字段,这将是Frameworksareimplementedinreflection.和结构代码一样,在这个生成的文件中,代理结构也被注册到框架中:},})}代理对象的注入以上内容描述了代理结构的定义和注册过程。当用户期望获得一个封装了AOP层的代理对象时,会先加载真实对象,然后尝试加载代理对象,最后通过反射实例化代理对象注入到接口中,从而得到接口运维能力。流程可以如下图所示:IOC-golangAOP-basedapplication理解了上面提到的实现思路后,我们可以认为在使用IOC-golang框架开发的应用程序中,注入和从框架获取的所有接口对象都是具备运维能力。基于AOP的思想,我们可以扩展我们期望的能力。我们提供了一个简单的电子商务系统demoshopping-system[4],展示了IOC-golang在分布式场景下基于AOP的可视化能力。感兴趣的开发者可以参考README,在自己的集群中运行这个系统,体验它的运维能力基础。4.1方法和参数可以观察查看应用接口和方法%ioclilistgithub.com/alibaba/ioc-golang/extension/autowire/rpc/protocol/protocol_impl.IOCProtocol[InvokeExport]github.com/ioc-golang/shopping-system/internal/auth.Authenticator[Check]github.com/ioc-golang/shopping-system/pkg/service/festival/api.serviceIOCRPCClient[ListCardsListCachedCards]监听调用参数通过iocliwatch命令,我们可以监听Check方法认证接口调用:iocliwatchgithub.com/ioc-golang/shopping-system/internal/auth.AuthenticatorCheck发起调用入口curl-i-XGET'localhost:8080/festival/listCards?user_id=1&num=10'查看被监控方法的调用参数和返回值,用户id为1。%iocliwatchgithub.com/ioc-golang/shopping-system/internal/auth.AuthenticatorCheck===========随叫随到==========github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()Param1:(int64)1===========OnResponse===========github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()Response1:(bool)true4.2基于AOP的全链接追踪IOC-golang层,可以在分布式场景下提供全认证,无用户感知和业务入侵。链接可追溯性。即用该框架开发的系统可以以任意接口方法为入口,以方法粒度收集跨进程调用的全链路。结合shopping-system整个环节的耗时信息,可以发现节日进程的gorm.First()方法是系统的瓶颈所在。该能力的实现包括两个部分,即进程内的方法粒度链接跟踪,以及进程间的RPC调用链接跟踪。IOC旨在为开发者打造开箱即用的应用开发生态组件。这些内置的组件和框架提供的RPC能力都具有运维能力。基于AOP的进程内链接跟踪IOC-golang提供的链接跟踪能力的进程内实现是基于AOP层的。为了让业务不知情,我们没有通过context上下文来识别调用链接。相反,它由goroutineid标识。使用go运行时调用栈记录当前调用相对于入口函数的深度。IOC-nativeRPC-basedinter-processlinktrackingIOC-golang提供的原生RPC能力不需要定义IDL文件,只需要标记//+ioc:autowire:type=rpc供服务提供者生成相关注册码和客户端客户端在启动时调用存根并暴露接口。客户端只需要引入该接口的客户端存根即可发起调用。这种原生的RPC能力是基于json序列化和http传输协议的,方便携带linktrackingid。往前看,IOC-golang开源至今已经超过700颗star,人气的增长超出了我的想象。也希望这个项目能够带来更大的开源价值和生产价值。欢迎越来越多的开发者参与到这个项目中。讨论和建设。参考链接:[1]https://github.com/grpc-ecosystem/go-grpc-middleware[2]https://github.com/alibaba/ioc-golang[3]https://github.com/google/wire[4]https://github.com/ioc-golang/shopping-system
