到 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 上。
本文配套源碼