本节给出一些规则,在使用 ASM API 时,要想确保你的代码在所有未来 ASM 版本中都有效(其意义见上述约定),就必须遵循这些规则。
首先,如果编写一个类生成器,那不需要遵循任何规则。例如,如果正在为 ASM 4.0 编写一个类生成器,它可能包含一个类似于 visitSource(mySource, myDebug)的调用,当然不包含对 visitLicense 的调用。如果不加修改地用 ASM 5.0 运行它,它将会调用过时的 visitSource 方法 ,但 ASM 5.0 ClassWriter 将会在 内 部将它 重 定向到visitSource(null, mySource, myDebug),生成所期望的结果(但其效率要稍低于直接将代码升级为调用这个新方法)。同理,缺少对 visitLicense 的调用也不会造成问题(所生成的类版本也没有变化,人们并不指望这个版本的类中会有一个许可属性)。
另一方面,如果编写一个类分析器或类适配器,也就是说,如果重写 ClassVisitor 类(或者任何其他类似的类,比如 FieldVisitor 或 MethodVisitor),就必须遵循一些规则,如下所述。
这里考虑一个类的简单情况:直接扩展 ClassVisitor(讨论和规则对于其他访问器类都是相同的;间接子类的情景在下一节讨论)。在这种情况下,只有一条规则:
规则 1:要为 ASM X 编写一个 ClassVisitor 子类,就以这个版本号为参数,调用 ClassVisitor 构造器,在这个版本的 ClassVisitor 类中,绝对不要重写或调用弃用的方法(或者将在之后版本引入的方法)。
就这么多。在我们的示例情景中(见 5.1.2 节),为 ASM 4.0 编写的类适配器必须看起来类似于如下所示:
class MyClassAdapter extends ClassVisitor {
public MyClassAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
...
public void visitSource(String source, String debug) { // optional
...
super.visitSource(source, debug); // optional
}
}
一旦针对 ASM5.0 升级之后,必须删除 visitSource(String, String),这个类看起来必须类似于如下所示:
class MyClassAdapter extends ClassVisitor {
public MyClassAdapter(ClassVisitor cv) {
super(ASM5, cv);
}
...
public void visitSource(String author,
String source, String debug) { // optional
...
super.visitSource(author, source, debug); // optional
}
public void visitLicense(String license) { // optional
...
super.visitLicense(license); // optional
}
}
它是如何工作的呢?在 ASM4.0中,ClassVisitor 的内部实现如下:
public abstract class ClassVisitor {
int api;
ClassVisitor cv;
public ClassVisitor(int api, ClassVisitor cv) {
this.api = api;
this.cv = cv;
}
...
public void visitSource(String source, String debug) {
if (cv != null) cv.visitSource(source, debug);
}
}
在 ASM 5.0 中,这一代码变为:
public abstract class ClassVisitor {
...
public void visitSource(String source, String debug) {
if (api < ASM5) {
if (cv != null) cv.visitSource(source, debug);
} else {
visitSource(null, source, debug);
}
}
public void visitSource(Sring author, String source, String debug) {
if (api < ASM5) {
if (author == null) {
visitSource(source, debug);
} else {
throw new RuntimeException();
}
} else {
if (cv != null) cv.visitSource(author, source, debug);
}
}
public void visitLicense(String license) {
if (api < ASM5) throw new RuntimeException();
if (cv != null) cv.visitSource(source, debug);
}
}
如果 MyClassAdapter 4.0 扩展了 ClassVisitor 4.0,那一切都将如预期中一样正常工作。如果升级到 ASM 5.0,但没有修改代码,MyClassAdapter 4.0 现在将扩展 ClassVisitor 5.0。但api 字段仍将是ASM4 <ASM5,容易看出,在这种情况下,在调用visitSource(String, String)时,ClassVisitor 5.0 的行为特性类似于 ClassVisitor 4.0。此外,如果用一个 null 作者一访问新的 visitSource 方法,该调用将被重定向至旧版本。最后,如果在输入类中找到非 null 作者或许可,执行过程将会失败,与约定中的规定一致(或者是在新的 visitSource 方法中,或者是在 visitLicense 中)。
如果升级到 ASM 5.0,并同时升级代码,现在将拥有扩展了 ClassVisitor 5.0 的MyClassAdapter 5.0。api 字段现在是 ASM5,visitLicense 和新的 visitSource 方法的行为就是直接将调用委托给下一个访问者 cv。此外,旧的 visitSource 方法现在将调用重定向至新的 visitSource 方法,这样可以确保:如果在转换链中,在我们自己的类适配器之前使用了一个旧类适配器,那 MyClassAdapter 5.0 不会错过这个访问事件。
ClassReader 将总是调用每个访问方法的最新版本。因此, 如果随 ASM 4.0 使用MyClassAdapter 4.0,或者随 ASM 5.0 使用 MyClassAdapter 5.0,将不会产生重定向。只有在随 ASM 5.0 使用 MyClassAdapter 4.0 时,才会在 ClassVisitor 中发生重定向(在新 visitSource 方法的第 3 行)。因此,尽管旧代码在新 ASM 版本中仍能正常使用,但它的运行速度要慢一些。将其升级为使用新的 API,将恢复其性能。
上述规则对于 ClassVisitor 或任意其他类似类的直接子类都足够了。对于间接子类,也就是说,如果定义了一个扩展 ClassVisitor 的子类 A1,而它本身又由 A2 扩展,……它本身又由 An 扩展,则必须为同一 ASM 版本编写所有这些子类。事实上,在一个继承链中混用不同版本将导致同时重写同一方法的几个版本,比如 visitSource(String,String) 和 visitSource(String,String,String),它们的行为可能不同,导致错误或不可预测的结果。如果这些类的来源不同,每个来源被独立升级、单独发布,那几乎不可能保证这一性质。这就引出第二条规则:
规则 2:不要使用访问器的继承,而要使用委托(即访问器链)。一种好的做法是让你的访问器类在默认情况为 final 的,以确保这一特性。
事实上,这一规则有两个例外:
- 如果能够完全由自己控制继承链,并同时发布层次结构中的所有类,那就可以使用访问器的继承。但必须确保层次结构中的所有类都是为同一 ASM 版本编写的。仍然要让层次结构的叶类是 final 的。
- 如果除了叶类之外,没有其他类重写任何访问方法(例如,如果只是为了引入方便的方法而在 ClassVisitor 和具体访问类之间使用了中间类),那就可以使用“访问器”的继承。仍然要让层次结构的叶类是 final 的(除非它们也没有重写任何访问方法;在这种情况下,提供一个以 ASM 版本为参数的构造器,使子类可以指定它们是为哪个版本编写的)。