hello~大家好,我是小樓,今天分享的話題是Go是否能實(shí)現(xiàn)AOP?
成都創(chuàng)新互聯(lián)公司主要從事網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作、網(wǎng)頁設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)金門,十年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):13518219792
寫Java的同學(xué)來寫Go就特別喜歡將兩者進(jìn)行對(duì)比,就經(jīng)??吹郊夹g(shù)群里討論,比如Go能不能實(shí)現(xiàn)Java那樣的AOP???Go寫個(gè)事務(wù)好麻煩啊,有沒有Spring那樣的@Transactional注解???
遇到這樣的問題我通常會(huì)回復(fù):沒有、實(shí)現(xiàn)不了、再見。
直到看了《Go語言底層原理剖析》這本書,開始了一輪認(rèn)真地探索。
AOP概念第一次是在若干年前學(xué)Java時(shí)看的一本書《Spring實(shí)戰(zhàn)》中看到的,它指的是一種面向切面編程的思想。注意它只是一種思想,具體怎么實(shí)現(xiàn),你看著辦。
AOP能在你代碼的前后織入代碼,這就能做很多有意思的事情了,比如統(tǒng)一的日志打印、監(jiān)控埋點(diǎn),事務(wù)的開關(guān),緩存等等。
可以分享一個(gè)我當(dāng)年學(xué)習(xí)AOP時(shí)的筆記片段:
在Java中的實(shí)現(xiàn)方式可以是JDK動(dòng)態(tài)代理
和字節(jié)碼增強(qiáng)技術(shù)
。
JDK動(dòng)態(tài)代理是在運(yùn)行時(shí)動(dòng)態(tài)地生成了一個(gè)代理類,JVM通過加載這個(gè)代理類再實(shí)例化來實(shí)現(xiàn)AOP的能力。
字節(jié)碼增強(qiáng)技術(shù)可以多嘮叨兩句,當(dāng)年學(xué)Java時(shí)第一章就說Java的特點(diǎn)是「一次編譯,到處運(yùn)行」。
但當(dāng)我們真正在工作中這個(gè)特性用處大嗎?好像并不大,生產(chǎn)中都使用了同一種服務(wù)器,只編譯了一次,也都只在這個(gè)系統(tǒng)運(yùn)行。做到一次編譯,到處運(yùn)行的技術(shù)底座是JVM,JVM可以加載字節(jié)碼并運(yùn)行,這個(gè)字節(jié)碼是平臺(tái)無關(guān)的一種二進(jìn)制中間碼。
似乎這個(gè)設(shè)定帶來了一些其他的好處。在JVM加載字節(jié)碼時(shí),字節(jié)碼有一次被修改的機(jī)會(huì),但這個(gè)字節(jié)碼的修改比較復(fù)雜,好在有現(xiàn)成的庫可用,如ASM、Javassist等。
至于像ASM這樣的庫是如何修改字節(jié)碼的,我還真就去問了Alibaba Dragonwell的一位朋友,他回答ASM是基于Java字節(jié)碼規(guī)范所做的「硬改」,但做了一些抽象,總體來說還是比較枯燥的。
由于這不是本文重點(diǎn),所以只是提一下,如果想更詳細(xì)地了解可自行網(wǎng)上搜索。
之前用「扁鵲三連」的方式回復(fù)Go不能實(shí)現(xiàn)AOP的基礎(chǔ)其實(shí)就是我對(duì)Java實(shí)現(xiàn)AOP的思考,因?yàn)镚o沒有虛擬機(jī)一說,也沒有中間碼,直接源碼編譯為可執(zhí)行文件,可執(zhí)行文件基本沒法修改,所以做不了。
但真就如此嗎?我搜索了一番。
還真就在Github找到了一個(gè)能實(shí)現(xiàn)類似AOP功能的庫gohook
(當(dāng)然也有類似的其他庫):
https://github.com/brahma-adshonor/gohook
看這個(gè)項(xiàng)目的介紹:
運(yùn)行時(shí)動(dòng)態(tài)地hook Go的方法,也就是可以在方法前插入一些邏輯。它是怎么做到的?
通過反射找到方法的地址(指針),然后插入一段代碼,執(zhí)行完后再執(zhí)行原方法。聽起來很牛X,但它下面有個(gè)Notes:
使用有一些限制,更重要的是沒有完全測(cè)試,不建議生產(chǎn)使用。這種不可靠的方式也就不嘗試了。
這種方式就是我在看《Go語言底層原理剖析》第一章看到的,其實(shí)我之前的文章也有寫過關(guān)于AST的,《Cobar源碼分析之AST》。
AST即抽象語法樹,可以認(rèn)為所有的高級(jí)編程語言都可以抽象為一種語法樹,即對(duì)代碼進(jìn)行結(jié)構(gòu)化的抽象,這種抽象可以讓我們更加簡(jiǎn)單地分析甚至操作源碼。
Go在編譯時(shí)大概分為詞法與語法分析、類型檢查、通用 SSA 生成和最后的機(jī)器代碼生成這幾個(gè)階段。
其中詞法與語法分析之后,生成一個(gè)AST樹,在Go中我們能調(diào)用Go提供的API很輕易地生成AST:
fset := token.NewFileSet()
// 這里file就是一個(gè)AST對(duì)象
file, err := parser.ParseFile(fset, "aop.go", nil, parser.ParseComments)
比如這里我的aop.go文件是這樣的:
package main
import "fmt"
func main() {
fmt.Println(execute("roshi"))
}
func execute(name string) string {
return name
}
想看生成的AST長(zhǎng)什么樣,可調(diào)用下面的方法:
ast.Print(fset, file)
由于篇幅太長(zhǎng),我截個(gè)圖感受下即可:
當(dāng)然也有一些開源的可視化工具,但我覺得大可不必,想看的話Debug看下file
的結(jié)構(gòu)。
至于Go AST結(jié)構(gòu)的介紹,也不是本文的重點(diǎn),而且AST中的類型很多很多,我建議如果你想看的話直接Debug來看,對(duì)照源碼比較清晰。
我們這里就實(shí)現(xiàn)一個(gè)簡(jiǎn)單的,在execute方法執(zhí)行之前添加一條打印before
的語句,接上述代碼:
const before = "fmt.Println(\"before\")"
...
exprInsert, err := parser.ParseExpr(before)
if err != nil {
panic(err)
}
decls := make([]ast.Decl, 0, len(file.Decls))
for _, decl := range file.Decls {
fd, ok := decl.(*ast.FuncDecl)
if ok {
if fd.Name.Name == "execute" {
stats := make([]ast.Stmt, 0, len(fd.Body.List)+1)
stats = append(stats, &ast.ExprStmt{
X: exprInsert,
})
stats = append(stats, fd.Body.List...)
fd.Body.List = stats
decls = append(decls, fd)
continue
} else {
decls = append(decls, decl)
}
} else {
decls = append(decls, decl)
}
}
file.Decls = decls
這里AST就被我們修改了,雖然我們是寫死了針對(duì)execute方法,但總歸是邁出了第一步。
再把AST轉(zhuǎn)換為源碼輸出,Go也提供了API:
var cfg printer.Config
var buf bytes.Buffer
cfg.Fprint(&buf, fset, file)
fmt.Printf(buf.String())
輸出效果如下:
看到這里,我猜你應(yīng)該有和我相同的想法,這玩意是不是可以用來格式化代碼?
沒錯(cuò),Go自帶的格式化代碼工具gofmt的原理就是如此。
當(dāng)我們寫完代碼時(shí),可以執(zhí)行g(shù)ofmt對(duì)代碼進(jìn)行格式化:
gofmt test.go
這相比于其他語言方便很多,終于有個(gè)官方的代碼格式了,甚至你可以在IDEA中安裝一個(gè)file watchers插件,監(jiān)聽文件變更,當(dāng)文件有變化時(shí)自動(dòng)執(zhí)行 gofmt 來格式化代碼。
看到這里你可能覺得太簡(jiǎn)單了,我查了下資料,AST中還能拿到注釋,這就厲害了,我們可以把注釋當(dāng)注解來玩,比如我加了 // before:
的注釋,自動(dòng)把這個(gè)注釋后的代碼添加到方法之前去。
// before:fmt.Println("before...")
func executeComment(name string) string {
return name
}
修改AST代碼如下,為了篇幅,省略了打印代碼:
cmap := ast.NewCommentMap(fset, file, file.Comments)
for _, decl := range file.Decls {
fd, ok := decl.(*ast.FuncDecl)
if ok {
if cs, ok := cmap[fd]; ok {
for _, cg := range cs {
for _, c := range cg.List {
if strings.HasPrefix(c.Text, "http:// before:") {
txt := strings.TrimPrefix(c.Text, "http:// before:")
ei, err := parser.ParseExpr(txt)
if err == nil {
stats := make([]ast.Stmt, 0, len(fd.Body.List)+1)
stats = append(stats, &ast.ExprStmt{
X: ei,
})
stats = append(stats, fd.Body.List...)
fd.Body.List = stats
decls = append(decls, fd)
continue
}
}
}
}
} else {
decls = append(decls, decl)
}
} else {
decls = append(decls, decl)
}
}
file.Decls = decls
跑一下看看:
雖然又是硬編碼,但這不重要,又不是不能用~
但你發(fā)現(xiàn),這樣實(shí)現(xiàn)AOP有個(gè)缺點(diǎn),必須在編譯期對(duì)代碼進(jìn)行一次重新生成,理論上來說,所有高級(jí)編程語言都可以這么操作。
但這不是說毫無用處,比如這篇文章《每個(gè) gopher 都需要了解的 Go AST》就給了我們一個(gè)實(shí)際的案例:
寫到最后,我又在思考另一個(gè)問題,為什么Go的使用者沒有AOP的需求呢?反倒是寫Java的同學(xué)會(huì)想到AOP。
我覺得可能還是Go太年輕了,Java之所以要用AOP,很大的原因是代碼已經(jīng)堆積如山,沒法修改,歷史包袱沉重,最小代價(jià)實(shí)現(xiàn)需求是首選,所以會(huì)選擇AOP這種技術(shù)。
反觀Go還年輕,大多數(shù)項(xiàng)目屬于造輪子期間,需要AOP的地方早就在代碼中提前埋伏好了。我相信隨著發(fā)展,一定也會(huì)出現(xiàn)一個(gè)生產(chǎn)可用Go AOP框架。
至于現(xiàn)在問我,Go能否實(shí)現(xiàn)AOP,我還是回答:沒有、實(shí)現(xiàn)不了、再見。
對(duì)了,本文的完整測(cè)試代碼這里可以看到:
https://github.com/lkxiaolou/all-in-one/tree/master/go-in-one/samples/tree
感謝大家,如果有點(diǎn)收獲,點(diǎn)個(gè)在看
、贊
、關(guān)注
吧,我們下期再見。
搜索關(guān)注微信公眾號(hào)"捉蟲大師",后端技術(shù)分享,架構(gòu)設(shè)計(jì)、性能優(yōu)化、源碼閱讀、問題排查、踩坑實(shí)踐。
網(wǎng)站題目:Go能實(shí)現(xiàn)AOP嗎?
轉(zhuǎn)載來于:http://chinadenli.net/article24/dsoisce.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、品牌網(wǎng)站建設(shè)、微信小程序、服務(wù)器托管、手機(jī)網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)