# 10.2 规则

本节给出一些规则,在使用 ASM 树 API 时,要想确保你的代码在所有未来 ASM 版本中都保持有效(其意义见 5.1.1 节定义的约定),就必须遵循这些规则。

首先,如果使用树 API 编写一个类生成器,那就不需要遵循什么规则(和核心 API 一样)。可以用任意构造器创建 ClassNode 和其他元素,可以使用这些类的任意方法。

另一方面,如果要用树 API 编写类分析器或类适配器,也就是说,如果使用 ClassNode 或其他直接或间接地通过 ClassReader.accept()填充的类似类,或者如果重写这些类中的一个,则必须遵循下面给出的规则。

# 10.2.1 基本规则

  1. 创建类节点

考虑这样一种情景,我们创建一个 ClassNode,通过一个 ClassReader 填充它,然后分析或转换它,最终根据需要用 ClassWriter 写出结果(这一讨论及相关规则同样适用于其他节点类;对于由别人创建的 ClassNode,其分析或转换在下一节讨论)。在这种情况下,仅有一条规则:

规则 3:要用 ASM 版本X 的树 API 编写类分析器或适配器,则使用以这一确切版本为参数的构造器创建 ClassNode(而不是使用没有参数的默认构造器)。

本规则的目的是在通过一个 ClassReader 填充 ClassNode 时,如果遇到未知特性,则抛出一个错误(根据后向兼容性约定的定义)。如果不遵循这一规则,在以后遇到未知元素时,你 的分析或转换代码可能会失败,也许能够成功运行,但却因为没有忽略这些未知元素而给出错误结果。换言之,如果不遵循这一规则,可能无法保证约定的最后一项条款。

如何做到呢?ASM 4.0 内部对 ClassNode 的实现如下(这里重复使用 5.1.2 节的示例):

public class ClassNode extends ClassVisitor {
    public ClassNode() {
        super(ASM4, null);
    }

    public ClassNode(int api) {
        super(api, null);
    }
    ...

    public void visitSource(String source, String debug) {
        // 将 source 和 debug 存储在局部字段中...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在 ASM 5.0 中,这一代码变为:

public class ClassNode extends ClassVisitor {
    ...

    public void visitSource(String source, String debug) {
        if (api < ASM5) {
             // 将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 {
             // 将author、source 和 debug 存储在局部字段中...
        }
    }

    public void visitLicense(String license) {
        if (api < ASM5) throw new RuntimeException();
        // 将 license 存储在局部字段中
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

如果使用 ASM 4.0,那创建 ClassNode(ASM4)没有什么特别之处。但如果升级到 ASM 5.0,但不修改代码,那就会得到一个 ClassNode 5.0,它的 api 字段将为 ASM4 < ASM5。于是容易看出,如果输入类包含一个非 null 作者或许可属性,那通过 ClassReader 填充ClassNode 时将会失败,如约定中的定义。如果还升级你的代码,将 api 字段改为 ASM5,并升级剩余代码,将这些新属性考虑在内,那在填充代码时就不会抛出错误。

注意,ClassNode 5.0 代码非常类似于 ClassVisitor 5.0 代码。这是为了确保在定义 ClassNode 的子类时能够拥有正确的语义(类似于 ClassVisitor 的子类——见 10.2.2 节)。

  1. 使用现有类代码

如果你的类分析器或适配器收到别人创建的 ClassNode,那你就不能肯定在创建它时传送 给其构造器的 ASM 版本。当然可以自行检查 api 字段,但如果发现这个版本高于你支持的版本, 直接拒绝这个类可能太过保守了。事实上,这个类中可能没有包含任何未知特性。另一方面,你不能检查是否存在未知特性(在我们的示例情景中,在为 ASM 4.0 编写代码时,你如何判断你的 ClassNode 中不存在未知的 license 字段呢?因为你在这里还不知道未来会添加这样一个字段)。于是设计了 ClassNode.check()方法来解决这个问题。这就引出了以下规则:

规则 4:要用 ASM 版本 X 的树 API 编写一个类分析器或适配器,使用别人创建的ClassNode,在以任何方式使用这个 ClassNode 之前,都要以这个确切版本号为参数,调用它的 check()方法。

其目的与规则 3 相同:如果不遵循这一规则,可能无法保证约定的最后一项条款。如何做到的呢?这个检查方法在 ASM 4.0 内部的实现如下:

public class ClassNode extends ClassVisitor {
    ...

    public void check(int api) {
    // 不做任何事
    }
}
1
2
3
4
5
6
7

在 ASM 5.0中,这一代码变为:

public class ClassNode extends ClassVisitor {
    ...

    public void check(int api) {
        if (api < ASM5 && (author != null || license != null)) {
            throw new RuntimeException();
        }
    }
}
1
2
3
4
5
6
7
8
9

如果你的代码是为 ASM 4.0 编写的,而且如果得到一个 ClassNode 4.0,它的 api 字段将为 ASM4,这样不会有问题,check 也不做任何事情。但如果你得到一个 ClassNode 5.0,如果这个节点实际上包含了非 null author 或 license,也就是说,它包含了 ASM 4.0 中未知的新特性,那 check(ASM4)方法将会失败。

注意:如果你自己创建 ClassNode,也可以使用这一规则。那就不需要遵循规则 3,也就是说,不需要在ClassNode 构造器中指明 ASM 版本。这一检查将在 check 方法中进行(但在填充 ClassNode 时,这种做法的效率要低于在之前进行检查)。

# 10.2.2 继承规则

如果希望提供 ClassNode 的子类或者其他类似节点类,那么规则 1 和 2 都是适用的。注意, 在一个 MethodNode 匿名子类的一个常用特例中,visitEnd()方法被重写:

class MyClassVisitor extends ClassVisitor {
    ...

    public MethodVisitor visitMethod(...) {
        final MethodVisitor mv = super.visitMethod(...);

        if (mv != null) {
            return new MethodNode(ASM4) {
                public void visitEnd() {
                    // perform a transformation accept(mv);
                }
            };
        }
        return mv;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

那就自动适用规则 2(匿名类不能被重写,尽管没有明确将它声明为 final 的)。你只需要遵循规则 3,也就是说,在 MehtodNode 构造器中指定 ASM 版本(或者遵循规则 4,也就是在执行转换之前调用 check(ASM4))。

# 10.2.3 其他包

asm.utilasm.commons 中的类都有两个构造函数变体:一个有 ASM 版本参数,一个没有。

如果只是希望像 asm.util 中的 ASMifier、Textifier 或 CheckXxx Adapter 类或者asm.commons 包中的任意类一样,加以实例化和应用,那可以用没有 ASM 版本参数的构造器来实例化它们。也可以使用带有 ASM 版本参数的构造器,那就会不必要地将这些组件限制于特定的 ASM 版本(而使用无参数构造器相当于在说“使用最新的 ASM 版本”)。这就是为什么使用 ASM 版本参数的构造器被声明为 protected。

另一方面,如果希望重写 asm.util 中的 ASMifier、Textifier 或 CheckXxx Adapter 类或者 asm.commons 包中的任意类,那适用规则 1 和 2。具体来说,你的构造器必须以你希望用作参数的 ASM 版本来调用 super(…)。

最后,如果希望使用或重写 asm.tree.analysis 中的 Interpreter 类或其子类,必须做出同样的区分。还要注意,在使用这个分析包之前,创建一个 MethodNode 或者从别人那里获取一个,那在将这一代码传送给 Analyzer 之前必须使用规则 3 和 4。