作者 | 李志信(冀鋒)
?AOP 與 IOC 的關(guān)系
AOP (面向切面編程)是一種編程設(shè)計思想,旨在通過攔截業(yè)務(wù)過程的切面,實現(xiàn)特定模塊化的能力,降低業(yè)務(wù)邏輯之間的耦合度。這一思路在眾多知名項目中都有實踐。例如 Spring 的切點 PointCut 、 gRPC的攔截器 Interceptor 、Dubbo 的過濾器 Filter。AOP 只是一種概念,這種概念被應(yīng)用在不同的場景下,產(chǎn)生了不同的實現(xiàn)。
我們首先討論比較具體的 RPC 場景,以 gRPC 為例。
圖片摘自 grpc.io
針對一次 RPC 過程,gRPC 提供了可供用戶擴展的 Interceptor 接口,方便開發(fā)者寫入與業(yè)務(wù)相關(guān)的攔截邏輯。例如引入鑒權(quán)、服務(wù)發(fā)現(xiàn)、可觀測等能力,在 gRPC 生態(tài)中存在很多基于 Interceptor 的擴展實現(xiàn),可參考 go-grpc-middleware[1]。這些擴展實現(xiàn)歸屬于 gRPC 生態(tài),限定于 Client 和 Server 兩側(cè)的概念,限定于 RPC 場景。
我們將具象的場景抽象化,參考 Spring 的做法。
Spring 具備強大的依賴注入能力,在此基礎(chǔ)之上,提供了適配與業(yè)務(wù)對象方法的 AOP 能力,可以通過定義切點,將攔截器封裝在業(yè)務(wù)函數(shù)外部。這些 “切面”、“切點” 的概念,都是限定于 Spring 框架內(nèi),由其依賴注入(也就是 IOC)能力所管理。
我想表達的觀點是,AOP 的概念需要結(jié)合具體場景落地,必須受到來自所集成生態(tài)的約束。我認(rèn)為單獨提 AOP 的概念,是不具備開發(fā)友好性和生產(chǎn)意義的,例如我可以按照面向過程編程的思路,寫一連串的函數(shù)調(diào)用,也可以說這是實現(xiàn)了 AOP,但其不具備可擴展性、可遷移性、更不具備通用性。這份約束是必要的,可強可弱,例如 Spring 生態(tài)的 AOP,較弱的約束具備較大的可擴展性,但實現(xiàn)起來相對復(fù)雜,發(fā)者需要學(xué)習(xí)其生態(tài)的眾多概念與 API,再若 Dubbo 、gRPC 生態(tài)的適配于 RPC 場景的 AOP,開發(fā)者只需要實現(xiàn)接口并以單一的 API 注入即可,其能力相對局限。
上述 “約束” 在實際開發(fā)場景可以具象為依賴注入,也就是 IOC。開發(fā)者需要使用的對象由生態(tài)所納管、封裝,無論是 Dubbo 的 Invoker、還是 Spring 的 Bean,IOC 過程為 AOP 的實踐提供了約束借口,提供了模型,提供了落地價值。
Go 生態(tài)與 AOP
AOP 概念與語言無關(guān),雖然我贊成使用 AOP 的最佳實踐方案需要 Java 語言,但我不認(rèn)為 AOP 是 Java 語言的專屬。在我所熟悉的 Go 生態(tài)中,依然有較多基于 AOP 思路的優(yōu)秀項目,這些項目的共性,也如我上一節(jié)所闡述的,都是結(jié)合特定生態(tài),解決特定業(yè)務(wù)場景問題,其中解決問題的廣度,取決于其 IOC 生態(tài)的約束力。IOC 是基石,AOP 是 IOC 生態(tài)的衍生物,一個不提供 AOP 的 IOC 生態(tài)可以做的很干凈很清爽,而一個提供 AOP 能力的 IOC 生態(tài),可以做的很包容很強大。
上個月我開源了 IOC-golang [2]服務(wù)框架,專注于解決 Go 應(yīng)用開發(fā)過程中的依賴注入問題。很多開發(fā)者把這個框架和 Google 開源的 wire [3]框架做比較,認(rèn)為沒有 wire 清爽好用,這個問題的本質(zhì)是兩個生態(tài)的設(shè)計初衷不同。wire 注重 IOC 而非 AOP,因此開發(fā)者可以通過學(xué)習(xí)一些簡單的概念和 API,使用腳手架和代碼生成能力,快速實現(xiàn)依賴注入,開發(fā)體驗很好。IOC-golang 注重基于 IOC 的 AOP 能力,并擁抱這一層的可擴展性,把 AOP 能力看作這一框架和其他 IOC 框架的差異點和價值點。
相比于解決具體問題的 SDK,我們可以把依賴注入框架的 IOC 能力看作“弱約束的IOC場景”,通過兩個框架差異點比較,拋出兩個核心的問題:
Go 生態(tài)在 “弱約束 IOC 的場景” 需不需要 AOP?
GO 生態(tài)在 “弱約束 IOC 的場景” 的 AOP 可以用來做什么?
我的觀點是:Go 生態(tài)一定是需要 AOP 的,即使在“弱約束 IOC 場景”,依然可以使用 AOP 來做一些業(yè)務(wù)無關(guān)的事情,比如增強應(yīng)用的運維可觀測能力。由于語言特性,Go 生態(tài)的 AOP 不能和 Java 劃等號,Go 不支持注解,限制了開發(fā)者使用編寫業(yè)務(wù)語義 AOP 層的便利性,所以我認(rèn)為 Go 的 AOP 并不適合處理業(yè)務(wù)邏輯,即使強行實現(xiàn)出來,也是反直覺的。我更接受把運維可觀測能力賦予 Go 生態(tài)的 AOP 層,而開發(fā)者對于 AOP 是無感知的。
例如,對于任何接口的實現(xiàn)結(jié)構(gòu),都可以使用 IOC-golang 框架封裝運維 AOP 層,從而讓一個應(yīng)用程序的所有對象都具備可觀測能力。除此之外,我們也可以結(jié)合 RPC 場景、服務(wù)治理場景、故障注入場景,產(chǎn)生出更多 “運維” 領(lǐng)域的擴展思路。
IOC-golang 的 AOP 原理
使用 Go 語言實現(xiàn)方法代理的思路有二,分別為通過反射實現(xiàn)接口代理,和基于 Monkey 補丁的函數(shù)指針交換。后者不依賴接口,可以針對任何結(jié)構(gòu)的方法封裝函數(shù)代理,需要侵入底層匯編代碼,關(guān)閉編譯優(yōu)化,對于 CPU 架構(gòu)有要求,并且在處理并發(fā)請求時會顯著削弱性能。
前者的生產(chǎn)意義較大,依賴接口,也是本節(jié)所討論的重點。
3.1 IOC-golang 的接口注入
在本框架開源的第一篇文章中有提到,IOC-golang 在依賴注入的過程具備兩個視角,結(jié)構(gòu)提供者和結(jié)構(gòu)使用者。框架接受來自結(jié)構(gòu)提供者定義的結(jié)構(gòu),并按照結(jié)構(gòu)使用者的要求把結(jié)構(gòu)提供出來。結(jié)構(gòu)提供者只需關(guān)注結(jié)構(gòu)本體,無需關(guān)注結(jié)構(gòu)實現(xiàn)了哪些接口。而結(jié)構(gòu)使用者需要關(guān)心結(jié)構(gòu)的注入和使用方式:是注入至接口?注入至指針?是通過 API 獲?。窟€是通過標(biāo)簽注入獲???
- 通過標(biāo)簽注入依賴對象
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 將實現(xiàn)注入至結(jié)構(gòu)體指針
ServiceStruct *ServiceStruct `singleton:""`
// 將實現(xiàn)注入至接口
ServiceImpl Service `singleton:"main.ServiceImpl1"`
}
App 的 ServiceStruct 字段是具體結(jié)構(gòu)的指針,字段本身已經(jīng)可以定位期望被注入的結(jié)構(gòu),因此不需要在標(biāo)簽中給定期望被注入的結(jié)構(gòu)名。對于這種注入到結(jié)構(gòu)體指針的字段,無法通過注入接口代理的方式提供 AOP 能力,只能通過上文提到的 monkey 補丁方案,這種方式不被推薦。
App 的 ServiceImpl 字段是一個名為 Service 的接口,期望注入的結(jié)構(gòu)指針是 main.ServiceImpl。本質(zhì)上是一個從結(jié)構(gòu)到接口的斷言邏輯,雖然框架可以進行接口實現(xiàn)的校驗,但仍需要結(jié)構(gòu)使用者保證注入的接口實現(xiàn)了該方法。對于這種注入到接口的方式,IOC-golang 框架自動為 main.ServiceImpl 結(jié)構(gòu)創(chuàng)建代理,并將代理結(jié)構(gòu)注入在 ServiceImpl 字段,因此這一接口字段具備了 AOP 能力。
因此,ioc 更建議開發(fā)者面向接口編程,而不是直接依賴具體結(jié)構(gòu),除了 AOP 能力之外,面向接口編程也會提高 go 代碼的可讀性、單元測試能力、模塊解耦合程度等。
- 通過 API 的方式獲取對象
IOC-golang 框架的開發(fā)者可以通過 API 的方式獲取結(jié)構(gòu)指針,通過調(diào)用自動裝載模型(例如singleton)的 GetImpl 方法,可以獲取結(jié)構(gòu)指針。
func GetServiceStructSingleton() (*ServiceStruct, error) {
i, err := singleton.GetImpl("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(*ServiceStruct)
return impl, nil
}
使用 IOC-golang 框架的開發(fā)者更推薦通過API 的方式獲取接口對象,通過調(diào)用自動裝載模型(例如singleton)的 GetImplWithProxy 方法,可以獲取代理結(jié)構(gòu),該結(jié)構(gòu)可被斷言為一個接口供使用。這個接口并非結(jié)構(gòu)提供者手動創(chuàng)建,而是由 iocli 自動生成的“結(jié)構(gòu)專屬接口”,在下文中將予以解釋。
func GetServiceStructIOCInterfaceSingleton() (ServiceStructIOCInterface, error) {
i, err := singleton.GetImplWithProxy("main.ServiceStruct", nil)
if err != nil {
return nil, err
}
impl := i.(ServiceStructIOCInterface)
return impl, nil
}
這兩種通過 API 獲取對象的方式可以由 iocli 工具自動生成,注意,這些代碼的作用都是方便開發(fā)者調(diào)用 API ,減少代碼量,而 ioc 自動裝載的邏輯內(nèi)核并不是由工具生成的,這是與 wire 提供的依賴注入實現(xiàn)思路的不同點之一,也是很多開發(fā)者誤解的一點。
- IOC-golang 的結(jié)構(gòu)專屬接口。
通過上面的介紹,我們知道 IOC-golang 框架推薦的 AOP 注入方式是強依賴接口的。但要求開發(fā)者為自己的全部結(jié)構(gòu),都手寫一個與之匹配的接口出來,這會耗費大量的時間。因此 iocli 工具可以自動生成結(jié)構(gòu)專屬接口,減輕開發(fā)人員的代碼編寫量。
例如一個名為 ServiceImpl 的結(jié)構(gòu),其包含 GetHelloString 方法。
// +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)
}
當(dāng)執(zhí)行 iocli gen 命令后, 會在當(dāng)前目錄生成一份代碼zz_generated.ioc.go 其中包含該結(jié)構(gòu)的“專屬接口”:
type ServiceImplIOCInterface interface {
GetHelloString(name string) string
}
專屬接口的命名為 $(結(jié)構(gòu)名)IOCInterface,專屬接口包含了結(jié)構(gòu)的全部方法。專屬接口的作用有二:
- 減輕開發(fā)者工作量,方便直接通過 API 的方式 Get 到代理結(jié)構(gòu),方便直接作為字段注入。
- 結(jié)構(gòu)專屬接口可以直接定位結(jié)構(gòu) ID,因此在注入專屬接口的時候,標(biāo)簽無需顯式指定結(jié)構(gòu)類型:
// +ioc:autowire=true
// +ioc:autowire:type=singleton
type App struct {
// 注入 ServiceImpl 結(jié)構(gòu)專屬接口,無需在標(biāo)簽中指定結(jié)構(gòu)ID
ServiceOwnInterface ServiceImplIOCInterface `singleton:""`
}
因此,隨便找一個現(xiàn)有的 go 工程,其中使用結(jié)構(gòu)指針的位置,我們推薦替換成結(jié)構(gòu)專屬接口,框架默認(rèn)注入代理;對于其中已經(jīng)使用了接口的字段,我們推薦直接通過標(biāo)簽注入結(jié)構(gòu),也是由框架默認(rèn)注入代理。按照這種模式開發(fā)的工程,其全部對象都將具備運維能力。
3.2 代理的生成與注入
上一小節(jié)所提到的“注入至接口”的對象,都被被框架默認(rèn)封裝了代理,具備運維能力,并提到了 iocli 會為所有結(jié)構(gòu)產(chǎn)生“專屬接口”。在本節(jié)中,將解釋框架如何封裝代理層,如何注入至接口的。
- 代理結(jié)構(gòu)的代碼生成與注冊
在前文提到生成的 zz.generated.ioc.go 代碼中包含結(jié)構(gòu)專屬接口,同樣,其中也包含結(jié)構(gòu)代理的定義。還是以上文中提到的 ServiceImpl 結(jié)構(gòu)為例,它生成的代理結(jié)構(gòu)如下:
type serviceImpl1_ struct {
GetHelloString_ func(name string) string
}
func (s *serviceImpl1_) GetHelloString(name string) string {
return s.GetHelloString_(name)
}
代理結(jié)構(gòu)命名為小寫字母開頭的 $(結(jié)構(gòu)名)_,其實現(xiàn)了“結(jié)構(gòu)專屬接口” 的全部方法,并將所有方法調(diào)用代理至 $(方法名)_ 的方法字段,該方法字段會被框架以反射的方式實現(xiàn)。
與結(jié)構(gòu)代碼一樣,代理結(jié)構(gòu)也會在這個生成的文件中注冊到框架:
func init(){
normal.RegisterStructDescriptor(&autowire.StructDescriptor{
Factory: func() interface{} {
return &serviceImpl1_{} // 注冊代理結(jié)構(gòu)
},
})
}
- 代理對象的注入
上述內(nèi)容描述了代理結(jié)構(gòu)的定義和注冊過程。當(dāng)用戶期望獲取封裝了AOP層的代理對象,將首先加載真實對象,然后嘗試加載代理對象,最終通過反射實例化代理對象,注入接口,從而賦予接口運維能力。該過程可由下圖展示:
IOC-golang 基于 AOP 的應(yīng)用
理解了上文中提到的實現(xiàn)思路,我們可以認(rèn)為,使用 IOC-golang 框架開發(fā)的應(yīng)用程序中,從框架注入、獲取的所有接口對象都是具備運維能力的。我們可以基于 AOP 的思路,擴展出我們期望的能力。我們提供了一個簡易的電商系統(tǒng) demo shopping-system[4],展示了在分布式場景下 IOC-golang 基于 AOP 的可視化能力。感興趣的開發(fā)者可以參考 README,在自己的集群里運行這個系統(tǒng),感受其運維能力底座。
4.1 方法、參數(shù)可觀測
- 查看應(yīng)用接口和方法
% 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]
- 監(jiān)聽調(diào)用參數(shù)
通過 iocli watch命令, 我們可以監(jiān)聽鑒權(quán)接口的 Check 方法的調(diào)用:
iocli watch github.com/ioc-golang/shopping-system/internal/auth.Authenticator Check
- 發(fā)起針對入口的調(diào)用
curl -i -X GET 'localhost:8080/festival/listCards?user_id=1&num=10'
可查看到被監(jiān)聽方法的調(diào)用參數(shù)和返回值,user id 為1。
% 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 全鏈路追蹤
基于 IOC-golang 的 AOP 層,可以提供用戶無感知、業(yè)務(wù)無侵入的分布式場景下全鏈路追蹤能力。即一個由本框架開發(fā)的系統(tǒng),可以以任何一個接口方法為入口,采集到方法粒度的跨進程調(diào)用全鏈路。
基于 shopping-system 的全鏈路耗時信息,可以排查到名為 festival 進程的 gorm.First() 方法是系統(tǒng)的瓶頸。
這個能力的實現(xiàn)包括兩部分,分別是進程內(nèi)的方法粒度鏈路追蹤,和進程之間的 RPC 調(diào)用鏈路追蹤。IOC 旨在打造開發(fā)者開箱即用的應(yīng)用開發(fā)生態(tài)組件,這些內(nèi)置的組件與框架提供的 RPC 能力都具備了運維能力。
- 基于 AOP 的進程內(nèi)鏈路追蹤
IOC-golang 提供的鏈路追蹤能力的進程內(nèi)實現(xiàn),是基于 AOP 層做的,為了做到業(yè)務(wù)無感知,我們并沒有通過 context 上下文的方式去標(biāo)識調(diào)用鏈路,而是通過 go routine id 進行標(biāo)識。通過 go runtime 調(diào)用棧,來記錄當(dāng)前調(diào)用相對入口函數(shù)的深度。
- 基于 IOC 原生 RPC 的進程間鏈路追蹤
IOC-golang 提供的原生 RPC 能力,無需定義 IDL文件,只需要為服務(wù)提供者標(biāo)注 // +ioc:autowire:type=rpc ,即可生成相關(guān)注冊代碼和客戶端調(diào)用存根,啟動時暴露接口??蛻舳酥恍枰脒@一接口的客戶端存根,即可發(fā)起調(diào)用。這一原生 RPC 能力基于 json 序列化和 http 傳輸協(xié)議,方便承載鏈路追蹤 id。
展望
IOC-golang 開源至今已經(jīng)突破 700 star,其熱度的增長超出了我的想象,也希望這個項目能帶來更大的開源價值與生產(chǎn)價值,歡迎越來越多的開發(fā)者參與到這個項目的討論和建設(shè)中。
參考鏈接:
[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