天天看點

Classworking 工具箱: 注釋(Annotation)與 ASM 自動化運作時類檔案修改

簡介: 您是否厭倦了為所有的資料類建構和維護 

toString()

 方法?在本期的 Classworking 工具箱文章中,Dennis Sosnoski 顧問向您展示了如何使用 J2SE 5.0 注釋和 ASM 位元組碼操作架構來自動化該過程。他使用新增的 J2SE 5.0 instrumentation API,來在類被載入 JVM 中時調用 ASM,以提供運作時的動态類修改。

到 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. 測試運作的控制台輸出(首行被折行)

[[email protected] 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 上。

文章出處:developerWorks