AOP = Proxy Pattern + Method Reflection + Aspect DSL + 自動(dòng)代碼生成
Declarative Programming & DSL
Aspect Oriented Programming(面向方面編程,簡稱AOP)是一種Declarative Programming(聲明式編程)。
Declarative Programming(聲明式編程)是和Imperative Programming(命令式編程)相對的概念。
我們平時(shí)使用的編程語言,比如C++、Java、Ruby、Python等,都屬于Imperative Programming(命令式編程)。Imperative Programming(命令式編程)的意思是,程序員需要一步步寫清楚程序需要如何做什么(How to do What)。
Declarative Programming(聲明式編程)的意思是,程序員不需要一步步告訴程序如何做,只需要告訴程序在哪些地方做什么(Where to do What)。比起Imperative Programming(命令式編程)來,Declarative Programming(聲明式編程)是在一個(gè)更高的層次上編程。Declarative Programming編程語言是更高級的語言。Declarative Programming通常處理一些總結(jié)性、總覽性的工作,不適合做順序相關(guān)的細(xì)節(jié)相關(guān)的底層工作。
如果說Imperative Programming是拼殺在第一線的基層工作人員,Declarative Programming就是總設(shè)計(jì)師、規(guī)則制定者。
Declarative Programming Language的概念,和Domain Specific Language(領(lǐng)域?qū)S谜Z言,簡稱DSL)的概念有相通之處。DSL主要是指一些對應(yīng)專門領(lǐng)域的高層編程語言,和通用編程語言的概念相對。DSL對應(yīng)的專門領(lǐng)域(Domain)一般比較狹窄,或者對應(yīng)于某個(gè)行業(yè),或者對應(yīng)于某一類具體應(yīng)用程序,比如數(shù)據(jù)庫等。
最常見的DSL就是關(guān)系數(shù)據(jù)庫的結(jié)構(gòu)化數(shù)據(jù)查詢語言SQL。同時(shí),SQL也是一門Declarative Programming Language。SQL只需要告訴數(shù)據(jù)庫,處理符合一定條件的數(shù)據(jù),而不需要自己一步步判斷每一條數(shù)據(jù)是否符合條件。SQL的形式一般是 select … where …,update … where …,delete … where …。
當(dāng)然,這樣一來,很多基層工作,SQL做不了。因此,大部分?jǐn)?shù)據(jù)庫都提供了另外的Imperative Programming Language,用來編寫Stored Procedure(存儲過程)等,以便處理一些更加細(xì)節(jié)的工作。
常見的DSL還有Rule Engine(規(guī)則引擎)語言、Workflow(工作流)語言等。Rule Engine和Workflow同時(shí)帶有Imperative Programming和Declarative Programming的特點(diǎn)。
Rule Engine允許用戶按照優(yōu)先級定義一系列條件組合,并定義對滿足條件的數(shù)據(jù)的處理過程。
Workflow也大致類似。Workflow把最基本的條件判斷和循環(huán)語句的常見組合,定義為更加高級復(fù)雜的常用程序流程邏輯塊。用戶可以用這些高級流程塊組合更加復(fù)雜的流程塊,從而定義更加復(fù)雜的流程跳轉(zhuǎn)條件。用戶也可以定義當(dāng)程序運(yùn)行上下文滿足一定條件的時(shí)候,應(yīng)該做什么樣的處理工作。
Rule Engine和Workflow的語言形式有可能是XML格式,也有可能是Ruby、Python、Javascript等腳本格式。我個(gè)人比較傾向于腳本格式,因?yàn)閄ML適合表達(dá)結(jié)構(gòu)化數(shù)據(jù),而不擅長表達(dá)邏輯流程。當(dāng)然,XML格式的好處也是顯而易見的。解析器可以很容易分析XML文件的結(jié)構(gòu),XML定義的條件或者程序流程都可以很方便地作為數(shù)據(jù)來處理。
介紹了Declarative Programming和DSL之后,我們來看本章題目表達(dá)的內(nèi)容——AOP。
AOP是Declarative Programming,AOP語言也可以看作是DSL。AOP語言對應(yīng)的專門領(lǐng)域(Domain)就是程序結(jié)構(gòu)的方方面面(Aspect),比如程序的類、方法、成員變量等結(jié)構(gòu),以及針對這些程序結(jié)構(gòu)的通用工作處理,比如日志管理、權(quán)限管理、事務(wù)管理等。
AOP處理的工作內(nèi)容一般都是這樣的一些總結(jié)性工作:“我想讓所有的數(shù)據(jù)庫類都自動(dòng)進(jìn)行數(shù)據(jù)庫映射”、“我想打印出所有業(yè)務(wù)類的工作流程日志”、“我想給所有關(guān)鍵業(yè)務(wù)方法都加上事務(wù)管理功能”、“我想給所有敏感數(shù)據(jù)處理方法都加上安全管理授權(quán)機(jī)制”等等。
下面我們介紹AOP的實(shí)現(xiàn)原理和使用方法。
AOP實(shí)現(xiàn)原理
AOP的實(shí)現(xiàn)原理可以看作是Proxy/Decorator設(shè)計(jì)模式的泛化。
我們先來看Proxy模式的簡單例子。
Proxy {
innerObject; // 真正的對象
f1() {
// 做一些額外的事情
innerObject.f1(); // 調(diào)用真正的對象的對應(yīng)方法
// 做一些額外的事情
}
}
在Python、Ruby等動(dòng)態(tài)類型語言中,只要實(shí)現(xiàn)了f1()方法的類,都可以被Proxy包裝。在Java等靜態(tài)類型語言中,則要求Proxy和被包裝對象實(shí)現(xiàn)相同的接口。動(dòng)態(tài)語言實(shí)現(xiàn)Proxy模式要比靜態(tài)語言容易得多,動(dòng)態(tài)語言實(shí)現(xiàn)AOP也要比靜態(tài)語言容易得多。
假設(shè)我們用Proxy包裝了10個(gè)類,我們通過調(diào)用Proxy的f1()方法來調(diào)用這10個(gè)類的f1()方法,這樣,所有的f1()調(diào)用都會(huì)執(zhí)行同樣的一段“額外的工作”,從而實(shí)現(xiàn)了“所有被Proxy包裝的類,都執(zhí)行一段同樣的額外工作”的任務(wù)。這段“額外的工作”可能是進(jìn)行日志記錄,權(quán)限檢查,事務(wù)管理等常見工作。
Proxy模式是可以疊加的。我們可以定義多種完成特定方面任務(wù)(Aspect),比如,我們可以定義LogProxy、SecurityProxy、TransactionProxy,分別進(jìn)行日志管理、權(quán)限管理、事務(wù)管理。
LogProxy {
f1(){
// 記錄方法進(jìn)入信息
innerObject.f1();// 調(diào)用真正的對象的對應(yīng)方法
// 記錄方法退出信息
}
}
SecurityProxy {
f1(){
// 進(jìn)行權(quán)限驗(yàn)證
innerObject.f1();// 調(diào)用真正的對象的對應(yīng)方法
}
}
TransactonProxy {
f1(){
Open Transaction
innerObject.f1();// 調(diào)用真正的對象的對應(yīng)方法
Close Transaction
}
}
根據(jù)AOP的慣用叫法,上述的這些Proxy也叫做Advice。這些Proxy(or Advice)可以按照一定的內(nèi)外順序套起來,最外面的Proxy會(huì)最先執(zhí)行。
包裝f1()方法,也叫做截獲(Intercept)f1()方法。Proxy/Advice有時(shí)候也叫做Interceptor。
看到這里,讀者可能會(huì)產(chǎn)生兩個(gè)問題。
問題一:上述代碼采用的Proxy模式只是面向?qū)ο蟮奶匦裕趺磿?huì)扯上一個(gè)新概念“面向方面(AOP)”呢?
問題二:Proxy模式雖然避免了重復(fù)“額外工作”代碼的問題,但是,每個(gè)相關(guān)類都要被Proxy包裝,這個(gè)工作也是很煩人。AOP Proxy如何能在應(yīng)用程序中大規(guī)模使用呢?
下面我們來解答著兩個(gè)問題。
對于問題一,我們來看一個(gè)復(fù)雜一點(diǎn)的例子。
假設(shè)被包裝對象有f1()和f2()兩個(gè)方法都要被包裝。
RealObject{
f1() {…}
f2() {…}
}
這個(gè)時(shí)候,我們應(yīng)該如何做?難道讓Proxy也定義f1()和f2()兩個(gè)方法?就象下面代碼這樣?
Proxy {
innerObject; // 真正的對象
f1() {
// 做一些額外的事情
innerObject.f1(); // 調(diào)用真正的對象的對應(yīng)方法
// 做一些額外的事情
}
f2() {
// 做一些額外的事情
innerObject.f2(); // 調(diào)用真正的對象的對應(yīng)方法
// 做一些額外的事情
}
}
這樣做有幾個(gè)不利之處。一是會(huì)造成代碼重復(fù),Proxy的f1()和f2()里面的“做一些額外的事情”代碼重復(fù)。二是難以擴(kuò)展,被包裝對象可能有多個(gè)不同的方法,不同的被包裝對象需要被包裝的方法也可能不同。
現(xiàn)在的問題就變成,“Proxy如何才能包裝截獲任何類的任何方法?”
答案呼之欲出。對,就是Reflection。Java、Python、Ruby都支持Reflection,都支持Method(方法)對象。那么我們就利用Method Reflection編寫一個(gè)能夠解惑任何類的任何方法的Proxy/Advice/Interceptor。
MethodInterceptor{
around( method ){
// 做些額外的工作
method.invoke(…); // 調(diào)用真正的對象方法
// 做些額外的工作
}
}
上述的MethodInterceptor就可以分別包裝和截獲f1()和f2()兩個(gè)方法。
這里的method參數(shù)就是方法對象,在Java、Ruby等面向?qū)ο笳Z言中,需要用Reflection獲取方法對象。這個(gè)方法對象就相當(dāng)于函數(shù)式編程的函數(shù)對象。
在函數(shù)式編程中,函數(shù)對象屬于“一等公民”,函數(shù)對象的獲取不需要經(jīng)過Reflection機(jī)制。所以,函數(shù)式編程對AOP的支持,比面向?qū)ο缶幊谈谩?
由此我們看到,AOP對應(yīng)的問題領(lǐng)域確實(shí)超出了OOP的力所能及的范圍。OOP只能處理同一個(gè)類體系內(nèi)的同一個(gè)方法簽名的截獲和包裝工作,一旦涉及到一個(gè)類的多個(gè)不同方法,或者多個(gè)不同類體系的不同方法,OOP就黔驢技窮,無能為力了。
使用Method Reflection的方式截獲任何方法對象,是AOP的常用實(shí)現(xiàn)手段之一。另一個(gè)常見手段就是自動(dòng)代碼生成了。這也回答了前面提出的問題二——如何在應(yīng)用系統(tǒng)中大規(guī)模使用AOP。
Proxy Pattern + Method Reflection + 自動(dòng)代碼生成 這樣一個(gè)三元組合,就是AOP的基本實(shí)現(xiàn)原理。
Proxy Pattern 和 Method Reflection,前面已經(jīng)做了闡述,下面我們來講解自動(dòng)代碼生成。
首先,AOP需要定義一種Aspect描述的DSL。Aspect DSL主要用來描述這樣的內(nèi)容:“用TransactionProxy包裝截獲business目錄下的所有類的公共業(yè)務(wù)方法”、“ 用SecurityProxy包裝截獲所有Login/Logout開頭的類的所有公共方法”、“用LogProxy包裝截獲所有文件的所有方法”等等。
Aspect DSL的形式有多種多樣。有的是一種類似Java的語法,比如AspectJ;有的是XML格式或者各種腳本語言,比如,Spring AOP等。
有了Aspect DSL,AOP處理程序就可以生成代碼了。AOP生成代碼有三種可能方式:
(1)靜態(tài)編譯時(shí)期,源代碼生成。為每個(gè)符合條件的類方法產(chǎn)生對應(yīng)的Proxy對象。AspectJ以前就是這種方式。
(2)靜態(tài)編譯時(shí)期,處理編譯后的字節(jié)碼。Java、Python之類的虛擬機(jī)語言都有一種中間代碼(Java的中間代碼叫做字節(jié)碼),AOP處理程序可以分析字節(jié)碼,并直接產(chǎn)生字節(jié)碼形式的Proxy。這種方式也叫做靜態(tài)字節(jié)碼增強(qiáng)。AspectJ也支持這種方式。Java有一些開源項(xiàng)目,比如ASM、Cglib等,可以分析并生成Java字節(jié)碼。這些開源項(xiàng)目不僅可以靜態(tài)分析增強(qiáng)字節(jié)碼,還可以在程序運(yùn)行期動(dòng)態(tài)分析增強(qiáng)字節(jié)碼。很多AOP項(xiàng)目,比如Spring AOP,都采用ASM/Cglib處理字節(jié)碼。
(3)動(dòng)態(tài)運(yùn)行時(shí)期,即時(shí)處理裝載到虛擬機(jī)內(nèi)部的類結(jié)構(gòu)字節(jié)碼。這也叫做動(dòng)態(tài)增強(qiáng)。比如,Spring AOP。如前所述,Spring AOP使用ASM/Cglib之類的處理字節(jié)碼的開源項(xiàng)目。Java運(yùn)行庫本身也提供了類似于ASM/Cglib的簡單的動(dòng)態(tài)處理字節(jié)碼的API,叫做Dynamic Proxy。
以上就是AOP的實(shí)現(xiàn)原理:Proxy Pattern + Method Reflection + Aspect DSL + 自動(dòng)代碼生成。
總體來說,實(shí)現(xiàn)AOP的便利程度,函數(shù)式編程語言 > 動(dòng)態(tài)類型語言 > 靜態(tài)類型語言。當(dāng)然,這個(gè)不等式并不是絕對的。有些動(dòng)態(tài)類型語言提供了豐富強(qiáng)大的語法特性,實(shí)現(xiàn)AOP的便利程度,可能要超過函數(shù)式編程語言。
DSL & Parser
本章前面還提到了DSL。SQL、Rule Engine、Workflow、Aspect DSL都屬于DSL。
DSL有可能是XML格式,也有可能是腳本格式。XML的解析比較容易,但是,XML不適合表達(dá)邏輯。腳本格式表達(dá)邏輯很方便靈活,但是,解析上就沒有XML那么容易。
DSL不是通用編程語言,語法一般比較簡單,沒有通用編程語言那么復(fù)雜。很多DSL提供商都自己定義語法。一些程序設(shè)計(jì)大師甚至提出Language Oriented Programming(面向語言編程,簡稱LOP)的激進(jìn)觀點(diǎn),認(rèn)為每一個(gè)應(yīng)用程序都可能需要一套獨(dú)立的自定義DSL。這個(gè)觀點(diǎn)提出來之后,跟風(fēng)者眾。業(yè)界掀起自定義DSL的狂潮。
從特定角度來看,這個(gè)觀點(diǎn)是有一定道理的。一般來說,每一個(gè)應(yīng)用程序都需要一套特定功能的公用函數(shù)庫。DSL的目的正好就是處理特定領(lǐng)域的特定問題。從這個(gè)意義上來說,特定功能公用函數(shù)庫和DSL的作用是一致的。DSL可以代替特定功能公用函數(shù)庫的作用。DSL定義的關(guān)鍵字就相當(dāng)于公用函數(shù)庫的API函數(shù)名。
DSL關(guān)鍵字比起API函數(shù)名的優(yōu)勢在于可讀性好。
比如,用DSL(比如SQL)我們可以寫,select * from …. where a = 1
用API函數(shù)我們就要寫,Query.find( tableName, new NumberEqualCriteria(“a”, 1) )
兩者的可讀性優(yōu)劣一目了然。
而且,DSL一般都是解釋執(zhí)行,不需要參與源程序編譯,這使得DSL程序(相當(dāng)于API函數(shù)的調(diào)用程序)很容易修改和配置。
但DSL的實(shí)現(xiàn)不如編寫API函數(shù)那么容易,尤其是腳本格式的DSL,程序員需要解析并執(zhí)行DSL腳本,需要實(shí)現(xiàn)一個(gè)小型解釋器。
對于很簡單的語法,可以利用動(dòng)態(tài)類型語言的高級特性,實(shí)現(xiàn)DSL腳本語句到函數(shù)方法調(diào)用的映射。比如,Open Transaction這條DSL語句經(jīng)過簡單的分析處理,可以映射為函數(shù)調(diào)用Transaction.open()。
復(fù)雜一些的DSL語法就需要用到專門的Parser(解析器)了。看過編譯原理知識的讀者,應(yīng)該還有印象:Parser(解析器)是編譯器的重要組成部分,Parser可以分為字符串分析、詞法分析、語法分析、語義分析等部分,Parser是一個(gè)相當(dāng)復(fù)雜的程序。
如果每一套自定義DSL都需要程序員從頭開始重新寫一個(gè)Parser,代價(jià)很大,得不償失。幸虧有一些輔助產(chǎn)生Parser的應(yīng)用程序,可以幫助生成Parser。
Parser產(chǎn)生器可以分為兩種:
(1)Parser Generator。比如,antlr、javacc、yacc等項(xiàng)目。程序員定義BNF格式的語法說明,然后用Parser Generator根據(jù)BNF語法說明,產(chǎn)生對應(yīng)的Parser程序。BNF格式是類似于這樣的一種表達(dá)方式:a ::= b & c; d ::= e | f。有興趣的讀者可以查閱BNF的具體定義。Parser Generator的優(yōu)點(diǎn)在于BNF語法簡單通用,缺點(diǎn)在于需要靜態(tài)生成Parser源代碼。
(2)Parser Combinator。代表作是Haskell語言編寫的一個(gè)叫做Parsec的項(xiàng)目。Parser Generator提供了基本的字符串解析器、詞法分析器等,還提供了與或非、循環(huán)、選擇、提前幾步判斷等語法組合子。