程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Classworking工具箱: 注釋(Annotation)與ASM

Classworking工具箱: 注釋(Annotation)與ASM

編輯:關於JAVA

到 J2SE 5.0,Sun 已經給 Java 平台添加了許多新特性。最為重要的一個新特性是支持注釋。注釋在關聯多種類型的元數據與 Java 代碼方面將會很有用,並且在擴展 Java 平台的新的和更新的 JSR 中,它已經被廣泛用來代替定制配置文件。在本文中,我將向您展示如何結合使用 ASM 字節碼操作框架和 J2SE 5.0 的新增特性 —— instrumentation 包 —— 來在類被加載到 JVM 中時,按照注釋的控制來轉換類。

注釋基礎知識

討論 J2SE 5.0 注釋的文章已經很多了,所以在此我只作一個簡短的歸納。注釋是一種針對 Java 代碼的元數據。在功能上類似於日益普及的用於處理復雜框架配置的 XDoclet 樣式的元數據,而其實現則與 C# 屬性有更多的共同點。

該語言特性的 Java 實現使用一種類似於接口的結構和 Java 語言語法的一些特殊擴展。我發現大多數情況下忽略這種類似於接口的結構,而把注釋看作是名值對的 hashmap 會更清晰。每個注釋類型定義了一組與之關聯的固定名稱。每個名稱可能被賦予一個默認值,否則的話每次使用該注釋時都要定義該名稱。注釋可以被指定應用於一種特定類型的 Java 組件(如類、字段、方法,等等),甚至還可以應用於其他的注釋。(實際上,您是通過在要限制的注釋的定義上使用一個特殊的預定義注釋,來限制注釋適用的組件的。)

不同於常規接口,注釋必須在定義中使用關鍵字 @interface。同樣不同於常規接口的是,注釋只能定義不帶參數且只返回簡單值(基本類型、String、 Class、enum 類型、注釋,以及任意這些類型的數組)的“方法”。這些“方法”是與注釋關聯的值的名稱。

注釋被用作聲明時的修飾符,就像 public、final,以及其他早於 J2SE 5.0 版本的 Java 語言所定義的關鍵字修飾符。注釋的使用是由 @ 符號後面跟注釋名來表明的。如果要給注釋賦值,在注釋名後面的圓括號中以名值對的形式給出。

清單 1 展示了一個示例注釋聲明,後面是將該注釋用於某些方法的類的定義。該 LogMe 注釋用來標記應該包含在應用程序的日志記錄中的方法。我已經給該注釋賦了兩個值:一個表示該調用被包含其中的日志記錄的級別,另一個表示用於該方法調用的名稱(默認是空字符串,假定沒有名稱時,處理該注釋的代碼將代入實際的方法名)。然後我將該注釋用於 StringArray 類中的兩個方法,對 merge() 方法只使用默認值,對 indexOf() 方法則提供顯式值。

清單 1. 反射代替接口及其實現

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
  * Annotation for method to be included in logging.
  */
@Target({ElementType.METHOD})
public @interface LogMe {
   int level() default 0;
   String name() default "";
}
public class StringArray
{
   private final String[] m_list;

   public StringArray(String[] list) {
     ...
   }

   public StringArray(StringArray base, String[] adds) {
     ...
   }

   @LogMe private String[] merge(String[] list1, String[]list2) {
     ...
   }

   public String get(int index) {
     return m_list[index];
   }

   @LogMe(level=1, name="lookup") public int indexOf(String value) {
     ...
   }

   public int size() {
     return m_list.length;
   }
}

下一小節我將介紹一個不同的(我認為是更有趣的)應用程序。

構建 toString() 方法

Java 平台提供了一個方便的掛鉤,以生成 toString() 方法形式的對象的文本描述。最終基類 java.lang.Object 提供了該方法的一個默認實現,但是仍鼓勵重寫默認實現以提供更有用的描述。許多開發人員習慣提供自己的實現,至少對於那些基本上是數據表示的類是這樣。我要先承認我不是其中之一 —— 我常常認為 toString() 非常有用,一般不會費心去重寫默認實現。為了更有用些,當從類中添加或刪除字段時,toString() 實現需要保持最新。而我發現總的來說這一步太麻煩而不值得實現。

把注釋與類文件修改組合起來可以提供一種走出這一困境的方法。我所遇到的維護 toString() 方法的問題是由於代碼與類中的字段聲明分離了,這意味著每次添加或刪除字段時還有一個需要記得更改的東西。通過在字段聲明時使用注釋,可以很容易地表明想要在 toString() 方法中包含哪些字段,而把該方法的實際實現留給 classworking 工具。這樣,所有的東西都在一個地方(字段聲明中),而且獲得了 toString() 的有用的描述而無需維護代碼。

源代碼示例

在實現 toString() 方法結構的注釋之前,我將給出要實現的代碼示例。清單 2 展示了源代碼中包含 toString() 方法的示例數據保持類:

清單 2. 帶有 toString() 方法的數據類

public class Address
{
   private String m_street;
   private String m_city;
   private String m_state;
   private String m_zip;

   public Address() {}
   public Address(String street, String city, String state, String zip) {
     m_street = street;
     m_city = city;
     m_state = state;
     m_zip = zip;
   }
   public String getCity() {
     return m_city;
   }
   public void setCity(String city) {
     m_city = city;
   }
   ...
   public String toString() {
     StringBuffer buff = new StringBuffer();
     buff.append("Address: street=");
     buff.append(m_street);
     buff.append(", city=");
     buff.append(m_city);
     buff.append(", state=");
     buff.append(m_state);
     buff.append(", zip=");
     buff.append(m_zip);
     return buff.toString();
   }
}

對於清單 2 的示例,我選擇在 toString() 輸出中包含所有的字段,字段順序與其在類中聲明的順序相同,並以“name=”文本來開始每個字段值,以在輸出中標識它們。對於本例,文本是通過剝去用來標識成員字段的“m_”前綴,來直接從字段名生成的。在其他情況下,我可能想要在輸出中僅包含某些字段、更改順序、更改用於值的標識符文本,或者甚至完全跳過標識符文本。注釋格式靈活得足以表示所有的可能。

定義注釋

可以以多種方式為 toString() 的生成定義注釋。為使它真正有用,我情願最小化所需的注釋數目,可能通過使用類注釋來標志我想要在其中生成方法的類,並使用單個的字段注釋來重寫字段的默認處理。這並不太難做到,但是實現代碼將變得相當復雜。對於本文來說,我想使它保持簡單,因此只使用包含在實例的描述中的單個字段的注釋。

我想要控制的因素有:要包含哪些字段,字段值是否有前導文本,該文本是否基於字段名,以及字段在輸出中的順序。清單 3 給出了一個針對該目的的基本注釋:

清單 3. toString() 生成的注釋

package com.sosnoski.asm;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
public @interface ToString {
   int order() default 0;
   String text() default "";
}

清單 3 的注釋只定義了一對命名值,給出了順序和用於一個字段的前導文本。我已經用 @Target 行將該注釋的使用限定到字段聲明。我還為每個值定義了默認值。這些默認值並不應用於成為二進制類表示的生成的注釋信息(只有當注釋在運行時作為偽接口被訪問時,它們才應用,而我不會這麼做),所以我實際上並不關心使用什麼值。我只是通過定義默認值,使值是可選的,而不必在每次使用注釋時都指定它們。

使用注釋時要記住的一個因素是,命名值必須始終是編譯時常量,而且不能為 null。該規則適用於默認值(如果指定的話)和由用戶設置的值。我猜測這個決定是基於與早期 Java 語言定義的一致性而做出的,但是我覺得奇怪的是,對 Java 語言做出如此重大修改的規范,卻只局限於這一方面的一致性。

實現生成

既然已經打好了基礎,就該研究實現 classworking 轉換了:當載入帶注釋的類時向它們添加 toString() 方法。該實現涉及三個單獨的代碼段:截獲 classloading、訪問注釋信息和實際轉換。

用 instrumentation 來截獲

J2SE 5.0 給 Java 平台添加了許多特性。就我個人而言,我並不認為所有這些添加的特性都是改進。但是,有兩個不太引人注意的新特性確實對 classworking 很有用,就是 java.lang.instrument 包和 JVM 接口,它們使您可以指定將在執行程序時使用的類轉換代理,當然還有其他功能。

要使用轉換代理,需要在啟動 JVM 時指定代理類。當使用 java 命令來運行 JVM 時,可以使用命令行參數,以 -javaagent:jarpath[=options] 的形式來指定代理,其中“jarpath”是到包含代理類的 JAR 文件的路徑,而“options”是代理的參數串。代理 JAR 文件使用一個特殊的清單屬性來指定實際的代理類,這必須定義一個方法: public static void premain(String options, Instrumentation inst)。 該代理 premain() 方法將先於應用程序的 main() 方法調用,而且能夠使用傳入的 java.lang.instrument.Instrumentation 類實例注冊實際的轉換器。

該轉換器類必須實現 java.lang.instrument.ClassFileTransformer 接口,後者定義了一個 transform() 方法。當使用 Instrumentation 類實例注冊一個轉換器實例時,將會為在 JVM 中創建的每個類調用該轉換器實例。轉換器將獲得到二進制類表示的訪問,並且可以在類表示被 JVM 加載之前修改它。

清單 4 給出了處理注釋的代理和轉換器類(在本例中是同一個類,但是這二者不一定要相同)實現。 transform() 實現使用 ASM 來掃描提供的二進制類表示,並尋找適當的注釋,收集關於該類的帶注釋字段的信息。如果找到帶注釋的字段,該類將被修改以包含生成的 toString() 方法,而修改後的二進制表示將被返回。否則 transform() 方法只返回 null,表明沒有必要進行修改。

清單 4. 代理和轉換器類

package com.sosnoski.asm;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class ToStringAgent implements ClassFileTransformer
{
   // transformer interface implementation
   public byte[] transform(ClassLoader loader, String cname, Class class,
     ProtectionDomain domain, byte[] bytes)
     throws IllegalClassFormatException {
     System.out.println("Processing class " + cname);
     try {

       // scan class binary format to find fields for toString() method
       ClassReader creader = new ClassReader(bytes);
       FieldCollector visitor = new FieldCollector();
       creader.accept(visitor, true);
       FieldInfo[] fields = visitor.getFields();
       if (fields.length > 0) {

         // annotated fields present, generate the toString() method
         System.out.println("Modifying " + cname);
         ClassWriter writer = new ClassWriter(false);
         ToStringGenerator gen = new ToStringGenerator(writer,
             cname.replace('.', '/'), fields);
         creader.accept(gen, false);
         return writer.toByteArray();

       }
     } catch (IllegalStateException e) {
       throw new IllegalClassFormatException("Error: " + e.getMessage() +
         " on class " + cname);
     }
     return null;
   }

   // Required method for instrumentation agent.
   public static void premain(String arglist, Instrumentation inst) {
     inst.addTransformer(new ToStringAgent());
   }
}

J2SE 5.0 的 instrumentation 特性遠遠不止是我在此所展示的,它包括訪問加載到 JVM 中的所有類,甚至重定義已有類(如果 JVM 支持的話)的能力。對於本文,我將跳過其他的特性,繼續來看用於處理注釋和修改類的 ASM 代碼。

累積元數據

ASM 2.0 使處理注釋變得更容易了。正如您在 上個月的文章 中了解到的,ASM 使用 visitor 的方法來報告類數據的所有組件。J2SE 5.0 注釋是使用 org.objectweb.asm.AnnotationVisitor 接口報告的。該接口定義了幾個方法,其中我將只使用兩個:visitAnnotation() 是處理注釋時調用的方法,而 visit() 是處理注釋的特定的名值對時調用的方法。我還需要實際字段信息,這是使用基本 org.objectweb.asm.ClassVisitor 接口中的 visitField() 方法報告的。

實現感興趣的兩個接口的所有方法將是冗長乏味的,但幸運的是 ASM 提供了一個方便的 org.objectweb.asm.commons.EmptyVisitor 類,作為編寫自己的 visitor 的基礎。EmptyVisitor 只是提供了所有不同種類的 visitor 的空的實現,允許您只對感興趣的 visitor 方法建子類和重寫。清單 5 給出了擴展 EmptyVisitor 類而得到的處理 ToString 注釋的 FieldCollector 類。清單中也包含了用來保存收集的字段信息的 FieldInfo 類。

清單 5. 處理類的注釋

package com.sosnoski.asm;
import java.util.ArrayList;
import java.util.Arrays;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.EmptyVisitor;
/**
  * Visitor implementation to collect field annotation information from class.
  */
public class FieldCollector extends EmptyVisitor
{
   private boolean m_isIncluded;
   private int m_fieldAccess;
   private String m_fieldName;
   private Type m_fieldType;
   private int m_fieldOrder;
   private String m_fieldText;
   private ArrayList m_fields = new ArrayList();

   // finish field handling, once we're past it
   private void finishField() {
     if (m_isIncluded) {
       m_fields.add(new FieldInfo(m_fieldName, m_fieldType,
         m_fieldOrder, m_fieldText));
     }
     m_isIncluded = false;
   }

   // return array of included field information
   public FieldInfo[] getFields() {
     finishField();
     FieldInfo[] infos =
       (FieldInfo[])m_fields.toArray(new FieldInfo[m_fields.size()]);
     Arrays.sort(infos);
     return infos;
   }

   // process field found in class
   public FieldVisitor visitField(int access, String name, String desc,
     String sig, Object init) {

     // finish processing of last field
     finishField();

     // save information for this field
     m_fieldAccess = access;
     m_fieldName = name;
     m_fieldType = Type.getReturnType(desc);
     m_fieldOrder = Integer.MAX_VALUE;

     // default text is empty if non-String object, otherwise from field name
     if (m_fieldType.getSort() == Type.OBJECT &&
       !m_fieldType.getClassName().equals("java.lang.String")) {
       m_fieldText = "";
     } else {
       String text = name;
       if (text.startsWith("m_") && text.length() > 2) {
         text = Character.toLowerCase(text.charAt(2)) +
           text.substring(3);
       }
       m_fieldText = text;
     }
     return super.visitField(access, name, desc, sig, init);
   }

   // process annotation found in class
   public AnnotationVisitor visitAnnotation(String sig, boolean visible) {

     // flag field to be included in representation
     if (sig.equals("Lcom/sosnoski/asm/ToString;")) {
       if ((m_fieldAccess & Opcodes.ACC_STATIC) == 0) {
         m_isIncluded = true;
       } else {
         throw new IllegalStateException("ToString " +
           "annotation is not supported for static field +" +
           " m_fieldName");
       }
     }
     return super.visitAnnotation(sig, visible);
   }

   // process annotation name-value pair found in class
   public void visit(String name, Object value) {

     // ignore anything except the pair defined for toString() use
     if ("order".equals(name)) {
       m_fieldOrder = ((Integer)value).intValue();
     } else if ("text".equals(name)) {
       m_fieldText = value.toString();
     }
   }
}
package com.sosnoski.asm;
import org.objectweb.asm.Type;
/**
  * Information for field value to be included in string representation.
  */
public class FieldInfo implements Comparable
{
   private final String m_field;
   private final Type m_type;
   private final int m_order;
   private final String m_text;

   public FieldInfo(String field, Type type, int order,
     String text) {
     m_field = field;
     m_type = type;
     m_order = order;
     m_text = text;
   }
   public String getField() {
     return m_field;
   }
   public Type getType() {
     return m_type;
   }
   public int getOrder() {
     return m_order;
   }
   public String getText() {
     return m_text;
   }

   /* (non-Javadoc)
    * @see java.lang.Comparable#compareTo(java.lang.Object)
    */
   public int compareTo(Object comp) {
     if (comp instanceof FieldInfo) {
       return m_order - ((FieldInfo)comp).m_order;
     } else {
       throw new IllegalArgumentException("Wrong type for comparison");
     }
   }
}

清單 5 的代碼保存了訪問字段時的字段信息,因為如果該字段有注釋呈現的話,以後將會需要該信息。當訪問注釋時,該代碼審查它是否是 ToString 注釋,如果是,設置一個標志,說明當前字段應該被包含在用於生成 toString() 方法的列表中。當訪問一個注釋名值對時,該代碼審查由 ToString 注釋定義的兩個名稱,當找到時,保存每個名稱的值。這些名稱的真正默認值(與在注釋定義中使用的默認值相對)是在字段的 visitor 方法中設置的,所以任意由用戶指定的值都將重寫這些默認值。

ASM 首先訪問字段,接著訪問注釋和注釋值。因為在處理字段的注釋時,沒有特定的方法可以調用,所以當處理一個新字段和當需要字段的完成列表時,我會調用一個 finishField() 方法。getFields() 方法向調用者提供字段的完成列表,以由注釋值所確定的順序排列。

轉換類

清單 6 展示了實現代碼的最後部分,它實際上向類添加了 toString() 方法。該代碼與 上個月的文章 中使用 ASM 構造一個類的代碼類似,但是需要另外構造以修改一個已有的類。這裡,ASM 使用的 visitor 方法增加了復雜性 —— 要修改一個已有的類,需要訪問所有的當前類目錄,並把它傳遞給類編寫者。org.objectweb.asm.ClassAdapter 是針對此目的的一個方便的基類。它實現了對提供的類編寫者實例的傳遞處理,使您可以只重寫需要特殊處理的方法。

清單 6. 添加 toString() 方法

package com.sosnoski.asm;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
  * Visitor to add <code>toString</code> method to a class.
  */
public class ToStringGenerator extends ClassAdapter
{
   private final ClassWriter m_writer;
   private final String m_internalName;
   private final FieldInfo[] m_fields;

   public ToStringGenerator(ClassWriter cw, String iname, FieldInfo[] props) {
     super(cw);
     m_writer = cw;
     m_internalName = iname;
     m_fields = props;
   }

   // called at end of class
   public void visitEnd() {

     // set up to build the toString() method
     MethodVisitor mv = m_writer.visitMethod(Opcodes.ACC_PUBLIC,
       "toString", "()Ljava/lang/String;", null, null);
     mv.visitCode();

     // create and initialize StringBuffer instance
     mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuffer");
     mv.visitInsn(Opcodes.DUP);
     mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuffer",
       "<init>", "()V");

     // start text with class name
     String name = m_internalName;
     int split = name.lastIndexOf('/');
     if (split >= 0) {
       name = name.substring(split+1);
     }
     mv.visitLdcInsn(name + ":");
     mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
       "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;");

     // loop through all field values to be included
     boolean newline = false;
     for (int i = 0; i < m_fields.length; i++) {

       // check type of field (objects other than Strings need conversion)
       FieldInfo prop = m_fields[i];
       Type type = prop.getType();
       boolean isobj = type.getSort() == Type.OBJECT &&
         !type.getClassName().equals("java.lang.String");

       // format lead text, with newline for object or after object
       String lead = (isobj || newline) ? "\n " : " ";
       if (prop.getText().length() > 0) {
         lead += prop.getText() + "=";
       }
       mv.visitLdcInsn(lead);
       mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
         "java/lang/StringBuffer", "append",
         "(Ljava/lang/String;)Ljava/lang/StringBuffer;");

       // load the actual field value and append
       mv.visitVarInsn(Opcodes.ALOAD, 0);
       mv.visitFieldInsn(Opcodes.GETFIELD, m_internalName,
         prop.getField(), type.getDescriptor());
       if (isobj) {

         // convert objects by calling toString() method
         mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
           type.getInternalName(), "toString",
           "()Ljava/lang/String;");
         mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
           "java/lang/StringBuffer", "append",
           "(Ljava/lang/String;)Ljava/lang/StringBuffer;");

       } else {

         // append other types directly to StringBuffer
         mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
           "java/lang/StringBuffer", "append", "(" +
           type.getDescriptor() + ")Ljava/lang/StringBuffer;");

       }
       newline = isobj;
     }

     // finish the method by returning accumulated text
     mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
       "toString", "()Ljava/lang/String;");
     mv.visitInsn(Opcodes.ARETURN);
     mv.visitMaxs(3, 1);
     mv.visitEnd();
     super.visitEnd();
   }
}

在清單 6 中,需要重寫的惟一方法就是 visitEnd() 方法。該方法在所有的已有類信息都已經被訪問之後調用,所以它對於添加新內容非常方便。我已經用 visitEnd() 方法向正在處理的類添加 toString() 方法。在代碼生成中,我已經添加了一些用於精密地格式化 toString() 輸出的特性,但是基本原理很簡單 —— 只是循環遍歷字段數組,生成代碼,該代碼首先向 StringBuffer 實例追加前導文本,然後追加實際字段值。

因為當前的代碼將只使用 J2SE 5.0(由於使用了 instrumentation 方法來截獲 classloading),所以我本應該使用新的 StringBuilder 類作為 StringBuffer 的更有效的等價物。我之所以選擇使用以前的方案,是因為下一篇文章中我將使用該代碼進行一些後續工作,但是您應該記住 StringBuilder 以便用於您自己的特定於 J2SE 5.0 的代碼。

運行 ToString

清單 7 展示了 ToString 注釋的一些測試類。我對實際注釋使用了混合樣式,在一些情況中指定了名值對,而其他的則只使用注釋本身。Run 類創建帶示例數據的 Customer 類實例,並打印出 toString() 方法調用的結果。

清單 7. ToString 的測試類

package com.sosnoski.dwct;
import com.sosnoski.asm.ToString;
public class Customer
{
   @ToString(order=1, text="#") private long m_number;
   @ToString() private String m_homePhone;
   @ToString() private String m_dayPhone;
   @ToString(order=2) private Name m_name;
   @ToString(order=3) private Address m_address;

   public Customer() {}
   public Customer(long number, Name name, Address address, String homeph,
     String dayph) {
     m_number = number;
     m_name = name;
     m_address = address;
     m_homePhone = homeph;
     m_dayPhone = dayph;
   }
   ...
}
...
public class Address
{
   @ToString private String m_street;
   @ToString private String m_city;
   @ToString private String m_state;
   @ToString private String m_zip;

   public Address() {}
   public Address(String street, String city, String state, String zip) {
     m_street = street;
     m_city = city;
     m_state = state;
     m_zip = zip;
   }
   public String getCity() {
     return m_city;
   }
   public void setCity(String city) {
     m_city = city;
   }
   ...
}
...
public class Name
{
   @ToString(order=1, text="") private String m_first;
   @ToString(order=2, text="") private String m_middle;
   @ToString(order=3, text="") private String m_last;

   public Name() {}
   public Name(String first, String middle, String last) {
     m_first = first;
     m_middle = middle;
     m_last = last;
   }
   public String getFirst() {
     return m_first;
   }
   public void setFirst(String first) {
     m_first = first;
   }
   ...
}
...
public class Run
{
   public static void main(String[] args) {
     Name name = new Name("Dennis", "Michael", "Sosnoski");
     Address address = new Address("1234 5th St.", "Redmond", "WA", "98052");
     Customer customer = new Customer(12345, name, address,
       "425 555-1212", "425 555-1213");
     System.out.println(customer);
   }
}

最後,清單 8 展示了測試運行的控制台輸出(首行被折行以適合屏幕):

清單 8. 測試運行的控制台輸出(首行被折行)

[dennis@notebook code]$ java -cp lib/asm-2.0.RC1.jar:lib/asm-commons-2.0.RC1.jar
  :lib/tostring-agent.jar:classes -javaagent:lib/tostring-agent.jar
  com.sosnoski.dwct.Run
Processing class sun/misc/URLClassPath$FileLoader$1
Processing class com/sosnoski/dwct/Run
Processing class com/sosnoski/dwct/Name
Modifying com/sosnoski/dwct/Name
Processing class com/sosnoski/dwct/Address
Modifying com/sosnoski/dwct/Address
Processing class com/sosnoski/dwct/Customer
Modifying com/sosnoski/dwct/Customer
Customer: #=12345
  Name: Dennis Michael Sosnoski
  Address: street=1234 5th St. city=Redmond state=WA zip=98052
  homePhone=425 555-1212 dayPhone=425 555-1213

結束語

我已經演示了如何使用 ASM 和 J2SE 5.0 注釋來完成自動的運行時類文件修改。我用作例子的 ToString 注釋是有趣而且(至少對於我來說)比較有用的。單獨使用時,並不妨礙代碼的可讀性。但是注釋如果被用於各種不同目的(這種情況將來肯定要發生,因為有如此多的 Java 擴展正在編寫或重寫以使用注釋),就很有可能會影響代碼的可讀性。

當我在後面的文章中研究注釋和外部配置文件的權衡時,我會再回到這個問題上。我個人的觀點是,二者都有自己的作用,雖然注釋基本上是作為配置文件的更容易的替代方案而開發的,但是獨立的配置文件在某些情況下仍然適用。明確地講,我認為 ToString 注釋是一個適當使用的例子!

使用 J2SE 5.0 擴展的一個局限是 JDK 1.5 編譯器輸出只能與 JDK 1.5 JVM 一起使用。下一篇 Classworking 工具箱 文章,我將介紹一個克服該局限的工具,並展示如何修改 ToString 實現以運行在以前的JVM 上。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved