Classworking 工具箱: 注释(Annotation)与 ASM

自动化运行时类文件修改

您是否厌倦了为所有的数据类构建和维护 toString() 方法?在本期的 Classworking 工具箱文章中,Dennis Sosnoski 顾问向您展示了如何使用 J2SE 5.0 注释和 ASM 字节码操作框架来自动化该过程。他使用新增的 J2SE 5.0 instrumentation API,来在类被载入 JVM 中时调用 ASM,以提供运行时的动态类修改。

Dennis Sosnoski (dms@sosnoski.com), Java 和 XML 顾问, Sosnoski Software Solutions, Inc.

Dennis Sosnoski 的照片Dennis Sosnoski 是西雅图地区 Java 技术咨询公司 Sosnoski Software Solutions, Inc. 的创始人和首席顾问,还是 J2EE、XML 和 Web 服务支持方面的专家。他有 30 多年的专业软件开发经验,最近几年致力于研究服务器端的 Java 技术。Dennis 经常在全国性的会议上做关于 XML 和 Java 技术的演讲,并且还是 Seattle Java-XML SIG 的主席。可以通过 dms@sosnoski.com 联系 Dennis。



2005 年 6 月 01 日

到 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。同样不同于常规接口的是,注释只能定义不带参数且只返回简单值(基本类型、StringClassenum 类型、注释,以及任意这些类型的数组)的“方法”。这些“方法”是与注释关联的值的名称。

注释被用作声明时的修饰符,就像 publicfinal,以及其他早于 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 上。


下载

描述名字大小
Sample codej-cwt06075code.zip175 KB

参考资料

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=86152
ArticleTitle=Classworking 工具箱: 注释(Annotation)与 ASM
publish-date=06012005