
作 者 | 李志信(冀锋)
AOP 与 IOC 的关系
AOP (面向切面编程)是一种编程设计思想,旨在通过拦截业务过程的切面,实现特定模块化的能力,降低业务逻辑之间的耦合度。这一思路在众多知名项目中都有实践。例如 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 生态与 AOP
AOP 概念与语言无关,虽然我赞成使用 AOP 的最佳实践方案需要 Java 语言,但我不认为 AOP 是 Java 语言的专属。在我所熟悉的 Go 生态中,依然有较多基于 AOP 思路的优秀项目,这些项目的共性,也如我上一节所阐述的,都是结合特定生态,解决特定业务场景问题,其中解决问题的广度,取决于其 IOC 生态的约束力。IOC 是基石,AOP 是 IOC 生态的衍生物,一个不提供 AOP 的 IOC 生态可以做的很干净很清爽,而一个提供 AOP 能力的 IOC 生态,可以做的很包容很强大。
上个月我开源了 IOC-golang [2]服务框架,专注于解决 Go 应用开发过程中的依赖注入问题。很多开发者把这个框架和 Google 开源的 wire [3]框架做比较,认为没有 wire 清爽好用,这个问题的本质是两个生态的设计初衷不同。wire 注重 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.1 IOC-golang 的接口注入
在本框架开源的第一篇文章中有提到,IOC-golang 在依赖注入的过程具备两个视角,结构提供者和结构使用者。框架接受来自结构提供者定义的结构,并按照结构使用者的要求把结构提供出来。结构提供者只需关注结构本体,无需关注结构实现了哪些接口。而结构使用者需要关心结构的注入和使用方式:是注入至接口?注入至指针?是通过 API 获取?还是通过标签注入获取?
-
通过标签注入依赖对象
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 将实现注入至结构体指针
ServiceStruct *ServiceStruct `singleton:""`
// 将实现注入至接口
ServiceImpl Service `singleton:"main.ServiceImpl1"`
-
通过 API 的方式获取对象
func GetServiceStructSingleton() (*ServiceStruct, error) {
i, err := singleton.GetImpl("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(*ServiceStruct)
return impl, nil
}
func GetServiceStructIOCInterfaceSingleton() (ServiceStructIOCInterface, error) {
i, err := singleton.GetImplWithProxy("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(ServiceStructIOCInterface)
return impl, nil
}
-
IOC-golang 的结构专属接口。
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type ServiceImpl struct {
}
func (s *ServiceImpl) GetHelloString(name string) string {
return fmt.Sprintf("This is ServiceImpl1, hello %s", name)
}
type ServiceImplIOCInterface interface {
GetHelloString(name string) string
}
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 注入 ServiceImpl 结构专属接口,无需在标签中指定结构ID
ServiceOwnInterface ServiceImplIOCInterface `singleton:""`
}
-
代理结构的代码生成与注册
type serviceImpl1_ struct {
GetHelloString_ func(name string) string
}
func (s *serviceImpl1_) GetHelloString(name string) string {
return s.GetHelloString_(name)
}
func init(){
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{} {
return &serviceImpl1_{} // 注册代理结构
},
})
}
-
代理对象的注入
IOC-golang 基于 AOP 的应用
-
查看应用接口和方法
% iocli list
github.com/alibaba/ioc-golang/extension/autowire/rpc/protocol/protocol_impl.IOCProtocol
[Invoke Export]
github.com/ioc-golang/shopping-system/internal/auth.Authenticator
[Check]
github.com/ioc-golang/shopping-system/pkg/service/festival/api.serviceIOCRPCClient
[ListCards ListCachedCards]
-
监听调用参数
iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
curl -i -X GET 'localhost:8080/festival/listCards?user_id=1&num=10'
% iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
========== On Call ==========
github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()
Param 1: (int64) 1
========== On Response ==========
github.com/ioc-golang/shopping-system/internal/auth.Authenticator.Check()
Response 1: (bool) true
4.2 全链路追踪
-
基于 AOP 的进程内链路追踪
-
基于 IOC 原生 RPC 的进程间链路追踪

