Classworking 工具箱: 注释(Annotation)与 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 toString
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”,
“
// 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 上。
欢迎转载,请注明出处:亲亲宝宝