明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

體驗(yàn)Java 1.5中面向(AOP)編程

[摘要]對(duì)于一個(gè)能夠訪問(wèn)源代碼的經(jīng)驗(yàn)豐富的Java開發(fā)人員來(lái)說(shuō),任何程序都可以被看作是博物館里透明的模型。類似線程轉(zhuǎn)儲(chǔ)(dump)、方法調(diào)用跟蹤、斷點(diǎn)、切面(profiling)統(tǒng)計(jì)表等工具可以讓我們了解程序目前正在執(zhí)行什么操作、剛才做了什么操作、未來(lái)將做什么操作。但是在產(chǎn)品環(huán)境中情況就沒(méi)有那么明顯了,這...
對(duì)于一個(gè)能夠訪問(wèn)源代碼的經(jīng)驗(yàn)豐富的Java開發(fā)人員來(lái)說(shuō),任何程序都可以被看作是博物館里透明的模型。類似線程轉(zhuǎn)儲(chǔ)(dump)、方法調(diào)用跟蹤、斷點(diǎn)、切面(profiling)統(tǒng)計(jì)表等工具可以讓我們了解程序目前正在執(zhí)行什么操作、剛才做了什么操作、未來(lái)將做什么操作。但是在產(chǎn)品環(huán)境中情況就沒(méi)有那么明顯了,這些工具一般是不能夠使用的,或最多只能由受過(guò)訓(xùn)練的開發(fā)者使用。支持團(tuán)隊(duì)和最終用戶也需要知道在某個(gè)時(shí)刻應(yīng)用程序正在執(zhí)行什么操作。

  為了填補(bǔ)這個(gè)空缺,我們已經(jīng)發(fā)明了一些簡(jiǎn)單的替代品,例如日志文件(典型情況下用于服務(wù)器處理)和狀態(tài)條(用于GUI應(yīng)用程序)。但是,由于這些工具只能捕捉和報(bào)告可用信息的一個(gè)很小的子集,并且通常必須把這些信息用容易理解的方式表現(xiàn)出來(lái),所以程序員趨向于把它們明確地編寫到應(yīng)用程序中。而這些代碼會(huì)纏繞著應(yīng)用程序的業(yè)務(wù)邏輯,當(dāng)開發(fā)者試圖調(diào)試或了解核心功能的時(shí)候,他們必須"圍繞這些代碼工作",而且還要記得功能發(fā)生改變后更新這些代碼。我們希望實(shí)現(xiàn)的真正功能是把狀態(tài)報(bào)告集中在某個(gè)位置,把單個(gè)狀態(tài)消息作為元數(shù)據(jù)(metadata)來(lái)管理。

  在本文中我將考慮使用嵌入GUI應(yīng)用程序中的狀態(tài)條組件的情形。我將介紹多種實(shí)現(xiàn)這種狀態(tài)報(bào)告的不同方法,從傳統(tǒng)的硬編碼習(xí)慣開始。隨后我會(huì)介紹Java 1.5的大量新特性,包括注解(annotation)和運(yùn)行時(shí)字節(jié)碼重構(gòu)(instrumentation)。

  狀態(tài)管理器(StatusManager)

  我的主要目標(biāo)是建立一個(gè)可以嵌入GUI應(yīng)用程序的JStatusBar Swing組件。圖1顯示了一個(gè)簡(jiǎn)單的Jframe中狀態(tài)條的樣式。
  
  
圖1.我們動(dòng)態(tài)生成的狀態(tài)條

  由于我不希望直接在業(yè)務(wù)邏輯中引用任何GUI組件,我將建立一個(gè)StatusManager(狀態(tài)管理器)來(lái)充當(dāng)狀態(tài)更新的入口點(diǎn)。實(shí)際的通知會(huì)被委托給StatusState對(duì)象,因此以后可以擴(kuò)展它以支持多個(gè)并發(fā)的線程。圖2顯示了這種安排。
  

圖2. StatusManager和JstatusBar

  現(xiàn)在我必須編寫代碼調(diào)用StatusManager的方法來(lái)報(bào)告應(yīng)用程序的進(jìn)程。典型情況下,這些方法調(diào)用都分散地貫穿于try-finally代碼塊中,通常每個(gè)方法一個(gè)調(diào)用。   

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

  這些代碼實(shí)現(xiàn)了我們所需要功能,但是在代碼庫(kù)中數(shù)十次、甚至于數(shù)百次地復(fù)制這些代碼之后,它看起來(lái)就有些混亂了。此外,如果我們希望用一些其它的方式訪問(wèn)這些消息該怎么辦呢?在本文的后面部分中,我將定義一個(gè)用戶友好的異常處理程序,它共享了相同的消息。問(wèn)題是我把狀態(tài)消息隱藏在方法的實(shí)現(xiàn)之中了,而沒(méi)有把消息放在消息所屬的接口中。

  面向?qū)傩跃幊?/b>

  我真正想實(shí)現(xiàn)的操作是把對(duì)StatusManager的引用都放到代碼外面的某個(gè)地方,并簡(jiǎn)單地用我們的消息標(biāo)記這個(gè)方法。接著我可以使用代碼生成(code-generation)或運(yùn)行時(shí)反。╥ntrospection)來(lái)執(zhí)行真正的工作。XDoclet項(xiàng)目把這種方法歸納為面向?qū)傩跃幊蹋ˋttribute-Oriented Programming),它還提供了一個(gè)框架組件,可以把自定義的類似Javadoc的標(biāo)記轉(zhuǎn)換到源代碼之中。

  但是,JSR-175包含了這樣的內(nèi)容,Java 1.5為了包含真實(shí)代碼中的這些屬性提供了一種結(jié)構(gòu)化程度更高的格式。這些屬性被稱為"注解(annotations)",我們可以使用它們?yōu)轭、方法、字段或變量定義提供元數(shù)據(jù)。它們必須被顯式聲明,并提供一組可以包含任意常量值(包括原語(yǔ)、字符串、枚舉和類)的名稱-值對(duì)(name-value pair)。

  注解(Annotations)

  為了處理狀態(tài)消息,我希望定義一個(gè)包含字符串值的新注解。注解的定義非常類似接口的定義,但是它用@interface關(guān)鍵字代替了interface,并且只支持方法(盡管它們的功能更像字段):   

  public @interface Status {
   String value();
  }

  與接口類似,我把@interface放入一個(gè)叫做Status.java的文件中,并把它導(dǎo)入到任何需要引用它的文件中。

  對(duì)我們的字段來(lái)說(shuō),value可能是個(gè)奇怪的名稱。類似message的名稱可能更適合;但是,value對(duì)于Java來(lái)說(shuō)具有特殊的意義。它允許我們使用@Status("...")代替@Status(value="...")來(lái)定義注解,這明顯更加簡(jiǎn)捷。

  我現(xiàn)在可以使用下面的代碼定義自己的方法:

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

  請(qǐng)注意,我們?cè)诰幾g這段代碼的時(shí)候必須使用-source 1.5選項(xiàng)。如果你使用Ant而不是直接使用javac命令行建立應(yīng)用程序,那么你需要使用Ant 1.6.1以上版本。

  作為類、方法、字段和變量的補(bǔ)充,注解也可以用于為其它的注解提供元數(shù)據(jù)。特別地,Java引入了少量注解,你可以使用這些注解來(lái)定制你自己的注解的工作方式。我們用下面的代碼重新定義自己的注解:

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

  @Target注解定義了@Status注解可以引用什么內(nèi)容。理想情況下,我希望標(biāo)記大塊的代碼,但是它的選項(xiàng)只有方法、字段、類、本地變量、參數(shù)和其它注解。我只對(duì)代碼感興趣,因此我選擇了METHOD(方法)。

  @Retention注解允許我們指定Java什么時(shí)候可以自主地拋棄消息。它可能是SOURCE(在編譯時(shí)拋棄)、CLASS(在類載入時(shí)拋棄)或RUNTIME(不拋棄)。我們先選擇SOURCE,但是在本文后部我們會(huì)更新它。

    重構(gòu)源代碼

  現(xiàn)在我的消息都被編碼放入元數(shù)據(jù)中了,我必須編寫一些代碼來(lái)通知狀態(tài)監(jiān)聽程序。假設(shè)在某個(gè)時(shí)候,我繼續(xù)把connectToDB方法保存源代碼控件中,但是卻沒(méi)有對(duì)StatusManager的任何引用。但是,在編譯這個(gè)類之前,我希望加入一些必要的調(diào)用。也就是說(shuō),我希望自動(dòng)地插入try-finally語(yǔ)句和push/pop調(diào)用。

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

  看起來(lái)沒(méi)有什么可以用于Java代碼的源代碼重構(gòu)的很好的工具。這類工具可能有市場(chǎng),但是你在本文的后面部分可以看到,字節(jié)碼重構(gòu)可能是一種更強(qiáng)大的技術(shù)。 重構(gòu)字節(jié)碼

  不是重構(gòu)源代碼然后編譯它,而是編譯原始的源代碼,然后重構(gòu)它所產(chǎn)生的字節(jié)碼。這樣的操作可能比源代碼重構(gòu)更容易,也可能更加復(fù)雜,而這依賴于需要的準(zhǔn)確轉(zhuǎn)換。字節(jié)碼重構(gòu)的主要優(yōu)點(diǎn)是代碼可以在運(yùn)行時(shí)被修改,不需要使用編譯器。

  盡管Java的字節(jié)碼格式相對(duì)簡(jiǎn)單,我還是希望使用一個(gè)Java類庫(kù)來(lái)執(zhí)行字節(jié)碼的分析和生成(這可以把我們與未來(lái)Java類文件格式的改變隔離開來(lái))。我選擇了使用Jakarta的Byte Code Engineering Library(字節(jié)碼引擎類庫(kù),BCEL),但是我還可以選用CGLIB、ASM或SERP。

  由于我將使用多種不同的方式重構(gòu)字節(jié)碼,我將從聲明重構(gòu)的通用接口開始。它類似于執(zhí)行基于注解重構(gòu)的簡(jiǎn)單框架組件。這個(gè)框架組件基于注解,將支持類和方法的轉(zhuǎn)換,因此該接口有類似下面的定義:  

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

  ClassGen和MethodGen都是BCEL類,它們使用了Builder模式(pattern)。也就是說(shuō),它們?yōu)楦淖兤渌豢勺兊模╥mmutable)對(duì)象、以及可變的和不可變的表現(xiàn)(representation)之間的轉(zhuǎn)換提供了方法。

  現(xiàn)在我需要為接口編寫實(shí)現(xiàn),它必須用恰當(dāng)?shù)腟tatusManager調(diào)用更換@Status注解。前面提到,我希望把這些調(diào)用包含在try-finally代碼塊中。請(qǐng)注意,要達(dá)到這個(gè)目標(biāo),我們所使用的注解必須用@Retention(RetentionPolicy.CLASS)進(jìn)行標(biāo)記,它指示Java編譯器在編譯過(guò)程中不要拋棄注解。由于在前面我把@Status聲明為@Retention(RetentionPolicy.SOURCE)的,我必須更新它。

  在這種情況下,重構(gòu)字節(jié)碼明顯比重構(gòu)源代碼更復(fù)雜。其原因在于try-finally是一種僅僅存在于源代碼中的概念。Java編譯器把try-finally代碼塊轉(zhuǎn)換為一系列的try-catch代碼塊,并在每一個(gè)返回之前插入對(duì)finally代碼塊的調(diào)用。因此,為了把try-finally代碼塊添加到已有的字節(jié)碼中,我也必須執(zhí)行類似的事務(wù)。

  下面是表現(xiàn)一個(gè)普通方法調(diào)用的字節(jié)碼,它被StatusManager更新環(huán)繞著:  

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

  下面是相同的方法調(diào)用,但是位于try-finally代碼塊中,因此,如果它產(chǎn)生了異常會(huì)調(diào)用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

  你可以發(fā)現(xiàn),為了實(shí)現(xiàn)一個(gè)try-finally,我必須復(fù)制一些指令,并添加了幾個(gè)跳轉(zhuǎn)和異常表記錄。幸運(yùn)的是,BCEL的InstructionList類使這種工作相當(dāng)簡(jiǎn)單。

  在運(yùn)行時(shí)重構(gòu)字節(jié)碼

  現(xiàn)在我擁有了一個(gè)基于注解修改類的接口和該接口的具體實(shí)現(xiàn)了,下一步是編寫調(diào)用它的實(shí)際框架組件。實(shí)際上我將編寫少量的框架組件,先從運(yùn)行時(shí)重構(gòu)所有類的框架組件開始。由于這種操作會(huì)在build過(guò)程中發(fā)生,我決定為它定義一個(gè)Ant事務(wù)。build.xml文件中的重構(gòu)目標(biāo)的聲明應(yīng)該如下:  

 。糹nstrument class="com.pkg.OurInstrumentor">
  <fileset dir="$(classes.dir)">
 。糹nclude name="**/*.class"/>
  </fileset>
 。/instrument>

  為了實(shí)現(xiàn)這種事務(wù),我必須定義一個(gè)實(shí)現(xiàn)org.apache.tools.ant.Task接口的類。我們的事務(wù)的屬性和子元素(sub-elements)都是通過(guò)set和add方法調(diào)用傳遞進(jìn)來(lái)的。我們調(diào)用執(zhí)行(execute)方法來(lái)實(shí)現(xiàn)事務(wù)所要執(zhí)行的工作--在示例中,就是重構(gòu)<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" 語(yǔ)法
     for (String file : ds.getIncludedFiles()) {
      instrumentFile(inst, file);
     }
    } catch (Exception ex) {
     throw new BuildException(ex);
    }
   }
   ...
  }

  用于該項(xiàng)操作的BCEL 5.1版本有一個(gè)問(wèn)題--它不支持分析注解。我可以載入正在重構(gòu)的類并使用反射(reflection)查看注解。但是,如果這樣,我就不得不使用RetentionPolicy.RUNTIME來(lái)代替RetentionPolicy.CLASS。我還必須在這些類中執(zhí)行一些靜態(tài)的初始化,而這些操作可能載入本地類庫(kù)或引入其它的依賴關(guān)系。幸運(yùn)的是,BCEL提供了一種插件(plugin)機(jī)制,它允許客戶端分析字節(jié)碼屬性。我編寫了自己的AttributeReader的實(shí)現(xiàn)(implementation),在出現(xiàn)注解的時(shí)候,它知道如何分析插入字節(jié)碼中的RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations屬性。BCEL未來(lái)的版本應(yīng)該會(huì)包含這種功能而不是作為插件提供。

  編譯時(shí)刻的字節(jié)碼重構(gòu)方法顯示在示例代碼的code/02_compiletime目錄中。

  但是這種方法有很多缺陷。首先,我必須給建立過(guò)程增加額外的步驟。我不能基于命令行設(shè)置或其它編譯時(shí)沒(méi)有提供的信息來(lái)決定打開或關(guān)閉重構(gòu)操作。如果重構(gòu)的或沒(méi)有重構(gòu)的代碼需要同時(shí)在產(chǎn)品環(huán)境中運(yùn)行,那么就必須建立兩個(gè)單獨(dú)的.jars文件,而且還必須決定使用哪一個(gè)。

    在類載入時(shí)重構(gòu)字節(jié)碼

  更好的方法可能是延遲字節(jié)碼重構(gòu)操作,直到字節(jié)碼被載入的時(shí)候才進(jìn)行重構(gòu)。使用這種方法的時(shí)候,重構(gòu)的字節(jié)碼不用保存起來(lái)。我們的應(yīng)用程序啟動(dòng)時(shí)刻的性能可能會(huì)受到影響,但是你卻可以基于自己的系統(tǒng)屬性或運(yùn)行時(shí)配置數(shù)據(jù)來(lái)控制進(jìn)行什么操作。

  Java 1.5之前,我們使用定制的類載入程序可能實(shí)現(xiàn)這種類文件維護(hù)操作。但是Java 1.5中新增加的java.lang.instrument程序包提供了少數(shù)附加的工具。特別地,它定義了ClassFileTransformer的概念,在標(biāo)準(zhǔn)的載入過(guò)程中我們可以使用它來(lái)重構(gòu)一個(gè)類。

  為了在適當(dāng)?shù)臅r(shí)候(在載入任何類之前)注冊(cè)ClassFileTransformer,我需要定義一個(gè)premain方法。Java在載入主類(main class)之前將調(diào)用這個(gè)方法,并且它傳遞進(jìn)來(lái)對(duì)Instrumentation對(duì)象的引用。我還必須給命令行增加-javaagent參數(shù)選項(xiàng),告訴Java我們的premain方法的信息。這個(gè)參數(shù)選項(xiàng)把我們的agent class(代理類,它包含了premain方法)的全名和任意字符串作為參數(shù)。在例子中我們把Instrumentor類的全名作為參數(shù)(它必須在同一行之中):

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

  現(xiàn)在我已經(jīng)安排了一個(gè)回調(diào)(callback),它在載入任何含有注解的類之前都會(huì)發(fā)生,并且我擁有Instrumentation對(duì)象的引用,可以注冊(cè)我們的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));
  }

  我們?cè)诖颂幾?cè)的適配器將充當(dāng)上面給出的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);
    }
   }
   ...
  }

  這種在啟動(dòng)時(shí)重構(gòu)字節(jié)碼的方法位于在示例的/code/03_startup目錄中。

  異常的處理

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

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

  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)

  ... 更多信息

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


  
圖3.顯示在錯(cuò)誤對(duì)話框中的堆棧跟蹤信息

  為了實(shí)現(xiàn)這些功能,我必須對(duì)已有的代碼進(jìn)行稍微的修改。首先,為了確保在運(yùn)行時(shí)@Status注解是可以看到的,我就必須再次更新@Retention,把它設(shè)置為@Retention(RetentionPolicy.RUNTIME)。請(qǐng)記住,@Retention控制著JVM什么時(shí)候拋棄注解信息。這樣的設(shè)置意味著注解不僅可以被編譯器插入字節(jié)碼中,還能夠使用新的Method.getAnnotation(Class)方法通過(guò)反射來(lái)進(jìn)行訪問(wèn)。

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

  請(qǐng)注意,在例子中為特定異常注冊(cè)可能更好,但是在Java 1.5.0beta1(#4986764)中有一個(gè)bug,它使這樣操作無(wú)法進(jìn)行。但是為所有線程設(shè)置一個(gè)處理程序是可以工作的,因此我就這樣操作了。

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

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

    取樣(Sampling)

  我現(xiàn)在有辦法把StackTraceElement數(shù)組轉(zhuǎn)換為@Status注解堆棧。這種操作比表明看到的更加有用。Java 1.5中的另一個(gè)新特性--線程反。╥ntrospection)--使我們能夠從當(dāng)前正在運(yùn)行的線程中得到準(zhǔn)確的StackTraceElement數(shù)組。有了這兩部分信息之后,我們就可以構(gòu)造JstatusBar的另一種實(shí)現(xiàn)。StatusManager將不會(huì)在發(fā)生方法調(diào)用的時(shí)候接收通知,而是簡(jiǎn)單地啟動(dòng)一個(gè)附加的線程,讓它負(fù)責(zé)在正常的間隔期間抓取堆棧跟蹤信息和每個(gè)步驟的狀態(tài)。只要這個(gè)間隔期間足夠短,用戶就不會(huì)感覺(jué)到更新的延遲。

  下面使"sampler"線程背后的代碼,它跟蹤另一個(gè)線程的經(jīng)過(guò):

  class StatusSampler implements Runnable
  {
   private Thread watchThread;  

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

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

   // 更新當(dāng)前的狀態(tài)

     StatusManager.setState(watchThread,state);

     //休眠到下一個(gè)周期

     try {
      Thread .sleep(SAMPLING_DELAY);
     } catch (InterruptedException ex) {}
    }
  //狀態(tài)復(fù)位
    StatusManager.setState(watchThread,new StatusState());
   }
  }

  與增加方法調(diào)用、手動(dòng)或通過(guò)重構(gòu)相比,取樣對(duì)程序的侵害性(invasive)更小。我根本不需要改變建立過(guò)程或命令行參數(shù),或修改啟動(dòng)過(guò)程。它也允許我通過(guò)調(diào)整SAMPLING_DELAY來(lái)控制占用的開銷。不幸的是,當(dāng)方法調(diào)用開始或結(jié)束的時(shí)候,這種方法沒(méi)有明確的回調(diào)。除了狀態(tài)更新的延遲之外,沒(méi)有原因要求這段代碼在那個(gè)時(shí)候接收回調(diào)。但是,未來(lái)我能夠增加一些額外的代碼來(lái)跟蹤每個(gè)方法的準(zhǔn)確的運(yùn)行時(shí)。通過(guò)檢查StackTraceElement是可以精確地實(shí)現(xiàn)這樣的操作的。

  通過(guò)線程取樣實(shí)現(xiàn)JStatusBar的代碼可以在peekinginside-pt2.tar.gz文件的/code/05_sampling目錄中找到。

  在執(zhí)行過(guò)程中重構(gòu)字節(jié)碼

  通過(guò)把取樣的方法與重構(gòu)組合在一起,我能夠形成一種最終的實(shí)現(xiàn),它提供了各種方法的最佳特性。默認(rèn)情況下可以使用取樣,但是應(yīng)用程序的花費(fèi)時(shí)間最多的方法可以被個(gè)別地進(jìn)行重構(gòu)。這種實(shí)現(xiàn)根本不會(huì)安裝ClassTransformer,但是作為代替,它會(huì)一次一個(gè)地重構(gòu)方法以響應(yīng)取樣過(guò)程中收集到的數(shù)據(jù)。

  為了實(shí)現(xiàn)這種功能,我將建立一個(gè)新類InstrumentationManager,它可以用于重構(gòu)和不重構(gòu)獨(dú)立的方法。它可以使用新的Instrumentation.redefineClasses方法來(lái)修改空閑的類,同時(shí)代碼則可以不間斷執(zhí)行。前面部分中增加的StatusSampler線程現(xiàn)在有了額外的職責(zé),它把任何自己"發(fā)現(xiàn)"的@Status方法添加到集合中。它將周期性地找出最壞的冒犯者并把它們提供給InstrumentationManager以供重構(gòu)。這允許應(yīng)用程序更加精確地跟蹤每個(gè)方法的啟動(dòng)和終止時(shí)刻。

  前面提到的取樣方法的一個(gè)問(wèn)題是它不能區(qū)分長(zhǎng)時(shí)間運(yùn)行的方法與在循環(huán)中多次調(diào)用的方法。由于重構(gòu)會(huì)給每次方法調(diào)用增加一定的開銷,我們有必要忽略頻繁調(diào)用的方法。幸運(yùn)的是,我們可以使用重構(gòu)解決這個(gè)問(wèn)題。除了簡(jiǎn)單地更新StatusManager之外,我們將維護(hù)每個(gè)重構(gòu)的方法被調(diào)用的次數(shù)。如果這個(gè)數(shù)值超過(guò)了某個(gè)極限(意味著維護(hù)這個(gè)方法的信息的開銷太大了),取樣線程將會(huì)永遠(yuǎn)地取消對(duì)該方法的重構(gòu)。

  理想情況下,我將把每個(gè)方法的調(diào)用數(shù)量存儲(chǔ)在重構(gòu)過(guò)程中添加到類的新字段中。不幸的是,Java 1.5中增加的類轉(zhuǎn)換機(jī)制不允許這樣操作;它不能增加或刪除任何字段。作為代替,我將把這些信息存儲(chǔ)在新的CallCounter類的Method對(duì)象的靜態(tài)映射中。

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

  概括

  圖4提供了一個(gè)矩形,它顯示了我給出的例子相關(guān)的特性和代價(jià)。   


  
圖4.重構(gòu)方法的分析

  你可以發(fā)現(xiàn),動(dòng)態(tài)的(Dynamic)方法是各種方案的良好組合。與使用重構(gòu)的所有示例類似,它提供了方法開始或終止時(shí)刻的明確的回調(diào),因此你的應(yīng)用程序可以準(zhǔn)確地跟蹤運(yùn)行時(shí)并立即為用戶提供反饋信息。但是,它還能夠取消某種方法的重構(gòu)(它被過(guò)于頻繁地調(diào)用),因此它不會(huì)受到其它的重構(gòu)方案遇到的性能問(wèn)題的影響。它沒(méi)有包含編譯時(shí)步驟,并且它沒(méi)有增加類載入過(guò)程中的額外的工作。

  未來(lái)的趨勢(shì)

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

  在源代碼重構(gòu)的幫助下,這可能顯得微不足道,因?yàn)槲覍⑹褂玫腇ormatter.format方法使用了可變參數(shù)(Java 1.5中增加的"魔術(shù)"功能)。重構(gòu)過(guò)的版本類似下面的情形:

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

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

  不幸的是,這種"魔術(shù)"功能是完全在編譯器中實(shí)現(xiàn)的。在字節(jié)碼中,F(xiàn)ormatter.format把Object[]作為參數(shù),編譯器明確地添加代碼來(lái)包裝每個(gè)原始的類型并裝配該數(shù)組。如果BCEL沒(méi)有加緊彌補(bǔ),而我又需要使用字節(jié)碼重構(gòu),我將不得不重新實(shí)現(xiàn)這種邏輯。

  由于它只能用于重構(gòu)(這種情況下方法參數(shù)是可用的)而不能用于取樣,你可能希望在啟動(dòng)的時(shí)候重構(gòu)這些方法,或最少使動(dòng)態(tài)實(shí)現(xiàn)偏向于任何方法的重構(gòu),還可以在消息中使用替代模式。

  你還可以跟蹤每個(gè)重構(gòu)的方法調(diào)用的啟動(dòng)次數(shù),因此你還可以更加精確地報(bào)告每個(gè)方法的運(yùn)行次數(shù)。你甚至于可以保存這些次數(shù)的歷史統(tǒng)計(jì)數(shù)據(jù),并使用它們形成一個(gè)真正的進(jìn)度條(代替我使用的不確定的版本)。這種能力將賦予你在運(yùn)行時(shí)重構(gòu)某種方法的一個(gè)很好的理由,因?yàn)楦櫲魏为?dú)立的方法的開銷都是很能很明顯的。

  你可以給進(jìn)度條增加"調(diào)試"模式,它不管方法調(diào)用是否包含@Status注解,報(bào)告取樣過(guò)程中出現(xiàn)的所有方法調(diào)用。這對(duì)于任何希望調(diào)試死鎖或性能問(wèn)題的開發(fā)者來(lái)說(shuō)都是無(wú)價(jià)之寶。實(shí)際上,Java 1.5還為死鎖(deadlock)檢測(cè)提供了一個(gè)可編程的API,在應(yīng)用程序鎖住的時(shí)候,我們可以使用該API把進(jìn)程條變成紅色。

  本文中建立的基于注解的重構(gòu)框架組件可能很有市場(chǎng)。一個(gè)允許字節(jié)碼在編譯時(shí)(通過(guò)Ant事務(wù))、啟

  動(dòng)時(shí)(使用ClassTransformer)和執(zhí)行過(guò)程中(使用Instrumentation)進(jìn)行重構(gòu)的工具對(duì)于少量其它新項(xiàng)目來(lái)說(shuō)毫無(wú)疑問(wèn)地非常有價(jià)值。

  總結(jié)

  在這幾個(gè)例子中你可以看到,元數(shù)據(jù)編程(meta-programming)可能是一種非常強(qiáng)大的技術(shù)。報(bào)告長(zhǎng)時(shí)間運(yùn)行的操作的進(jìn)程僅僅是這種技術(shù)的應(yīng)用之一,而我們的JStatusBar僅僅是溝通這些信息的一種媒介。我們可以看到,Java 1.5中提供的很多新特性為元數(shù)據(jù)編程提供了增強(qiáng)的支持。特別地,把注解和運(yùn)行時(shí)重構(gòu)組合在一起為面向?qū)傩缘木幊烫峁┝苏嬲齽?dòng)態(tài)的形式。我們可以進(jìn)一步使用這些技術(shù),使它的功能超越已有的框架組件(例如XDoclet提供的框架組件的功能)。