# 6.1 接口和组件

# 6.1.1 介绍

用于生成和转换已编译 Java 类的 ASM 树 API 是基于 ClassNode 类的(见图 6.1)。

图 6.1 ClassNode 类(仅给出了字段)

public class ClassNode ...{
    public int version;
    public int access;
    public String name;
    public String signature;
    public String superName;
    public List<String> interfaces;
    public String sourceFile;
    public String sourceDebug;
    public String outerClass;
    public String outerMethod;
    public String outerMethodDesc;
    public List<AnnotationNode> visibleAnnotations;
    public List<AnnotationNode> invisibleAnnotations;
    public List<Attribute> attrs;
    public List<InnerClassNode> innerClasses;
    public List<FieldNode> fields;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以看出,这个类的公共字段对应于图 2.1 中给出的类文件结构部分。这些字段的内容与核心 API 相同。例如,name 是一个内部名字,signature 是一个类签名(见 2.1.2 节和 4.1 节)。一些字段包含其他 XxxNode 类:这些类将在随后各章详细介绍,它们拥有一种类似的结构,即拥有一些字段,对应于类文件结构的子部分。例如,FieldNode 类看起来是这样的:

public class FieldNode ...{

    public int access;
    public String name;
    public String desc;
    public String signature;
    public Object value;
    public FieldNode(int access,String name,String desc,String signature,Object value){
            ...
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12

MethodNode 类是类似的:

public class MethodNode ... { 
    public int access;
    public String name; 
    public String desc; 
    public String signature;
    public List<String> exceptions;
    ...
    public MethodNode(int access, String name, String desc, String signature, String[] exceptions){
    ...
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 6.1.2 生成类

用树 API 生成类的过程就是:创建一个 ClassNode 对象,并初始化它的字段。例如,2.2.3 节的 Comarable 接口可用如下代码生成(其代码数量大体与 2.2.3 节相同):

ClassNode cn = new ClassNode(); 
cn.version = V1_5;
cn.access = ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE;
cn.name = "pkg/Comparable"; cn.superName = "java/lang/Object"; cn.interfaces.add("pkg/Mesurable");
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1)));
cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null));
1
2
3
4
5
6
7
8

使用树 API 生成类时,需要多花费大约 30%的时间(见附录 A.1),占用的内存也多于使用核心 API。但可以按任意顺序生成类元素,这在一些情况下可能非常方便。

# 6.1.3 添加和删除类成员

添加和删除类就是在 ClassNode 对象的 fields 或 methods 列表中添加或删除元素。例如,如果像下面这样定义了 ClassTransformer 类,以便能够轻松地编写类转换器:

public class ClassTransformer { 
    protected ClassTransformer ct;
    
    public ClassTransformer(ClassTransformer ct) { 
        this.ct = ct;
    }
    
    public void transform(ClassNode cn) { 
        if (ct != null) {
            ct.transform(cn);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

则 2.2.5 节中的 RemoveMethodAdapter 可实现如下:

public class RemoveMethodTransformer extends ClassTransformer {
    private String methodName;
    private String methodDesc;

    public RemoveMethodTransformer(ClassTransformer ct, String methodName, String methodDesc) {
        super(ct);
        this.methodName = methodName;
        this.methodDesc = methodDesc;
    }

    @Override
    public void transform(ClassNode cn) {
        Iterator<MethodNode> i = cn.methods.iterator();
        while (i.hasNext()) {
            MethodNode mn = i.next();
            if (methodName.equals(mn.name) && methodDesc.equals(mn.desc)) {
                i.remove();
            }
        }
        super.transform(cn);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

可以看出,它与核心 API 的主要区别是需要迭代所有方法,而在使用核心 API 时是不需要这样做的(这一工作会在 ClassReader 中为你完成)。事实上,这一区别对于几乎所有基于树的转换都是有效的。例如,在用树 API 实现 2.2.6 节的 AddFieldAdapter 时,它还需要一个迭代器:

public class AddFieldTransformer extends ClassTransformer {
    private int fieldAccess;
    private String fieldName;
    private String fieldDesc;

    public AddFieldTransformer(ClassTransformer ct, int fieldAccess, String fieldName, String fieldDesc) {
        super(ct);
        this.fieldAccess = fieldAccess;
        this.fieldName = fieldName;
        this.fieldDesc = fieldDesc;
    }

    @Override
    public void transform(ClassNode cn) {
        boolean isPresent = false;
        for (FieldNode fn : cn.fields) {
            if (fieldName.equals(fn.name)) {
                isPresent = true;
                break;
            }
        }
        if (!isPresent) {
            cn.fields.add(new FieldNode(fieldAccess, fieldName, fieldDesc,

                    null, null));
        }
        super.transform(cn);
    }
}
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
27
28
29

和生成类的情景一样,使用树 API 转换类时,所花费的时间和占用的内存也要多于使用核心 API 的时候。但使用树 API 有可能使一些转换的实现更为容易。比如有一个转换,要向一个类中添加注释,包含其内容的数字签名,就属于上述情景。在使用核心 API 时,只有在访问了整个类之后才能计算数字签名,但这时再添加包含其内容的注释就太晚了,因为对注释的访问必须位于类成员之前。而在使用树 API 时,这个问题就消失了,因为这时不存在此种限制。

事实上,有可能用核心 API 实现 AddDigitialSignature 示例,但随后,必须分两遍来转换这个类。第一遍,首先用一个 ClassReader(没有 ClassWriter)来访问这个类,以根据类的内容来计算数字签名。在第二遍,重复利用同一个 ClassReader 对类进行第一次访问, 这一次是向一个 ClassWriter 链接一个 AddAnnotationAdapter。通过推广这一论述过程, 我们可以看出,事实上,任何转换都可以仅用核心 API 来实现,只需在必要时分几遍完成。但这样就提高了转换代码的复杂性,要求在各遍之间存储状态(这种状态可能非常复杂,需要一个完整的树形表示!),而且对一个类进行多次分析是有成本的,必需将这一成本与构造相应 ClassNode 的成本进行比较。

结论是:树 API 通常用于那些不能由核心 API 一次实现的转换。但当然也存在例外。例如一个混淆器不能由核心 API 一遍实现,因为必须首先在原名称和混淆后的名字之间建立了完整的映射之后,才可能转换类,而这个映射的建立需要对所有类进行分析。但树 API 也不是一个好的解决方案,因为它需要将所有待混淆类的对象表示保存在内存中。在这种情况下,最好是分两遍使用核心 API:一遍用于计算原名与混淆后名称之间的映射(一个简单的散列表,它需要的内存要远少于所有类的完整对象表示),另一遍用于根据这一映射来转换类。