Python 是一門(mén)極簡(jiǎn)的語(yǔ)言荆忍,語(yǔ)言簡(jiǎn)潔學(xué)習(xí)起來(lái)也是相當(dāng)輕松的,但是依然有一些高級(jí)技巧撤缴,例如裝飾器刹枉、協(xié)程、并發(fā)腹泌,會(huì)讓人感覺(jué)困惑嘶卧、失望與沮喪。
本文將重點(diǎn)講解 Python 裝飾器的使用凉袱,使用常見(jiàn)的例子讓我們更直觀(guān)的看到裝飾器的強(qiáng)大表達(dá)能力芥吟,最后給出了編寫(xiě)裝飾器常見(jiàn)的工具。
熟悉 Java 的同學(xué)一定熟悉注解的使用专甩,借助于注解可以定義元數(shù)據(jù)配置钟鸵,我們常常會(huì)有這種感受,”只要加上這個(gè)注解涤躲,我的組件就會(huì)被注冊(cè)進(jìn)去”棺耍、”只要加上這個(gè)注解,就會(huì)添加事務(wù)控制”种樱,也會(huì)困惑”為什么加了這個(gè)注解依然沒(méi)有生效?”蒙袍。 Python 沒(méi)有提供像 Java 似的注解,但是提供了相比注解表達(dá)能力更加強(qiáng)大的裝飾器嫩挤。
例如 Web 框架 Flask 中的 route 害幅、errorhandler 和 Python 自帶的 property、staticmethod等岂昭。 實(shí)際上 Java 注解能實(shí)現(xiàn)的功能以现,Python 的裝飾器絕大部分都是可以勝任的,裝飾器更像 Java 中注解加上 Aop 兩者的組合约啊, 這個(gè)結(jié)論最后我們會(huì)重點(diǎn)討論邑遏,先按下不表。
首先以日志打印的簡(jiǎn)單例子初步講解一下裝飾器的使用恰矩。
1. 裝飾器的簡(jiǎn)單例子
從輸出的結(jié)果可以看到裝飾器的裝飾邏輯已正確被執(zhí)行外傅。
didiyun execute test_dec param:1, 3, yuhaiqiang
didiyun execute done:test_dec
- @1. 定義 log 裝飾器孽鸡,輸入?yún)?shù) func 是需要被裝飾的函數(shù)蹂午,本例中輸出打印是 test_dec ;@2. 定義一個(gè)裝飾函數(shù)彬碱,參數(shù)類(lèi)型包括變長(zhǎng)的位置參數(shù)和名字參數(shù),適應(yīng)被裝飾函數(shù)不同的參數(shù)組合奥洼,這種寫(xiě)法可以代表任意參數(shù)組合 巷疼;@3. 執(zhí)行實(shí)際的函數(shù) func_dec, 注意處理返回值灵奖,不要”吞掉”被裝飾函數(shù)的返回值 嚼沿;@4. 在被裝飾函數(shù)上添加裝飾器,注意此處不要加 ()瓷患,后面會(huì)解釋具體原因骡尽,了解該原因,就能完全了解裝飾器的小九九 擅编。
裝飾器可以在函數(shù)外層添加額外的功能裝飾原函數(shù)攀细, 本例的裝飾器只是在函數(shù)外層打印一行日志,實(shí)現(xiàn)的是非常簡(jiǎn)單的功能爱态,實(shí)際中裝飾器并不是”僅僅打印日志的雕蟲(chóng)小技”谭贪,還能實(shí)現(xiàn)其他更有用的功能。
2. 使用裝飾器巧用文件鎖
2.1 使用 fcntl 實(shí)現(xiàn)文件鎖
借助于 fcntl 庫(kù) 我們已經(jīng)實(shí)現(xiàn)文件鎖,感興趣的讀者可以深入了解一下 fcntl 庫(kù)洞渔,下面我們以文件鎖為例套媚,介紹一下裝飾器很實(shí)用很常見(jiàn)的一些功能。
2.2 定義文件鎖裝飾器
- @1. 定義 file_lock 裝飾器,接受兩個(gè)參數(shù)衷快, lock_name:鎖路徑宙橱、block: 是否阻塞式的獲取蘸拔;
- @2. 該處在獲取鎖失敗時(shí)师郑,僅僅返回了 None, 調(diào)用方無(wú)法 明確知道 None 是 get_length 的返回值還是獲取鎖失敗调窍,實(shí)際上應(yīng)該拋出一個(gè)異常宝冕,交給上游調(diào)用方去處理 獲取鎖操作失敗的異常;
- @3. try finally 保證鎖一定可以被釋放邓萨;
- @4. 在使用文件鎖的時(shí)候地梨,還需要提供鎖的值菊卷,能否提供默認(rèn)值呢?只對(duì)該實(shí)例方法加鎖,讀者可以考慮一下如何實(shí)現(xiàn)宝剖。此外細(xì)心地讀者能比較出來(lái)洁闰,file_lock 裝飾器,添加了括號(hào) ()万细,但是兩者的區(qū)別可不僅僅因?yàn)橐粋€(gè)有參數(shù)扑眉,一個(gè)無(wú)參數(shù),后一節(jié)解釋下裝飾器的語(yǔ)法糖本質(zhì)赖钞;
- @5. 實(shí)際在項(xiàng)目中使用時(shí)腰素,經(jīng)常會(huì)遇到文件鎖的問(wèn)題, 在項(xiàng)目調(diào)試階段雪营, 由于經(jīng)常需要手動(dòng)終止強(qiáng)殺程序弓千, 會(huì)導(dǎo)致文件鎖沒(méi)有被正確清理,讀者可以考慮將文件鎖指定在一個(gè)固定的目錄献起, 每次進(jìn)程啟動(dòng)時(shí)洋访,檢測(cè)是否有同路徑進(jìn)程,如果沒(méi)有征唬, 可以清理該目錄捌显,如果存在同路徑進(jìn)程,說(shuō)明現(xiàn)在有并發(fā)執(zhí)行总寒,不清理該目錄扶歪。 如果沒(méi)有清理功能可能會(huì)導(dǎo)致永遠(yuǎn)無(wú)法獲取到鎖。 如果不實(shí)際使用以上代碼實(shí)現(xiàn)文件鎖摄闸,可以忽略該問(wèn)題善镰,不影響理解裝飾器。 感興趣的讀者可以試試年枕,希望能提出更好的文件鎖方案
使用文件鎖之后炫欺,調(diào)用該方法必須先獲取到鎖,否則只能先阻塞熏兄。 因此實(shí)際的處理方法不需要處理同步邏輯品洛, 只需要一行裝飾器, 就額外擴(kuò)展了同步功能摩桶, 通過(guò)異城抛矗控制,還能保證文件鎖一定可以被釋放硝清, 避免文件鎖泄露辅斟, 通過(guò)裝飾器我們還可以實(shí)現(xiàn)很多其他有用的功能,但是文件鎖裝飾器的實(shí)現(xiàn)已經(jīng)相比日志裝飾器復(fù)雜了芦拿, 仔細(xì)觀(guān)察士飒, 它已經(jīng)嵌套了三層函數(shù)查邢,后續(xù)我們會(huì)優(yōu)化這個(gè)問(wèn)題。
2.3 解釋 1.0 的疑問(wèn)酵幕,何時(shí)使用裝飾器需要添加括號(hào)()
在 1.0 日志裝飾器的例子中我們留下了一個(gè)疑問(wèn)扰藕,為什么 log 裝飾器不需要加 () ,而文件鎖裝飾器的使用卻加了 ()裙盾?
回到 1.0 的裝飾器實(shí)現(xiàn)实胸,假如我們不使用 @ log 的方式,使用如下方式能不能實(shí)現(xiàn)相同的邏輯呢?
log 方法接受的參數(shù)是 func 钢属, 自然當(dāng)手動(dòng)顯式調(diào)用 log 裝飾 foo 函數(shù)時(shí)徘熔, 絲毫不影響實(shí)現(xiàn)裝飾的功能. 但是顯得很啰嗦, 幸福的是 Python 提供了裝飾器的語(yǔ)法糖淆党, 使用該語(yǔ)法糖就好像我們手動(dòng)執(zhí)行裝飾一樣酷师。
但是如果我們加上括號(hào)代表什么意思呢?
@log() 的寫(xiě)法, 不就相當(dāng)于調(diào)用了 log 函數(shù)染乌,但是又不給其傳參山孔。實(shí)際 Python 解釋器也是這么”抗議” 我們的。
但是為什么文件鎖又加上了括號(hào)呢?
答案是荷憋,裝飾器有時(shí)候需要一些額外的參數(shù)台颠,例如 Flask 中我們常用的 route,我們需要告訴 Flask勒庄, 如何將 url 映射到具體的 handler串前, 自然需要告訴 route, 需要綁定的 url 是什么实蔽, 和 spring 的 @RequestMapping 作用類(lèi)似荡碾。
當(dāng)裝飾器加上參數(shù)之后, 驚訝的發(fā)現(xiàn)裝飾器更像是三層函數(shù)了局装, 可理解性極差坛吁, 但是一旦理解之后我們會(huì)發(fā)現(xiàn)三層函數(shù)是原因的。
不妨這樣理解铐尚, 當(dāng)裝飾器沒(méi)有參數(shù)拨脉, 就像 log 裝飾器,該裝飾器接受參數(shù)為 func塑径, 我們稱(chēng)其為”兩層”裝飾器 女坑,以上我們已經(jīng)分析了它的的原理。
foo = log(foo) 裝飾器的 @ 標(biāo)記等于告訴 Python 解釋器:”你把 @ 下一行的函數(shù)作為參數(shù)傳給該裝飾器统舀,然后把返回值賦值給該函數(shù)”匆骗, 相當(dāng)于執(zhí)行 foo = log(foo)劳景。 當(dāng)我們調(diào)用 foo() 相當(dāng)于是調(diào)用 log(foo)()。
對(duì)于帶參數(shù)的裝飾器 file_lock(name碉就,block)盟广, 我們分成兩個(gè)階段理解,回顧一下 file_lock 的三層函數(shù)實(shí)現(xiàn)瓮钥,我們?cè)诘诙佣x了一個(gè) wrapper 函數(shù)筋量,該函數(shù)接受了一個(gè) func 參數(shù),隨后我們?cè)?file_lock 的最后將其 return碉熄, 我們可以這樣認(rèn)為:
第一階段執(zhí)行了最外層的函數(shù) file_lock锈津, 返回了 wrapper呀酸。
第二階段,使用 wrapper 裝飾 test琼梆, 第二階段我們已經(jīng)熟悉理解了性誉。
實(shí)際上, 只有第一階段是多執(zhí)行的茎杂。 由于我們多給它加了一個(gè)括號(hào)错览, Python 解釋器自然會(huì)去執(zhí)行該函數(shù), 該函數(shù)返回另一個(gè)裝飾函數(shù)煌往, 這樣就到了第二階段倾哺。
Python 解釋器希望我們這樣去理解,否則三層函數(shù)的寫(xiě)法很讓人崩潰携冤。后續(xù)我們繼續(xù)探索裝飾器能不能實(shí)現(xiàn)相同的功能悼粮,但是擺脫編寫(xiě)三層函數(shù)的噩夢(mèng)。
以上分析了帶參數(shù)和不帶參數(shù)的裝飾器的區(qū)別曾棕,及如何在心里去理解與接受這種寫(xiě)法扣猫, Python 通過(guò)語(yǔ)法糖, 函數(shù)之上的裝飾器定義 代替蠢笨的手動(dòng)裝飾調(diào)用翘地。
我們可以實(shí)現(xiàn)讓復(fù)雜的裝飾器申尤,提供極其優(yōu)雅的使用方式給調(diào)用方還是讓人鼓舞的,事實(shí)上衙耕, Python 的框架中大量的使用了裝飾器昧穿,也說(shuō)明了裝飾器的強(qiáng)大與優(yōu)雅。
3. Python 裝飾器方法的執(zhí)行時(shí)機(jī)與順序
Python 是解釋執(zhí)行的語(yǔ)言橙喘。做一個(gè)小實(shí)驗(yàn)时鸵,以上例子是先定義 log 裝飾器,而后再使用 log 裝飾器,讓我們換一下順序:
毫無(wú)疑問(wèn),這樣會(huì)報(bào)語(yǔ)法錯(cuò)誤彭雾, Python 是從 Python 文件從上到下執(zhí)行解釋?zhuān)?只有已經(jīng)定義了 log碟刺, 才能使用它。
在 Python 中薯酝,函數(shù)是一等公民也是對(duì)象半沽,定義函數(shù)也是在聲明一個(gè)函數(shù)對(duì)象:
def foo():
pass
def foo() 就是在聲明一個(gè)函數(shù)對(duì)象,foo 即是該函數(shù)對(duì)象的引用吴菠, 我們可以額外定義該對(duì)象的屬性者填, 通過(guò) dir(foo) 查看一下該函數(shù)對(duì)象有哪些屬性,其實(shí)是和類(lèi)實(shí)例對(duì)象沒(méi)有區(qū)別的做葵。
以上提過(guò) test 被 log裝飾 后幔托, test() 等同于 log(test)(), Python 裝飾器解釋執(zhí)行完 @log def test()蜂挪,等同于 test=log(test) 此時(shí) test 引用的函數(shù)對(duì)象是 log 裝飾后的函數(shù)對(duì)象。
3.1 裝飾器的執(zhí)行順序
實(shí)際開(kāi)發(fā)中我們經(jīng)常會(huì)使用多個(gè)裝飾器,如果讀者理解了 3.0 及以上的函數(shù)對(duì)象的概念刺覆,其實(shí)能猜出來(lái)裝飾器的裝飾順序是從上往下執(zhí)行的:
例如:foo = a(b(c(foo))) 實(shí)際的代碼執(zhí)行順序是 c->b->a 严肪。
以上我們用日志裝飾器和文件鎖裝飾器介紹了裝飾器的使用驳糯,并且討論了帶參數(shù)及不帶參數(shù)裝飾器的區(qū)別。
其中”三層函數(shù)”的定義方式可讀性非常差氢橙,在下一節(jié)將重點(diǎn)討論如何使用類(lèi)實(shí)現(xiàn)裝飾器簡(jiǎn)化三層裝飾器的邏輯酝枢,減少相似代碼的編寫(xiě)。
4. 裝飾器類(lèi)的設(shè)計(jì)
在本節(jié)中悍手,我們重點(diǎn)優(yōu)化三層裝飾器的編寫(xiě)帘睦,除此之外,筆者在實(shí)際開(kāi)發(fā)中還發(fā)現(xiàn)了其他常見(jiàn)的需求坦康。例如:
1. 暫存裝飾器的參數(shù)竣付。 期望通過(guò)被裝飾函數(shù)找到裝飾器參數(shù), 筆者在自動(dòng)化測(cè)試中就使用裝飾器定義測(cè)試 case滞欠, 需要在 case 中配置元數(shù)據(jù)信息古胆,在實(shí)際的執(zhí)行引擎部分,訪(fǎng)問(wèn)該元數(shù)據(jù)信息筛璧,就是將元數(shù)據(jù)信息放到函數(shù)對(duì)象中逸绎;
2. 注冊(cè)被裝飾函數(shù)對(duì)象惹恃。 例如某些 web 框架,注冊(cè) handler桶良, 需要在裝飾器中實(shí)現(xiàn)某些注冊(cè)邏輯座舍。
從以上內(nèi)容出發(fā),可以看到裝飾器的邏輯有某些通用的部分陨帆,然而以上裝飾器的例子都是通過(guò)函數(shù)實(shí)現(xiàn)的曲秉, 但函數(shù)在內(nèi)部狀態(tài)、繼承等方面明顯不如類(lèi)疲牵,所以我們嘗試使用類(lèi)實(shí)現(xiàn)裝飾器承二,并嘗試實(shí)現(xiàn)一個(gè)通用的裝飾基類(lèi)。
4.1 思路
Python 提供了很多奇異方法纲爸,所謂的奇異方法是指亥鸠,只要你實(shí)現(xiàn)了這個(gè)方法,就可以使用 Python 的某些工具方法识啦,例如實(shí)現(xiàn)了 __ len__ 方法负蚊,可以使用 len() 來(lái)獲取長(zhǎng)度,實(shí)現(xiàn)了 __ iter__ 可以使用 iter 方法返回一個(gè)迭代器颓哮,其他方法還有 “eq”家妆、”ne”、 “next” 等冕茅。 其中當(dāng)實(shí)現(xiàn) __ call__ 方法時(shí)伤极, 類(lèi)可以被當(dāng)做一個(gè)函數(shù)使用,例如以下示例:
是否也可以使用 Python 的這個(gè)特性實(shí)現(xiàn)裝飾器呢?
答案是可以的,讓我們來(lái)實(shí)現(xiàn)一個(gè)裝飾基類(lèi)乍楚, 解決以上的痛點(diǎn):
BaseDecorator 實(shí)現(xiàn)的并不是具體的某個(gè)裝飾器邏輯炊豪,它可以作為裝飾器類(lèi)的基類(lèi)凌箕,之前我們?cè)治鼍帉?xiě)裝飾器通用的需求已經(jīng)痛點(diǎn)铁材。先具體講解這個(gè)類(lèi)的實(shí)現(xiàn)古瓤,而后在討論如何使用振定。
- @1__call__ 函數(shù)簽名花椭,*_ 代表忽略變長(zhǎng)的位置參數(shù)厂财,只接受命名參數(shù)蹂季。實(shí)際的裝飾器中齐帚,一般都是使用命名參數(shù)庐冯,代碼可讀性高;
- @2__call__ 本身的實(shí)現(xiàn)邏輯委托給了 do_call 方法慧妄,主要是考慮BaseDecorator 作為裝飾基類(lèi)顷牌,需要提供某些工具方法及可擴(kuò)展方法,但是__ call__ 方法本身無(wú)法被繼承塞淹,所以我們退而求次窟蓝,將工具方法封裝在自定義方法中,子類(lèi)還是需要重新實(shí)現(xiàn)__ call__饱普, 并調(diào)用 do_call 方法运挫, do_call 方法的簽名和__ call__ 相同;
- @3 functools 提供了 wraps 裝飾器套耕,之前我們分析過(guò) Python 是使用裝飾后的函數(shù)對(duì)象替換之前的函數(shù)對(duì)象達(dá)到裝飾的效果谁帕。
可能有人會(huì)有疑問(wèn),如果之前的函數(shù)對(duì)象有一些自定義屬性呢? 裝飾后的新函數(shù)會(huì)不會(huì)丟掉冯袍?
答案是肯定的匈挖, 我們可以訪(fǎng)問(wèn)之前的函數(shù)對(duì)象,給其設(shè)置屬性康愤,這些屬性會(huì)被存儲(chǔ)在對(duì)象的__ dict__ 字典中儡循, 而 wraps 裝飾器會(huì)把原函數(shù)的__ dict__拷貝到新的裝飾后的函數(shù)對(duì)象中, 因此 wraps 裝飾后征冷,就不會(huì)丟掉原有的屬性贮折,而不使用則一定會(huì)丟掉。 感興趣的讀者可以點(diǎn)開(kāi) wraps 裝飾器资盅,看一下具體實(shí)現(xiàn)邏輯;
- @4 在本節(jié)開(kāi)始踊赠,我們提出裝飾器的通用需求呵扛,其中之一是需要將裝飾器的參數(shù)存放到被裝飾的函數(shù)中,_add_dict 方法便是將裝飾器參數(shù)設(shè)置到原函數(shù)以及裝飾后的函數(shù)中筐带;
- @5 invoke 負(fù)責(zé)實(shí)現(xiàn)具體的裝飾邏輯今穿,例如日志裝飾器僅僅是打印日志,那么該方法實(shí)現(xiàn)就是打印日志以及調(diào)用原函數(shù)伦籍。文件鎖裝飾器蓝晒,則需要先獲取鎖后再執(zhí)行原函數(shù),具體的裝飾邏輯在該方法中實(shí)現(xiàn)帖鸦, 具體的裝飾器子類(lèi)應(yīng)該重寫(xiě)該方法芝薇。下一節(jié)我們將繼承該 BaseDecorator 重寫(xiě)以上的日志及文件鎖裝飾器;
- @6 invoke 方法是裝飾函數(shù)調(diào)用時(shí)被觸發(fā)的作儿, 而 wrapper 方法只會(huì)被觸發(fā)一次洛二,當(dāng) Python 解釋器執(zhí)行到 @log 時(shí),會(huì)執(zhí)行該裝飾器的 wrapper 方法。相當(dāng)于函數(shù)被定義的時(shí)候晾嘶,執(zhí)行了 wrapper 方法妓雾。在該方法內(nèi)可以實(shí)現(xiàn)某些注冊(cè)功能,將函數(shù)和某些鍵值映射起來(lái)放到字典中垒迂,例如 web 框架的 url 和 handler 映射關(guān)系的注冊(cè)械姻。
BaseDecorator 抽出來(lái)了 invoke,wrapper 目的是讓子類(lèi)裝飾器可以在這兩個(gè)維度上擴(kuò)展机断,分別實(shí)現(xiàn)裝飾及某些注冊(cè)邏輯楷拳,在下一節(jié)我們將嘗試重寫(xiě)日志及文件鎖裝飾器,更直觀(guān)的感受 BaseDeceator 給我們帶來(lái)的便利毫缆。
4.2 重寫(xiě)日志及文件鎖裝飾器
- @1. invoke 方法中包括原函數(shù)以及原函數(shù)的輸入?yún)?shù)苦丁,該輸入?yún)?shù)不是裝飾器的參數(shù)信息浸颓;
- @2. 通過(guò) func 可以訪(fǎng)問(wèn)到裝飾器中定義的 desc 參數(shù)信息;
- @3. 創(chuàng)建裝飾器實(shí)例便可以像之前一樣使用 @log旺拉,需要注意的是該裝飾類(lèi)變成單例产上,在定義裝飾邏輯的時(shí)候,不要輕易在 self 中儲(chǔ)存變量蛾狗。
通過(guò)重寫(xiě)日志裝飾器晋涣, 擺脫了三層函數(shù)的噩夢(mèng), 成功的分離了裝飾器的基本代碼以及裝飾邏輯代碼沉桌,我們可以更加聚焦于裝飾邏輯的核心代碼編寫(xiě)谢鹊,同時(shí)可以通過(guò)原函數(shù)訪(fǎng)問(wèn)裝飾器中輸入的參數(shù),比如我們可以訪(fǎng)問(wèn)到日志裝飾器的 desc留凭。
以下我們?cè)僦貙?xiě)文件鎖裝飾器:
- @1. 可以通過(guò) func 訪(fǎng)問(wèn)到裝飾器中定義的 name 參數(shù);
- @2. 把參數(shù)傳給 do_call 委托執(zhí)行.
- @3. 創(chuàng)建文件鎖實(shí)例,其他位置就可以使用 @file_lock 了.
使用新的裝飾基類(lèi)后蔼夜, 編寫(xiě)新的裝飾器子類(lèi)是非常輕松方便的兼耀,不需要再定義復(fù)雜的三層函數(shù),不需要重復(fù)的設(shè)置裝飾器參數(shù).如果我們?cè)陧?xiàng)目中大量使用裝飾器求冷,不妨使用裝飾基類(lèi)瘤运,統(tǒng)一常見(jiàn)的功能需求。
裝飾器的更多用法還需要讀者去發(fā)掘匠题,但是熟悉 Java 的同學(xué)
一定熟悉 Aop 的理念拯坟, 筆者深受 Java 折磨多年, 對(duì) Aop 也幾分偏愛(ài)韭山。在我看來(lái)似谁, Python 的裝飾器是 Java 中的注解加 Aop 的結(jié)合傲绣。
下一節(jié)我們將橫向?qū)Ρ?Java 注解與 Python 裝飾器的相似點(diǎn), 論證文章開(kāi)頭我們留下的一個(gè)論點(diǎn)巩踏。
5. 對(duì)比 Java 的注解
之所以對(duì)比 Java 注解秃诵,主要是筆者想從 Java 的某些用法得到一些借鑒與參考, 以便于我們應(yīng)用到 Python 中塞琼,通過(guò)兩種語(yǔ)言的對(duì)比可以讓我們更深刻的理解語(yǔ)言設(shè)計(jì)者添加該特性的初衷菠净,以便更好的使用該特性。更重要的是彪杉,我們面對(duì)不同語(yǔ)言的異同會(huì)有更大的包容性毅往, 站在欣賞的角度去對(duì)比思考,對(duì)于我們快速掌握新的語(yǔ)言十分有益派近。本節(jié)絕不是為了爭(zhēng)吵兩種語(yǔ)言的優(yōu)劣攀唯, 更不想挑起語(yǔ)言的戰(zhàn)爭(zhēng)。
裝飾器和注解最直觀(guān)的相似點(diǎn)可能就是 @ 符號(hào)了渴丸, Python 使用相同的符號(hào)對(duì)于 Java 程序員是一種”關(guān)照”侯嘀。 因?yàn)?Java 程序員對(duì)于注解有一種特殊的迷戀, 第三方框架就是使用眼花繚亂的注解 幫助 Java 程序員實(shí)現(xiàn)一個(gè)個(gè)神奇的功能谱轨。而裝飾器也是可以勝任的戒幔。
Java 的注解本身只是一種元數(shù)據(jù)配置,在沒(méi)有注解之前土童, 如果實(shí)現(xiàn)相同的元數(shù)據(jù)配置只能依賴(lài)于 xml 配置诗茎, 有了注解之后,我們可以把元數(shù)據(jù)配置和代碼放到一起献汗,這樣更加直觀(guān)也更便于修改敢订,至于某些人說(shuō) xml 配置可以省去編譯打包, 其實(shí)在筆者經(jīng)歷的項(xiàng)目中罢吃,不論是改代碼還是改配置都是需要重新走發(fā)布流程枢析, 嚴(yán)禁直接修改配置重啟程序(除極特殊情況)。
注解和注解解釋器是密不可分的刃麸,定義注解之后,首先就應(yīng)該想到如何定義解釋器司浪,讀取注解上的元數(shù)據(jù)配置泊业,使用該元數(shù)據(jù)配置做什么。
最常見(jiàn)的是使用方式是使用注解注冊(cè)某些組件啊易,開(kāi)啟某項(xiàng)功能吁伺。例如 Spring 中使用 Component 注冊(cè) bean,使用 RequestMapping 注冊(cè) web url 映射租谈, Junit 使用 Test 注冊(cè)測(cè)試 Case篮奄, Spring boot 中使用 EnableXXX 開(kāi)啟某些擴(kuò)展功能等等捆愁。
注解解釋器首先需要獲取到 Class 對(duì)象,使用反射獲取到注解中的元數(shù)據(jù)配置窟却,然后實(shí)現(xiàn)”注冊(cè)”昼丑、 “開(kāi)關(guān)”邏輯。
以上在我們實(shí)現(xiàn)的解釋器基類(lèi)中夸赫,也實(shí)現(xiàn)了類(lèi)似的功能菩帝,我們把裝飾器的參數(shù)存放到具體的函數(shù)對(duì)象中, 等同于注解的元數(shù)據(jù)配置茬腿, 讀者也可以擴(kuò)展呼奢, 添加一個(gè)標(biāo)記, 標(biāo)記該函數(shù)對(duì)象確實(shí)被某裝飾器裝飾過(guò)切平。 這樣便能像 Java 一樣輕松的實(shí)現(xiàn)某些注冊(cè)或者開(kāi)關(guān)功能握础。
除此之外,注解作為元數(shù)據(jù)配置悴品,可以作為 Aop 的切面禀综,這也是注解被廣泛使用的原因, 注解可以配置在類(lèi)他匪、屬性菇存、方法之上, “注冊(cè)” 功能一般是配置在類(lèi)上邦蜜, 如果使用注解切面依鸥,需要將注解配置在方法之上。
以下列出使用注解 Aop 可以實(shí)現(xiàn)的功能:
- 異常攔截悼沈。在使用該注解的函數(shù)切面上贱迟,將異常攔截住,可以做一些通用的功能絮供,例如異常上報(bào)衣吠,異常兜底,異常忽略等壤靶;
- 權(quán)限控制缚俏。日志記錄。 可以控制注解方法的切面的用戶(hù)訪(fǎng)問(wèn)權(quán)限贮乳,也可以記錄用戶(hù)操作忧换;
- 自動(dòng)重試。 異步處理向拆。如果我們希望異步調(diào)用某方法亚茬,或者某些需要異常重試的方法,可以使用注解定義切面浓恳, 添加異步或重試處理刹缝。
注解碗暗,提供了非常靈活的切面定義方式。以上三種只是常見(jiàn)的使用方式梢夯,當(dāng)注解定義了切面言疗,Aop 會(huì)替換被代理的類(lèi), 添加某些代理邏輯厨疙, 拋開(kāi)底層實(shí)現(xiàn)原理洲守, 實(shí)際上 Aop 這種機(jī)制和 Python 的裝飾器區(qū)別并不是很大, 設(shè)計(jì)模式中裝飾器和代理模式本身就非常相似沾凄,以上注解可以實(shí)現(xiàn)的功能梗醇, Python 的裝飾器都是可以一一實(shí)現(xiàn)的。在函數(shù)被定義的時(shí)刻裝飾器就已經(jīng)生效了撒蟀, 而 Aop也是通過(guò)編譯期或者運(yùn)行期在實(shí)際調(diào)用之前代理叙谨。
Python 的裝飾器本身也是一個(gè)函數(shù),它通過(guò)語(yǔ)法糖的方式保屯,幫我們實(shí)現(xiàn)了裝飾手负,而靜態(tài)類(lèi)型的 Java 選擇了動(dòng)態(tài)修改字節(jié)碼,編譯器織入等更加復(fù)雜的技術(shù)實(shí)現(xiàn)了類(lèi)似的功能姑尺。
不同的底層實(shí)現(xiàn)竟终, 并不能影響在使用方式及場(chǎng)景上互相借鑒。 所以筆者還是認(rèn)為裝飾器更像 Java 注解 + Aop 的組合切蟋。 這樣對(duì)比 對(duì)于 Java 程序可能更容易理解统捶,更好的使用裝飾器。