時間:2005-10-24 作者:Alexandre Vasseur,?Aspect Werkz,?Joakim Dahlstedt,?Jonas Bonér |
前一篇文章 介紹了面向方面編程和關注點分離的概念,解釋了這種概念如何在方面構(gòu)造的幫助下增強軟件的模塊化,以及如何使用它來補充面向?qū)ο缶幊獭7矫娲砟K化的單元,并且由切點(何處)、建議(什么)以及類型間聲明(在這個新的方面補充對象模型)組成。有許多技術可以將關注點編織進應用程序,在當今的Java領域中,最常用的技術是字節(jié)碼測試,在AspectWerkz和AspectJ(從1.1版開始)中實現(xiàn)了這種技術。
但是,這種AOP實現(xiàn)方式具有幾個缺點,本系列的第1篇文章對此進行了詳細解釋。盡管在字節(jié)碼測試領域還有很大的發(fā)展余地(包括Java 5中的JVMTI/JSR-163測試代理規(guī)范和高效字節(jié)碼操作庫,比如ObjectWeb ASM),但字節(jié)碼測試代價不菲。此外,已經(jīng)證明,使用字節(jié)碼測試實現(xiàn)AOP是不完善的。例如,如果不采用非常特殊且效率低下的解決方案,就無法通過切點匹配反射式方法調(diào)用或get和set字段。總的來說,所有基于字節(jié)碼測試的產(chǎn)品都受到字節(jié)碼測試技術相關問題的影響,而且隨著這種技術的普及,問題將逐漸增加。
所有這些缺點促使JRockit團隊提出了JVM對AOP的支持。其目標是盡可能全面地實現(xiàn)當前的AOP語義,同時不把JVM限制在某個特定的面向方面框架的語言細節(jié)和編程模型上。
本文通過具體的代碼示例介紹該API,然后描述其好處及未來的發(fā)展方向。
我們的動機讓我們快速地回顧引入JVM的AOP支持的技術動機。
JVM編織是對上面提到的問題最自然的解決方案。為了說明其原因,我們將引入兩個例子,它們說明JVM已經(jīng)完成了編織所涉及的大多數(shù)工作:當加載一個類時,JVM讀取字節(jié)碼,建立為java.lang.reflect.* API進行服務所需的數(shù)據(jù);另一個例子是方法調(diào)度。目前的JVM將方法或代碼塊的字節(jié)碼編譯為更高級、效率也更高的構(gòu)造和執(zhí)行流(在適用代碼內(nèi)聯(lián)的地方進行代碼內(nèi)聯(lián))。由于HotSwap API的需要,JRockit JVM(可能還包括其他JVM)還會記錄哪個方法調(diào)用了其他方法,因此如果在運行時重新定義某個類,那么在所有期望的位置(內(nèi)聯(lián)的或非內(nèi)聯(lián)的),類中定義的方法主體仍然可以進行熱交換。
因此,不必為了編織進一個建議調(diào)用而修改字節(jié)碼,比如說,在特定的方法調(diào)用之前。JVM實際上可以掌握關于這個建議調(diào)用的知識,它會在任何匹配的聯(lián)結(jié)點上對此建議進行調(diào)度,然后再調(diào)度實際的方法。
由于不接觸字節(jié)碼,立即可以獲得以下好處:
- 不會由于字節(jié)碼測試而產(chǎn)生啟動開銷。
- 對于在任何位置、任何時間、以遞增式開銷添加和刪除建議的完全的運行時支持。
- 對建議的反射式調(diào)用的隱式支持。
- 不需要將類模型復制到特定于框架的某些結(jié)構(gòu),因此減少了額外的內(nèi)存占用。
與JVMDI_EVENT_METHOD_ENTRY或JVMDI_EVENT_FIELD_ACCESS等JVMDI規(guī)范中定義的眾所周知的C級別事件相比,這種方式有很大區(qū)別。在JVMDI中,必須首先處理C級別API,這使得它對于大多數(shù)開發(fā)人員來說有些復雜,而且難以分發(fā)。其次,規(guī)范沒有提供細粒度的聯(lián)結(jié)點匹配機制,而是要求預定所有這樣的事件。這仍然會導致顯著的開銷,因此不得不進行調(diào)試。
我們的方法我們想讓您先了解一下如何在JVM中添加AOP支持。關鍵之處在于我們在Java API級別上提供了動作調(diào)度和預定(下面會詳細描述)。因此,您可以寫出下面這樣的代碼:
Weaver w = WeaverFactory.getWeaver();Method staticActionMethod =SimpleAction.class.getDeclaredMethod
("simpleStaticAction",new Class[0]//no arguments);
MethodSubscription ms = new MethodSubscription
(/* where to match*/,InsertionType.BEFORE,staticActionMethod);
w.addSubscription(ms);
如您所見,我們提供了一個可訪問的JVM API,可以用它來實現(xiàn)更傳統(tǒng)的AOP方法。這為解決前面提到的傳統(tǒng)AOP實現(xiàn)問題提供了極大的靈活性,而且也使其他使用方式成為可能。下面幾節(jié)將詳細介紹這個API。
動作調(diào)度和預定JRockit JVM AOP支持公開了一個Java API,它與JVM方法調(diào)度和對象模型組件緊密集成在一起。為了確保不使JVM被限制在當前或未來的任何特定于AOP的技術方向上,我們決定實現(xiàn)一個動作調(diào)度和預定模型。
這個API使您能夠在指定的切點上描述定義良好的預定,這樣就能夠注冊JVM將要調(diào)度的動作。動作由以下組件組成:
- 一個常規(guī)Java方法——我們稱之為動作方法,對于每個匹配這個預定的聯(lián)結(jié)點,都將調(diào)用這個方法。
- 一個可選的動作實例,在這個實例上調(diào)用動作方法。
- 一組可選的參數(shù)級注釋,它們向JVM指出動作方法期望從調(diào)用堆棧獲得哪些參數(shù)。
動作還可以分為before動作、after returning動作、after throwing動作或者instead-of動作(類似于AOP的“around”概念)。
為了調(diào)用這個API,必須獲得一個jrockit.ext.weaving.Weaver實例的句柄。這個編織器實例根據(jù)它的調(diào)用者上下文來控制允許進行哪些操作。例如,在容器級編織器可以預定特定于應用程序的聯(lián)結(jié)點時,用戶可能不希望部署在應用服務器中的應用程序創(chuàng)建編織器,從而預定某些容器級或特定于JDK的聯(lián)結(jié)點的動作方法。這種編織器可見性理念反映了底層類加載器的委托模型。
我們簡單介紹一下這些構(gòu)造如何映射到常規(guī)的AOP構(gòu)造,這有助于理解這個模型:
·預定可以視為一個有類型的聯(lián)結(jié)點,或者就是一個有類型的聯(lián)結(jié)點(字段get()、set()、方法call()等等),加上一個within()/withincode()切點。
·動作實例可以視為方面實例。
·動作方法可以視為建議。
熟悉AOP的讀者可能已經(jīng)看出,要想用這個JVM級API實現(xiàn)一個完整的AOP框架,還需要進行一些開發(fā),包括一個(按照規(guī)定)管理方面實例化模型的中間層、cflow()切點的實現(xiàn)以及切點的完全合成和正交的實現(xiàn)。
API細節(jié):動作方法動作方法(與AOP的建議概念相似)就像(作為方面的)常規(guī)類的常規(guī)Java方法。它可以是static方法,也可以是成員方法。它的返回類型必須符合某些隱式約定,而且before動作的返回類型應該是void。對于instead-of動作(類似于AOP的around建議語義),其返回類型還是作為動作調(diào)用結(jié)果的堆棧的類型。
動作方法可以有參數(shù),參數(shù)的注釋進一步控制上下文公開,如下面的代碼示例所示:
import java.lang.reflect.*;import jrockit.ext.weaving.*;?
public class SimpleAction
{? public static void simpleStaticAction()
{out.println("hello static action!");
}
?public void simpleAction() {out.println("hello action!");
}
?public void simpleAction(@CalleeMethod WMethod calleeM,@CallerMethod WMethod callerM)
{out.println(callerM.getMethod().getName());
out.println(" calling ");
out.println(calleeM.getMethod().getName());
}?
}
該代碼示例引入了jrockit.ext.weaving.WMethod類。該類用作java.lang.reflect.Method、java.lang.reflect.Constructor和類的靜態(tài)初始化器(它在java.lang.reflect.*中沒有出現(xiàn))的包裝器。這與AspectJ JoinPoint.StaticPart.getSignature()抽象化相似。
下面是當前定義的注釋及其含義。
注釋 | 公開 | 備注 |
@CalleeMethod | 被調(diào)用者方法(方法、構(gòu)造函數(shù)、靜態(tài)初始化器) | ? |
@CallerMethod | 調(diào)用者方法(方法、構(gòu)造函數(shù)、靜態(tài)初始化器) | ? |
@Callee | 被調(diào)用者實例 | 濾除靜態(tài)成員調(diào)用。用作instance-of型過濾器:被調(diào)用者類型必須是所注釋的參數(shù)類型的實例。 |
@Caller | 調(diào)用者實例 | 濾除來自靜態(tài)成員的調(diào)用。用作instance-of型過濾器:調(diào)用者類型必須是所注釋的參數(shù)類型的實例。 |
@Arguments | 調(diào)用參數(shù) | ? |
為了支持instead-of,并能夠決定是否沿著截取鏈前進(就像在AOP中通過JoinPoint.proceed()概念實現(xiàn)),我們引入了jrockit.ext.weaving.InvocationContext構(gòu)造,如下所示:
import jrockit.ext.weaving.*; public class InsteadOfAction { public Object instead( InvocationContext jp, @CalleeMethod Method callee) { return jp.proceed(); } }
API的細節(jié):動作實例和動作類型
正如前面代碼示例中所示,動作方法可以是靜態(tài)的,也可以不是。如果動作方法不是靜態(tài)的,那么就必須傳遞一個動作實例,JVM在這個實例上調(diào)用動作方法。
其語法風格與Java開發(fā)人員使用java.lang.reflect.Method.invoke(null/*static method*/, .../*args*/)對方法進行反射式調(diào)用一樣。但是,利用JVM的AOP支持,底層的動作調(diào)用根本不涉及任何反射。
允許用戶控制動作實例,就會產(chǎn)生有趣的用例。例如,可以實現(xiàn)一個簡單的委托模式,在運行時用另一個實現(xiàn)替換整個動作實例,而不涉及JVM的內(nèi)部組件。
注意,這將有助于(按照規(guī)定)實現(xiàn)AOP方面實例化模型,比如issingleton()、pertarget()、perthis()、percflow()等等,同時不會將JVM API限制在某些預定義的語義上。
在將預定注冊到編織器實例之前,賦予它一個類型作為建議類型:before、instead-of、after-returning或after-throwing。
可以編寫下面這樣的代碼來創(chuàng)建預定:
// Get a Weaver instance that will act as a // container for the subscription(s) we create Weaver w = WeaverFactory.getWeaver(); // regular java.lang.reflect is used to refer // to the action method "simpleStaticAction()" Method staticActionMethod = SimpleAction.class.getDeclaredMethod( "simpleStaticAction", new Class[0]//no arguments ); MethodSubscription ms = new MethodSubscription( .../* where to match*/, InsertionType.BEFORE, staticActionMethod ); w.addSubscription(ms);
該代碼示例假設用戶使用靜態(tài)動作方法實現(xiàn)。也可以使用實例方法編寫這個示例,在這種情況下,應該傳遞給MethodSubscription一個包含類實例。
// Use of an action instance to refer to the // non static action method "simpleAction()" Method actionMethod = SimpleAction.class.getDeclaredMethod( "simpleAction", new Class[0]// no arguments ); // Instantiate the action instance SimpleAction actionInstance = new SimpleAction(); MethodSubscription ms2 = new MethodSubscription( ...,// where to match, explained below InsertionType.BEFORE, actionMethod, actionInstance ); w.addSubscription(ms2);
諸如within()和withincode()類型模式之類的AOP語義也通過該API的變體實現(xiàn)。
API細節(jié):預定
如前面的代碼示例所示,預定API依賴于java.lang.reflect.*對象模型和一些簡單的抽象化(比如jrockit.ext.weaving.WMethod)來合并方法、構(gòu)造函數(shù)和類的靜態(tài)初始化器處理。
new MethodSubscription(...)調(diào)用的第一個參數(shù)必須是jrockit.ext.weaving.Filter實例,這個實例具有幾個具體實現(xiàn)以便匹配方法、字段等等。
jrockit.ext.weaving.MethodFilter實例用作定義,JVM編織器實現(xiàn)根據(jù)它進行聯(lián)結(jié)點陰影匹配(shadow matching)。jrockit.ext.weaving.MethodFilter允許根據(jù)以下各項進行過濾(還提供額外的結(jié)構(gòu)支持within()/withincode()語義):
- 方法修飾符(比如使用java.lang.reflect.Modifier時的int)。
- Class<? extends java.lang.annotation.Annotation>,匹配方法運行時可見性注釋。
- jrockit.ext.weaving.ClassFilter實例,匹配聲明類型。
- jrockit.ext.weaving.StringFilter實例,匹配方法名。
- jrockit.ext.weaving.ClassFilter實例,匹配方法返回類型。
- jrockit.ext.weaving.UserDefinedFilter實例,用于實現(xiàn)更精細的匹配邏輯。
使用jrockit.ext.weaving.UserDefinedFilter回調(diào)機制來實現(xiàn)更高級的匹配方式(與Spring AOPorg.springframework.aop.MethodMatcher和org.springframework.aop.ClassFilter相似)。
所有這些結(jié)構(gòu)都是可選的,如果遇到null,就表示“任意匹配”。
jrockit.ext.weaving.ClassFilter提供一種類似的方式:
- Class<? extends java.lang.annotation.Annotation>,匹配類運行時可見性注釋。
- Class,匹配類類型。
- boolean值,表示是否匹配子類型。
因此,以下代碼匹配所有名稱以“bar”開頭的方法調(diào)用。注意,在這個非常簡單的例子中傳遞了好幾個null值:
StringFilter sf = new StringFilter("bar", STARTSWITH); MethodFilter mf = new MethodFilter(0, null, null, sf, null, null); MethodSubscription ms = new MethodSubscription( mf, InsertionType.BEFORE, staticActionMethod ); w.addSubscription(ms);
作為更現(xiàn)實的例子,以下代碼匹配所有三個EJB業(yè)務方法:
// Prepare the pointcut to match // @Stateless annotated classes business methods MethodFilter ejbBizMethods = new MethodFilter( PUBLIC_NONSTATIC, // Method annotation does not matter null, new ClassFilter( // Declaring class, the java.lang.Class // for the EJB we are currently manipulating ejbClass, // no subtypes matching false, // class annotation Stateless.class ), // EJB methods matching is handled // in a UserDefinedFilter below instead null, // return type does not matter null, // custom Filter callback new UserDefinedFilter() { public boolean match( MethodFilter methodFilter, WMember member, WMethod within) { return !isEjbLifeCycleMethod(member); } } );
好處
使用JVM編織而不是字節(jié)碼測試有幾個好處。從較高的層面來看,編織作為JVM功能的自然擴展出現(xiàn),因此在許多方面它不那么具有侵入性,并且為性能、可伸縮性和可用性各方面帶來了許多好處。
關于字節(jié)碼編織的問題(尤其是在加載時編織的情況下)的詳細討論,請參考本系列的第1部分。以下好處解決了所有這些問題。
- 不使用字節(jié)碼測試,增強了可伸縮性
字節(jié)碼沒有被修改。在JVM內(nèi)部組件中,仍然采用從字節(jié)碼到可執(zhí)行代碼的常規(guī)編譯管道。使用字節(jié)碼測試時,需要分析字節(jié)碼指令并用某些中間結(jié)構(gòu)來表示它們,這樣才能在測試框架(AOP編織器或基于字節(jié)碼測試的產(chǎn)品)中操縱它們;而使用JVM編織不需要這么做。
編織器變得無所不在了。即使用戶希望在啟動時注冊預定,這也不再是必須的。因為根本不需要分析字節(jié)碼指令來尋找要截取的聯(lián)結(jié)點,所以大大減少了應用程序的啟動時間。這也提供了開發(fā)真正動態(tài)的系統(tǒng)的機會——動態(tài)意味著可以在任何時候部署方面和解除方面部署,而又不會由此引起額外的開銷或復雜性。
- 不使用冗余的類型信息記錄,降低了內(nèi)存耗用并且提高了可伸縮性
因為不再進行字節(jié)碼測試,因此與對象模型雙重記錄問題相關的問題就不會出現(xiàn)了。預定API依賴于java.lang.reflect.*模型,而這個模型已經(jīng)以類似的方式向Java開發(fā)人員提供了此信息。
- 多個代理可以保持一致
因為所編織的類的字節(jié)碼沒有經(jīng)過修改,所以不會因為兩個不同的代理以不兼容的方式修改字節(jié)碼(相互隱藏原始程序的屬性),而造成沖突。預定的注冊次序起到了優(yōu)先權規(guī)則的作用。注意,如果類是可序列化的,那么不會為了在運行時執(zhí)行所編織的建議而向其添加隱藏結(jié)構(gòu),所以常規(guī)的序列化將得到充分支持。而字節(jié)碼測試技術通常需要確定序列化能力是否有所保留(例如,serialVersionUID字段的處理)。
- 支持截取反射式調(diào)用
通過使用JVM級方法調(diào)度,所有反射式調(diào)用(方法調(diào)用或者get或set字段)都可以被匹配,就像它們是常規(guī)調(diào)用,而且所有注冊的動作都將被觸發(fā)一樣。這不需要任何額外的開銷,也不涉及特定于實現(xiàn)的細節(jié)和復雜性。
未來的發(fā)展方向
盡管JVM編織很有幫助,而且解決了與字節(jié)碼測試技術相關的可伸縮性和可用性問題,但是仍然必須解決一些缺陷才能使其完美地實現(xiàn)用例,這可能需要采用一些補充方法。
一些基于字節(jié)碼測試的產(chǎn)品使用了細粒度更改,當前的JVM AOP API還無法實現(xiàn)這一特性。某些用例處理同步塊,因此不同的鎖定機制(如:分布式鎖定)可以透明地注入常規(guī)的應用程序。這樣的細粒度動作常常要求對同步塊進行有條件執(zhí)行,甚至完全刪除同步塊,并使用某個專用鎖定API調(diào)用來替換它。可以在JVM中解決這樣的特定需求,但是實際上不可能找到一個對每種用例都有效的高效解決方案。還有必要提醒一下的是,目前領先的AOP框架還不能將同步塊公開為聯(lián)結(jié)點。
在JVM級別上,無法輕松地實現(xiàn)AspectJ定義的某些細粒度語義。例如,AspectJ支持預初始化、初始化和構(gòu)造函數(shù)執(zhí)行切點。構(gòu)造函數(shù)執(zhí)行切點挑選出源代碼中出現(xiàn)的構(gòu)造函數(shù),初始化切點挑選出獲得已初始化實例的所有構(gòu)造函數(shù)執(zhí)行,包括this(...)構(gòu)造函數(shù)委托。JVM難以把握這兩者的差異。更具侵入性的代碼內(nèi)聯(lián)策略可能會出現(xiàn)在哪些地方實際上也可能取決于編譯器。
隨著字節(jié)碼測試逐漸流行起來,新的JVM API的引入肯定會遇到挑戰(zhàn)。如果要開發(fā)一種同時適應兩種JVM(支持新API的JVM,比如JRockit,以及不支持新API的JVM)的產(chǎn)品,那么成本會相當高。這個領域中的規(guī)范(如:JSR)可能有助于克服這種困難。
結(jié)束語
字節(jié)碼測試技術目前已經(jīng)在不同領域的Java平臺上得到廣泛使用,從面向方面軟件開發(fā)到更特定于應用的解決方案(如:應用程序監(jiān)控、持久性或分布式計算)。隨著字節(jié)碼測試的可用性和透明性的提高,加載時編織和部署時測試將會流行起來。
遺憾的是,這種技術沒有為可伸縮性和可用性需求提供適當?shù)闹С帧L貏e是隨著這種技術的應用越來越廣泛,以及對來自不同產(chǎn)品的不同測試代理的混合使用,這個問題會越來越嚴重。JVM編織和JVM對AOP的支持(比如在JRockit中所實現(xiàn)的)是解決這個問題的自然方法,可以促進革新和技術發(fā)展。JRockit團隊所提出的Java API將JVM方法調(diào)度內(nèi)部組件與用戶定義的動作聯(lián)系起來,僅依賴于java.lang.reflect API的預定優(yōu)雅地填補了以前的鴻溝,并解決了主要的可伸縮性和可用性問題。
這種新的API要想獲得廣泛采用,需要對它進行認真的評估,并將它應用于真實的用例,比如AOP或者大型應用程序的運行時自適應。