進入AOP時代

一、AOP編程概覽

  面向對象編程技術進入軟件開發的主流對軟件的開發方式產生了極大的影響,開發者可以用一組實體以及這些實體之間的關係將系統形象地表示出來,這使得他們能夠設計出規模更大、更復雜的系統,開發週期也比以前更短。OO開發的唯一問題是,它本質上是靜態的,需求的細微變化就可能對開發進度造成重大影響。

  Aspect-Oriented Programming(AOP)是對OO技術的補充和完善,它允許開發者動態地修改靜態的OO模型,構造出一個能夠不斷增長以滿足新增需求的系統,就象現實世界中的對象會在其生命週期中不斷改變自身,應用程序也可以在發展中擁有新的功能。

  例如,許多人想必有過在開發簡單的Web應用時將Servlet作爲入口點的經驗,即用Servlet接收HTML表單的輸入,經過處理後返回給用戶。開始時的Servlet可能是非常簡單的,只有剛好滿足用戶需求的最少量的代碼。然而,隨着“第二需求”的實現,例如實現異常處理、安全、日誌等功能,代碼的體積就會增加到原來的三、四倍——之所以稱之爲“第二需求”,是因爲Servlet的基本功能是接受和處理用戶的請求,對於這個目標來說,日誌、安全之類的機制並不是必不可少的。

  AOP允許動態地改變OO的靜態模型,不必修改原來的靜態模型也可以加入滿足第二需求所需的代碼(實際上,甚至連原來的源代碼也不需要)。更令人稱奇的是,後來加入的代碼往往可以集中在一個地方,而不必象單純使用OO時那樣將後來加入的代碼分散到整個模型。

  二、基本術語

  在介紹AOP開發實例之前,我們先來了解幾個標準的AOP術語,以便更好地掌握相關的概念。

  █ Cross-cutting concern

  在OO模型中,雖然大部份的類只有單一的、特定的功能,但它們通常會與其他類有着共同的第二需求。例如,當線程進入或離開某個方法時,我們可能既要在數據訪問層的類中記錄日誌,又要在UI層的類中記錄日誌。雖然每個類的基本功能極然不同,但用來滿足第二需求的代碼卻基本相同。

  █ Advice

  它是指想要應用到現有模型的附加代碼。在本例中,它是指線程進入或退出某個方法時要運行的日誌代碼。

  █ Point-cut

  這個術語是指應用程序中的一個執行點,在這個執行點上需要採用前面的cross-cutting concern。在本例中,當線程進入一個方法時出現一個Point-cut,當線程離開方法時又出現另一個Point-cut。

  █ Aspect

  Point-cut和advice結合在一起就叫做aspect。在下面的例子中,我們通過定義一個point-cut並給予適當的advice加入了一個日誌(logging)aspect。

  AOP還有其它許多特性和術語,例如引入(Introduction),即把接口/方法/域引入到現有的類——它極大地拓寬了開發者的想象力。不過本文只介紹一些最基本的持性,熟悉這裏介紹的概念後,你再深入一步研究AOP的其它特性,看看如何在自己的開發環境中使用它們。

  三、現有的框架

  目前最成熟、功能最豐富的AOP框架當數AspectJ,AspectJ已成爲大多數其它框架跟從的標準。但是,AspectJ也走出了非同尋常的一步,它的實現爲Java語言增添了新的關鍵詞。雖然新的語法並不難學,但卻意味着我們必須換一個編譯器,還要重新配製編輯器,只有這樣才能適應新的語法。在規模較大的開發組中,這些要求可能難以辦到,因爲整個開發小組都會受到影響。由於語言本身的變化,開發小組把AOP技術引入到現有項目的學習週期隨之延長。

  現在我們需要的是這樣一個框架,它可以方便地引入,且不會對原來的開發和構造過程產生任何影響。滿足這些要求的框架不止一個,例如JBoss AOP、Nanning、Aspectwerkz(AW)。本文選用的是Aspectwerkz,因爲它可能是最容易學習的框架,也是最容易集成到現有項目的框架。

  Aspectwerkz由Jonas Boner和Alexandre Vasseur創建,它是目前最快速、功能最豐富的框架之一。雖然它還缺乏AspectJ的某些功能,但己足以滿足大多數開發者在許多情形下的需要。

  Aspectwerkz最令人感興趣的特性之一是它能夠以兩種不同的模式運行:聯機模式和脫機模式。在聯機模式下,AW直接干預屬於JVM的底層類裝入機制,截取所有的類裝入請求,對字節碼實施即時轉換。AW提供了干預類裝入過程的許多選項,另外還有一個替代bin/java命令的封裝腳本,這個腳本能夠根據Java版本和JVM能力自動生成一組可運行的配製。對於開發者,聯機模式有許多優點,它能插入到任何類裝入器並在類裝入期間生成新的類。也就是說,我們不必手工修改應用程序的類,只要按通常的方式部署即可。不過,聯機模式要求對應用服務器進行額外的配製,有時這一要求可能很難滿足。

  在脫機模式下,生成類需要二個步驟。第一步是用標準的編譯器編譯,第二步是重點——以脫機模式運行AWcompiler編譯器,讓它處理新生成的類。編譯器將修改這些類的字節碼,根據一個XML文件的定義,在適當的point-cut插入advice。脫機模式的優點是AWcompiler生成的類能夠在任何JVM 1.3以上的虛擬機運行,本文下面要用的就是這種模式,因爲它不需要對Tomcat作任何修改,只要對構造過程稍作修改就可以照搬到大多數現有的項目。

  四、安裝

  本文將以一個簡單的Web應用程序爲例,它用Ant編譯,部署在Tomcat 4+ Servlet容器上。下面我們假定讀者己準備好上述環境,包括JVM 1.3+,同時Tomcat被設置成從webapps文件夾自動部署應用,自動將WAR擴展到目錄(這是Tomcat默認的操作方式,因此只要你尚未修改Tomcat的運行方式,下面的範例可直接運行)。我們將把Tomcat的安裝位置稱爲%TOMCAT_HOME%。

  ⑴ 從http://apectwerkz.codehaus.org/下載Aspectwerkz,解開壓縮到適當的位置。我們將把這個位置稱爲%ASPECTWERKZ_HOME%。

  ⑵ 設置%ASPECTWERKZ_HOME%環境變量。

  ⑶ 將Aspectwerkz加入到PATH環境變量,即設置set PATH=%PATH%;%ASPECTWERKZ_HOME%/bin/aspectwerkz

  ⑷ 下載本文的示範程序,將它放入%TOMCAT_HOME%/webapps文件夾。

  ⑸ 將Aspectwerkz的運行時類加入到Tomcat的classpath。你可以將它的JAR文件放入示例應用的WEB-INF/lib文件夾,或放入%TOMCAT_HOME%/common/lib。




對於一個能夠訪問源代碼的經驗豐富的Java開發人員來說,任何程序都可以被看作是博物館裏透明的模型。類似線程轉儲(dump)、方法調用跟蹤、斷點、切面(profiling)統計表等工具可以讓我們瞭解程序目前正在執行什麼操作、剛纔做了什麼操作、未來將做什麼操作。但是在產品環境中情況就沒有那麼明顯了,這些工具一般是不能夠使用的,或最多隻能由受過訓練的開發者使用。支持團隊和最終用戶也需要知道在某個時刻應用程序正在執行什麼操作。

  爲了填補這個空缺,我們已經發明瞭一些簡單的替代品,例如日誌文件(典型情況下用於服務器處理)和狀態條(用於GUI應用程序)。但是,由於這些工具只能捕捉和報告可用信息的一個很小的子集,並且通常必須把這些信息用容易理解的方式表現出來,所以程序員趨向於把它們明確地編寫到應用程序中。而這些代碼會纏繞着應用程序的業務邏輯,當開發者試圖調試或瞭解核心功能的時候,他們必須"圍繞這些代碼工作",而且還要記得功能發生改變後更新這些代碼。我們希望實現的真正功能是把狀態報告集中在某個位置,把單個狀態消息作爲元數據(metadata)來管理。

  在本文中我將考慮使用嵌入GUI應用程序中的狀態條組件的情形。我將介紹多種實現這種狀態報告的不同方法,從傳統的硬編碼習慣開始。隨後我會介紹Java 1.5的大量新特性,包括註解(annotation)和運行時字節碼重構(instrumentation)。
狀態管理器(StatusManager)

  我的主要目標是建立一個可以嵌入GUI應用程序的JStatusBar Swing組件。圖1顯示了一個簡單的Jframe中狀態條的樣式。

221784.gif
圖1.我們動態生成的狀態條

  由於我不希望直接在業務邏輯中引用任何GUI組件,我將建立一個StatusManager(狀態管理器)來充當狀態更新的入口點。實際的通知會被委託給StatusState對象,因此以後可以擴展它以支持多個併發的線程。圖2顯示了這種安排。

221785.gif
圖2. StatusManager和JstatusBar

  現在我必須編寫代碼調用StatusManager的方法來報告應用程序的進程。典型情況下,這些方法調用都分散地貫穿於try-finally代碼塊中,通常每個方法一個調用。

public void connectToDB (String url) {
 StatusManager.push("Connecting to database");
 try {
  ...
 } finally {
  StatusManager.pop();
 }
}

  這些代碼實現了我們所需要功能,但是在代碼庫中數十次、甚至於數百次地複製這些代碼之後,它看起來就有些混亂了。此外,如果我們希望用一些其它的方式訪問這些消息該怎麼辦呢?在本文的後面部分中,我將定義一個用戶友好的異常處理程序,它共享了相同的消息。問題是我把狀態消息隱藏在方法的實現之中了,而沒有把消息放在消息所屬的接口中。

  面向屬性編程

  我真正想實現的操作是把對StatusManager的引用都放到代碼外面的某個地方,並簡單地用我們的消息標記這個方法。接着我可以使用代碼生成(code-generation)或運行時反省(introspection)來執行真正的工作。XDoclet項目把這種方法歸納爲面向屬性編程(Attribute-Oriented Programming),它還提供了一個框架組件,可以把自定義的類似Javadoc的標記轉換到源代碼之中。

  但是,JSR-175包含了這樣的內容,Java 1.5爲了包含真實代碼中的這些屬性提供了一種結構化程度更高的格式。這些屬性被稱爲"註解(annotations)",我們可以使用它們爲類、方法、字段或變量定義提供元數據。它們必須被顯式聲明,並提供一組可以包含任意常量值(包括原語、字符串、枚舉和類)的名稱-值對(name-value pair)。

  註解(Annotations)

  爲了處理狀態消息,我希望定義一個包含字符串值的新註解。註解的定義非常類似接口的定義,但是它用@interface關鍵字代替了interface,並且只支持方法(儘管它們的功能更像字段):

public @interface Status {
 String value();
}

  與接口類似,我把@interface放入一個叫做Status.java的文件中,並把它導入到任何需要引用它的文件中。

  對我們的字段來說,value可能是個奇怪的名稱。類似message的名稱可能更適合;但是,value對於Java來說具有特殊的意義。它允許我們使用@Status("...")代替@Status(value="...")來定義註解,這明顯更加簡捷。

  我現在可以使用下面的代碼定義自己的方法:

@Status("Connecting to database")
public void connectToDB (String url) {
...
}

  請注意,我們在編譯這段代碼的時候必須使用-source 1.5選項。如果你使用Ant而不是直接使用javac命令行建立應用程序,那麼你需要使用Ant 1.6.1以上版本。

  作爲類、方法、字段和變量的補充,註解也可以用於爲其它的註解提供元數據。特別地,Java引入了少量註解,你可以使用這些註解來定製你自己的註解的工作方式。我們用下面的代碼重新定義自己的註解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Status {
String value();
}

  @Target註解定義了@Status註解可以引用什麼內容。理想情況下,我希望標記大塊的代碼,但是它的選項只有方法、字段、類、本地變量、參數和其它註解。我只對代碼感興趣,因此我選擇了METHOD(方法)。
 
  @Retention註解允許我們指定Java什麼時候可以自主地拋棄消息。它可能是SOURCE(在編譯時拋棄)、CLASS(在類載入時拋棄)或RUNTIME(不拋棄)。我們先選擇SOURCE,但是在本文後部我們會更新它。
重構源代碼

  現在我的消息都被編碼放入元數據中了,我必須編寫一些代碼來通知狀態監聽程序。假設在某個時候,我繼續把connectToDB方法保存源代碼控件中,但是卻沒有對StatusManager的任何引用。但是,在編譯這個類之前,我希望加入一些必要的調用。也就是說,我希望自動地插入try-finally語句和push/pop調用。

  XDoclet框架組件是一種Java源代碼生成引擎,它使用了類似上述的註解,但是把它們存儲在Java源代碼的註釋(comment)中。XDoclet生成整個Java類、配置文件或其它建立的部分的時候非常完美,但是它不支持對已有Java類的修改,而這限制了重構的有效性。作爲代替,我可以使用分析工具(例如JavaCC或ANTLR,它提供了分析Java源代碼的語法基礎),但是這需要花費大量精力。

  看起來沒有什麼可以用於Java代碼的源代碼重構的很好的工具。這類工具可能有市場,但是你在本文的後面部分可以看到,字節碼重構可能是一種更強大的技術。 重構字節碼

  不是重構源代碼然後編譯它,而是編譯原始的源代碼,然後重構它所產生的字節碼。這樣的操作可能比源代碼重構更容易,也可能更加複雜,而這依賴於需要的準確轉換。字節碼重構的主要優點是代碼可以在運行時被修改,不需要使用編譯器。

  儘管Java的字節碼格式相對簡單,我還是希望使用一個Java類庫來執行字節碼的分析和生成(這可以把我們與未來Java類文件格式的改變隔離開來)。我選擇了使用Jakarta的Byte Code Engineering Library(字節碼引擎類庫,BCEL),但是我還可以選用CGLIB、ASM或SERP。

  由於我將使用多種不同的方式重構字節碼,我將從聲明重構的通用接口開始。它類似於執行基於註解重構的簡單框架組件。這個框架組件基於註解,將支持類和方法的轉換,因此該接口有類似下面的定義:

public interface Instrumentor
{
 public void instrumentClass (ClassGen classGen,Annotation a);
 public void instrumentMethod (ClassGen classGen,MethodGen methodGen,Annotation a);
}

  ClassGen和MethodGen都是BCEL類,它們使用了Builder模式(pattern)。也就是說,它們爲改變其它不可變的(immutable)對象、以及可變的和不可變的表現(representation)之間的轉換提供了方法。

  現在我需要爲接口編寫實現,它必須用恰當的StatusManager調用更換@Status註解。前面提到,我希望把這些調用包含在try-finally代碼塊中。請注意,要達到這個目標,我們所使用的註解必須用@Retention(RetentionPolicy.CLASS)進行標記,它指示Java編譯器在編譯過程中不要拋棄註解。由於在前面我把@Status聲明爲@Retention(RetentionPolicy.SOURCE)的,我必須更新它。

  在這種情況下,重構字節碼明顯比重構源代碼更復雜。其原因在於try-finally是一種僅僅存在於源代碼中的概念。Java編譯器把try-finally代碼塊轉換爲一系列的try-catch代碼塊,並在每一個返回之前插入對finally代碼塊的調用。因此,爲了把try-finally代碼塊添加到已有的字節碼中,我也必須執行類似的事務。

  下面是表現一個普通方法調用的字節碼,它被StatusManager更新環繞着:

0: ldc #2; //字符串消息
2: invokestatic #3; //方法StatusManager.push:(LString;)V
5: invokestatic #4; //方法 doSomething:()V
8: invokestatic #5; //方法 StatusManager.pop:()V
11: return

  下面是相同的方法調用,但是位於try-finally代碼塊中,因此,如果它產生了異常會調用StatusManager.pop():

0: ldc #2; //字符串消息
2: invokestatic #3; //方法 StatusManager.push:(LString;)V
5: invokestatic #4; //方法 doSomething:()V
8: invokestatic #5; //方法 StatusManager.pop:()V
11: goto 20
14: astore_0
15: invokestatic #5; //方法 StatusManager.pop:()V
18: aload_0
19: athrow
20: return

Exception table:
from to target type
5 8 14 any
14 15 14 any

  你可以發現,爲了實現一個try-finally,我必須複製一些指令,並添加了幾個跳轉和異常表記錄。幸運的是,BCEL的InstructionList類使這種工作相當簡單。

  在運行時重構字節碼

  現在我擁有了一個基於註解修改類的接口和該接口的具體實現了,下一步是編寫調用它的實際框架組件。實際上我將編寫少量的框架組件,先從運行時重構所有類的框架組件開始。由於這種操作會在build過程中發生,我決定爲它定義一個Ant事務。build.xml文件中的重構目標的聲明應該如下:

<instrument class="com.pkg.OurInstrumentor">
<fileset dir="$(classes.dir)">
<include name="**/*.class"/>
</fileset>
</instrument>

  爲了實現這種事務,我必須定義一個實現org.apache.tools.ant.Task接口的類。我們的事務的屬性和子元素(sub-elements)都是通過set和add方法調用傳遞進來的。我們調用執行(execute)方法來實現事務所要執行的工作--在示例中,就是重構<fileset>中指定的類文件。

public class InstrumentTask extends Task {
 ...
 public void setClass (String className) { ... }
 public void addFileSet (FileSet fileSet) { ... }

 public void execute () throws BuildException {
  Instrumentor inst = getInstrumentor();

  try {
   DirectoryScanner ds =fileSet.getDirectoryScanner(project);
   // Java 1.5 的"for" 語法
   for (String file : ds.getIncludedFiles()) {
    instrumentFile(inst, file);
   }
  } catch (Exception ex) {
   throw new BuildException(ex);
  }
 }
 ...
}

  用於該項操作的BCEL 5.1版本有一個問題--它不支持分析註解。我可以載入正在重構的類並使用反射(reflection)查看註解。但是,如果這樣,我就不得不使用RetentionPolicy.RUNTIME來代替RetentionPolicy.CLASS。我還必須在這些類中執行一些靜態的初始化,而這些操作可能載入本地類庫或引入其它的依賴關係。幸運的是,BCEL提供了一種插件(plugin)機制,它允許客戶端分析字節碼屬性。我編寫了自己的AttributeReader的實現(implementation),在出現註解的時候,它知道如何分析插入字節碼中的RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations屬性。BCEL未來的版本應該會包含這種功能而不是作爲插件提供。

  編譯時刻的字節碼重構方法顯示在示例代碼的code/02_compiletime目錄中。

  但是這種方法有很多缺陷。首先,我必須給建立過程增加額外的步驟。我不能基於命令行設置或其它編譯時沒有提供的信息來決定打開或關閉重構操作。如果重構的或沒有重構的代碼需要同時在產品環境中運行,那麼就必須建立兩個單獨的.jars文件,而且還必須決定使用哪一個。
在類載入時重構字節碼

  更好的方法可能是延遲字節碼重構操作,直到字節碼被載入的時候才進行重構。使用這種方法的時候,重構的字節碼不用保存起來。我們的應用程序啓動時刻的性能可能會受到影響,但是你卻可以基於自己的系統屬性或運行時配置數據來控制進行什麼操作。

  Java 1.5之前,我們使用定製的類載入程序可能實現這種類文件維護操作。但是Java 1.5中新增加的java.lang.instrument程序包提供了少數附加的工具。特別地,它定義了ClassFileTransformer的概念,在標準的載入過程中我們可以使用它來重構一個類。

  爲了在適當的時候(在載入任何類之前)註冊ClassFileTransformer,我需要定義一個premain方法。Java在載入主類(main class)之前將調用這個方法,並且它傳遞進來對Instrumentation對象的引用。我還必須給命令行增加-javaagent參數選項,告訴Java我們的premain方法的信息。這個參數選項把我們的agent class(代理類,它包含了premain方法)的全名和任意字符串作爲參數。在例子中我們把Instrumentor類的全名作爲參數(它必須在同一行之中):

-javaagent:boxpeeking.instrument.InstrumentorAdaptor=
boxpeeking.status.instrument.StatusInstrumentor

  現在我已經安排了一個回調(callback),它在載入任何含有註解的類之前都會發生,並且我擁有Instrumentation對象的引用,可以註冊我們的ClassFileTransformer了:

public static void premain (String className,
Instrumentation i)
throws ClassNotFoundException,
InstantiationException,
IllegalAccessException
{
 Class instClass = Class.forName(className);
 Instrumentor inst = (Instrumentor)instClass.newInstance();
 i.addTransformer(new InstrumentorAdaptor(inst));
}

  我們在此處註冊的適配器將充當上面給出的Instrumentor接口和Java的ClassFileTransformer接口之間的橋樑。

public class InstrumentorAdaptor
implements ClassFileTransformer
{
 public byte[] transform (ClassLoader cl,String className,Class classBeingRedefined,
ProtectionDomain protectionDomain,byte[] classfileBuffer)
 {
  try {
   ClassParser cp =new ClassParser(new ByteArrayInputStream(classfileBuffer),className + ".java");
   JavaClass jc = cp.parse();

   ClassGen cg = new ClassGen(jc);

   for (Annotation an : getAnnotations(jc.getAttributes())) {
    instrumentor.instrumentClass(cg, an);
   }

   for (org.apache.bcel.classfile.Method m : cg.getMethods()) {
    for (Annotation an : getAnnotations(m.getAttributes())) {
     ConstantPoolGen cpg =cg.getConstantPool();
     MethodGen mg =new MethodGen(m, className, cpg);
     instrumentor.instrumentMethod(cg, mg, an);
     mg.setMaxStack();
     mg.setMaxLocals();
     cg.replaceMethod(m, mg.getMethod());
    }
   }
   JavaClass jcNew = cg.getJavaClass();
   return jcNew.getBytes();
  } catch (Exception ex) {
   throw new RuntimeException("instrumenting " + className, ex);
  }
 }
 ...
}

  這種在啓動時重構字節碼的方法位於在示例的/code/03_startup目錄中。

  異常的處理

  文章前面提到,我希望編寫附加的代碼使用不同目的的@Status註解。我們來考慮一下一些額外的需求:我們的應用程序必須捕捉所有的未處理異常並把它們顯示給用戶。但是,我們不是提供Java堆棧跟蹤,而是顯示擁有@Status註解的方法,而且還不應該顯示任何代碼(類或方法的名稱或行號等等)。

  例如,考慮下面的堆棧跟蹤信息:

java.lang.RuntimeException: Could not load data for symbol IBM
at boxpeeking.code.YourCode.loadData(Unknown Source)
at boxpeeking.code.YourCode.go(Unknown Source)
at boxpeeking.yourcode.ui.Main+2.run(Unknown Source)
at java.lang.Thread.run(Thread.java:566)
Caused by: java.lang.RuntimeException: Timed out
at boxpeeking.code.YourCode.connectToDB(Unknown Source)
... 更多信息

  這將導致圖1中所示的GUI彈出框,上面的例子假設你的YourCode.loadData()、YourCode.go()和YourCode.connectToDB()都含有@Status註解。請注意,異常的次序是相反的,因此用戶最先得到的是最詳細的信息。

           221786.gif
           圖3.顯示在錯誤對話框中的堆棧跟蹤信息

  爲了實現這些功能,我必須對已有的代碼進行稍微的修改。首先,爲了確保在運行時@Status註解是可以看到的,我就必須再次更新@Retention,把它設置爲@Retention(RetentionPolicy.RUNTIME)。請記住,@Retention控制着JVM什麼時候拋棄註解信息。這樣的設置意味着註解不僅可以被編譯器插入字節碼中,還能夠使用新的Method.getAnnotation(Class)方法通過反射來進行訪問。

  現在我需要安排接收代碼中沒有明確處理的任何異常的通知了。在Java 1.4中,處理任何特定線程上未處理異常的最好方法是使用ThreadGroup子類並給該類型的ThreadGroup添加自己的新線程。但是Java 1.5提供了額外的功能。我可以定義UncaughtExceptionHandler接口的一個實例,併爲任何特定的線程(或所有線程)註冊它。

  請注意,在例子中爲特定異常註冊可能更好,但是在Java 1.5.0beta1(#4986764)中有一個bug,它使這樣操作無法進行。但是爲所有線程設置一個處理程序是可以工作的,因此我就這樣操作了。

  現在我們擁有了一種截取未處理異常的方法了,並且這些異常必須被報告給用戶。在GUI應用程序中,典型情況下這樣的操作是通過彈出一個包含整個堆棧跟蹤信息或簡單消息的模式對話框來實現的。在例子中,我希望在產生異常的時候顯示一個消息,但是我希望提供堆棧的@Status描述而不是類和方法的名稱。爲了實現這個目的,我簡單地在Thread的StackTraceElement數組中查詢,找到與每個框架相關的java.lang.reflect.Method對象,並查詢它的堆棧註解列表。不幸的是,它只提供了方法的名稱,沒有提供方法的特徵量(signature),因此這種技術不支持名稱相同的(但@Status註解不同的)重載方法。

  實現這種方法的示例代碼可以在peekinginside-pt2.tar.gz文件的/code/04_exceptions目錄中找到。
 取樣(Sampling)

  我現在有辦法把StackTraceElement數組轉換爲@Status註解堆棧。這種操作比表明看到的更加有用。Java 1.5中的另一個新特性--線程反省(introspection)--使我們能夠從當前正在運行的線程中得到準確的StackTraceElement數組。有了這兩部分信息之後,我們就可以構造JstatusBar的另一種實現。StatusManager將不會在發生方法調用的時候接收通知,而是簡單地啓動一個附加的線程,讓它負責在正常的間隔期間抓取堆棧跟蹤信息和每個步驟的狀態。只要這個間隔期間足夠短,用戶就不會感覺到更新的延遲。

  下面使"sampler"線程背後的代碼,它跟蹤另一個線程的經過:

class StatusSampler implements Runnable
{
 private Thread watchThread;

 public StatusSampler (Thread watchThread)
 {
  this.watchThread = watchThread;
 }

 public void run ()
 {
  while (watchThread.isAlive()) {
   // 從線程中得到堆棧跟蹤信息
   StackTraceElement[] stackTrace =watchThread.getStackTrace();
   // 從堆棧跟蹤信息中提取狀態消息
   List<Status> statusList =StatusFinder.getStatus(stackTrace);
   Collections.reverse(statusList);
   // 用狀態消息建立某種狀態
   StatusState state = new StatusState();
   for (Status s : statusList) {
    String message = s.value();
    state.push(message);
   }

   // 更新當前的狀態
   StatusManager.setState(watchThread,state);
   //休眠到下一個週期
   try {
    Thread .sleep(SAMPLING_DELAY);
   } catch (InterruptedException ex) {}
  }

  //狀態復位
  StatusManager.setState(watchThread,new StatusState());
 }
}

  與增加方法調用、手動或通過重構相比,取樣對程序的侵害性(invasive)更小。我根本不需要改變建立過程或命令行參數,或修改啓動過程。它也允許我通過調整SAMPLING_DELAY來控制佔用的開銷。不幸的是,當方法調用開始或結束的時候,這種方法沒有明確的回調。除了狀態更新的延遲之外,沒有原因要求這段代碼在那個時候接收回調。但是,未來我能夠增加一些額外的代碼來跟蹤每個方法的準確的運行時。通過檢查StackTraceElement是可以精確地實現這樣的操作的。

  通過線程取樣實現JStatusBar的代碼可以在peekinginside-pt2.tar.gz文件的/code/05_sampling目錄中找到。

  在執行過程中重構字節碼

  通過把取樣的方法與重構組合在一起,我能夠形成一種最終的實現,它提供了各種方法的最佳特性。默認情況下可以使用取樣,但是應用程序的花費時間最多的方法可以被個別地進行重構。這種實現根本不會安裝ClassTransformer,但是作爲代替,它會一次一個地重構方法以響應取樣過程中收集到的數據。

  爲了實現這種功能,我將建立一個新類InstrumentationManager,它可以用於重構和不重構獨立的方法。它可以使用新的Instrumentation.redefineClasses方法來修改空閒的類,同時代碼則可以不間斷執行。前面部分中增加的StatusSampler線程現在有了額外的職責,它把任何自己"發現"的@Status方法添加到集合中。它將週期性地找出最壞的冒犯者並把它們提供給InstrumentationManager以供重構。這允許應用程序更加精確地跟蹤每個方法的啓動和終止時刻。

  前面提到的取樣方法的一個問題是它不能區分長時間運行的方法與在循環中多次調用的方法。由於重構會給每次方法調用增加一定的開銷,我們有必要忽略頻繁調用的方法。幸運的是,我們可以使用重構解決這個問題。除了簡單地更新StatusManager之外,我們將維護每個重構的方法被調用的次數。如果這個數值超過了某個極限(意味着維護這個方法的信息的開銷太大了),取樣線程將會永遠地取消對該方法的重構。

  理想情況下,我將把每個方法的調用數量存儲在重構過程中添加到類的新字段中。不幸的是,Java 1.5中增加的類轉換機制不允許這樣操作;它不能增加或刪除任何字段。作爲代替,我將把這些信息存儲在新的CallCounter類的Method對象的靜態映射中。

  這種混合的方法可以在示例代碼的/code/06_dynamic目錄中找到。

  概括

  圖4提供了一個矩形,它顯示了我給出的例子相關的特性和代價。

221787.gif
圖4.重構方法的分析

  你可以發現,動態的(Dynamic)方法是各種方案的良好組合。與使用重構的所有示例類似,它提供了方法開始或終止時刻的明確的回調,因此你的應用程序可以準確地跟蹤運行時並立即爲用戶提供反饋信息。但是,它還能夠取消某種方法的重構(它被過於頻繁地調用),因此它不會受到其它的重構方案遇到的性能問題的影響。它沒有包含編譯時步驟,並且它沒有增加類載入過程中的額外的工作。

  未來的趨勢

  我們可以給這個項目增加大量的附件特性,使它更加適用。其中最有用的特性可能是動態的狀態信息。我們可以使用新的java.util.Formatter類把類似printf的模式替換(pattern substitution)用於@Status消息中。例如,我們的connectToDB(String url)方法中的@Status("Connecting to %s")註解可以把URL作爲消息的一部分報告給用戶。

  在源代碼重構的幫助下,這可能顯得微不足道,因爲我將使用的Formatter.format方法使用了可變參數(Java 1.5中增加的"魔術"功能)。重構過的版本類似下面的情形:

public void connectToDB (String url) {
 Formatter f = new Formatter();
 String message = f.format("Connecting to %s", url);

 StatusManager.push(message);
 try {
  ...
 } finally {
  StatusManager.pop();
 }
}

  不幸的是,這種"魔術"功能是完全在編譯器中實現的。在字節碼中,Formatter.format把Object[]作爲參數,編譯器明確地添加代碼來包裝每個原始的類型並裝配該數組。如果BCEL沒有加緊彌補,而我又需要使用字節碼重構,我將不得不重新實現這種邏輯。

  由於它只能用於重構(這種情況下方法參數是可用的)而不能用於取樣,你可能希望在啓動的時候重構這些方法,或最少使動態實現偏向於任何方法的重構,還可以在消息中使用替代模式。

  你還可以跟蹤每個重構的方法調用的啓動次數,因此你還可以更加精確地報告每個方法的運行次數。你甚至於可以保存這些次數的歷史統計數據,並使用它們形成一個真正的進度條(代替我使用的不確定的版本)。這種能力將賦予你在運行時重構某種方法的一個很好的理由,因爲跟蹤任何獨立的方法的開銷都是很能很明顯的。

  你可以給進度條增加"調試"模式,它不管方法調用是否包含@Status註解,報告取樣過程中出現的所有方法調用。這對於任何希望調試死鎖或性能問題的開發者來說都是無價之寶。實際上,Java 1.5還爲死鎖(deadlock)檢測提供了一個可編程的API,在應用程序鎖住的時候,我們可以使用該API把進程條變成紅色。

  本文中建立的基於註解的重構框架組件可能很有市場。一個允許字節碼在編譯時(通過Ant事務)、啓動時(使用ClassTransformer)和執行過程中(使用Instrumentation)進行重構的工具對於少量其它新項目來說毫無疑問地非常有價值。

  總結

  在這幾個例子中你可以看到,元數據編程(meta-programming)可能是一種非常強大的技術。報告長時間運行的操作的進程僅僅是這種技術的應用之一,而我們的JStatusBar僅僅是溝通這些信息的一種媒介。我們可以看到,Java 1.5中提供的很多新特性爲元數據編程提供了增強的支持。特別地,把註解和運行時重構組合在一起爲面向屬性的編程提供了真正動態的形式。我們可以進一步使用這些技術,使它的功能超越已有的框架組件(例如XDoclet提供的框架組件的功能)。
發佈了115 篇原創文章 · 獲贊 2 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章